From abf386b5908ef40f519d42c50af0c80c0ee927c6 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Wed, 21 May 2025 16:35:39 -0500 Subject: [PATCH 1/9] Begin implementing SteamMatchmaking --- .../steam/handlers/steammatchmaking/Filter.kt | 171 ++++++++++++++++++ .../steam/handlers/steammatchmaking/Lobby.kt | 78 ++++++++ .../handlers/steammatchmaking/LobbyCache.kt | 106 +++++++++++ .../steam/handlers/steammatchmaking/Member.kt | 32 ++++ .../callback/CreateLobbyCallback.kt | 24 +++ .../callback/GetLobbyListCallback.kt | 24 +++ .../callback/JoinLobbyCallback.kt | 24 +++ .../callback/LeaveLobbyCallback.kt | 24 +++ .../callback/LobbyDataCallback.kt | 21 +++ .../callback/SetLobbyDataCallback.kt | 24 +++ .../callback/SetLobbyOwnerCallback.kt | 24 +++ .../callback/UserJoinedLobbyCallback.kt | 20 ++ .../callback/UserLeftLobbyCallback.kt | 20 ++ .../steam/steamclient/SteamClient.kt | 8 +- .../steam/steamclient/SteamClientTest.java | 2 + 15 files changed, 600 insertions(+), 2 deletions(-) create mode 100644 src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt create mode 100644 src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt create mode 100644 src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/LobbyCache.kt create mode 100644 src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Member.kt create mode 100644 src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/CreateLobbyCallback.kt create mode 100644 src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/GetLobbyListCallback.kt create mode 100644 src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/JoinLobbyCallback.kt create mode 100644 src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LeaveLobbyCallback.kt create mode 100644 src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LobbyDataCallback.kt create mode 100644 src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyDataCallback.kt create mode 100644 src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyOwnerCallback.kt create mode 100644 src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserJoinedLobbyCallback.kt create mode 100644 src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserLeftLobbyCallback.kt diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt new file mode 100644 index 00000000..61106b07 --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt @@ -0,0 +1,171 @@ +package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking + +import `in`.dragonbra.javasteam.enums.ELobbyComparison +import `in`.dragonbra.javasteam.enums.ELobbyDistanceFilter +import `in`.dragonbra.javasteam.enums.ELobbyFilterType +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms + +/** + * The lobby filter base class. + * + * @constructor Base constructor for all filter sub-classes. + * @param filterType The type of filter. + * @param key The metadata key this filter pertains to. + * @param comparison The comparison method used by this filter. + * + * @property filterType The type of filter. + * @property key The metadata key this filter pertains to. Under certain circumstances e.g. a distance filter, this will be an empty string. + * @property comparison The comparison method used by this filter. + */ +abstract class Filter( + val filterType: ELobbyFilterType, + val key: String, + val comparison: ELobbyComparison, +) { + /** + * Serializes the filter into a representation used internally by SteamMatchmaking. + * @return A protobuf serializable representation of this filter. + */ + open fun serialize(): SteammessagesClientserverMms.CMsgClientMMSGetLobbyList.Filter.Builder = + SteammessagesClientserverMms.CMsgClientMMSGetLobbyList.Filter.newBuilder().apply { + this.filterType = filterType + this.key = key + this.comparision = comparison.code() + } +} + +/** + * Can be used to filter lobbies geographically (based on IP according to Steam's IP database). + * + * @constructor Initializes a new instance of the [DistanceFilter] class. + * @param value Steam distance filter value. + * + * @property value Steam distance filter value. + */ +class DistanceFilter( + val value: ELobbyDistanceFilter, +) : Filter( + filterType = ELobbyFilterType.Distance, + key = "", + comparison = ELobbyComparison.Equal +) { + /** + * Serializes the distance filter into a representation used internally by SteamMatchmaking. + * @return A protobuf serializable representation of this filter. + */ + override fun serialize(): SteammessagesClientserverMms.CMsgClientMMSGetLobbyList.Filter.Builder = + super.serialize().apply { + this.value = this@DistanceFilter.value.code().toString() + } +} + +/** + * Can be used to filter lobbies with a metadata value closest to the specified value. Multiple + * near filters can be specified, with former filters taking precedence over latter filters. + * + * @constructor Initializes a new instance of the [NearValueFilter] class. + * @param key The metadata key this filter pertains to. + * @param value Integer value to compare against + * + * @param value Integer value that lobbies' metadata value should be close to. + */ +sealed class NearValueFilter( + key: String, + val value: Int, +) : Filter( + filterType = ELobbyFilterType.NearValue, + key = key, + comparison = ELobbyComparison.Equal +) { + /** + * Serializes the slots available filter into a representation used internally by SteamMatchmaking. + * @return A protobuf serializable representation of this filter. + */ + override fun serialize(): SteammessagesClientserverMms.CMsgClientMMSGetLobbyList.Filter.Builder = + super.serialize().apply { + this.value = this@NearValueFilter.value.toString() + } +} + +/** + * Can be used to filter lobbies by comparing an integer against a value in each lobby's metadata. + * + * @constructor Initializes a new instance of the [NumericalFilter] class. + * @param key The metadata key this filter pertains to. + * @param comparison The comparison method used by this filter. + * @param value Integer value to compare against. + * + * @property value Integer value to compare against. + */ +sealed class NumericalFilter( + key: String, + comparison: ELobbyComparison, + val value: Int, +) : Filter( + filterType = ELobbyFilterType.Numerical, + key = key, + comparison = comparison +) { + /** + * Serializes the numerical filter into a representation used internally by SteamMatchmaking. + * @return A protobuf serializable representation of this filter. + */ + override fun serialize(): SteammessagesClientserverMms.CMsgClientMMSGetLobbyList.Filter.Builder = + super.serialize().apply { + this.value = this@NumericalFilter.value.toString() + } +} + +/** + * Can be used to filter lobbies by minimum number of slots available. + * + * @constructor Initializes a new instance of the [SlotsAvailableFilter] class. + * @param slotsAvailable Integer value to compare against. + * + * @property slotsAvailable Minimum number of slots available in the lobby. + */ +sealed class SlotsAvailableFilter( + val slotsAvailable: Int, +) : Filter( + filterType = ELobbyFilterType.SlotsAvailable, + key = "", + comparison = ELobbyComparison.Equal, +) { + /** + * Serializes the slots available filter into a representation used internally by SteamMatchmaking. + * @return A protobuf serializable representation of this filter. + */ + override fun serialize(): SteammessagesClientserverMms.CMsgClientMMSGetLobbyList.Filter.Builder = + super.serialize().apply { + this.value = slotsAvailable.toString() + } +} + +/** + * Can be used to filter lobbies by comparing a string against a value in each lobby's metadata. + * + * @constructor Initializes a new instance of the [StringFilter] class. + * @param key The metadata key this filter pertains to. + * @param comparison The comparison method used by this filter. + * @param value String value to compare against. + * + * @property value String value to compare against. + */ +sealed class StringFilter( + key: String, + comparison: ELobbyComparison, + val value: String, +) : Filter( + filterType = ELobbyFilterType.String, + key = key, + comparison = comparison +) { + /** + * Serializes the string filter into a representation used internally by SteamMatchmaking. + * @return A protobuf serializable representation of this filter. + */ + override fun serialize(): SteammessagesClientserverMms.CMsgClientMMSGetLobbyList.Filter.Builder = + super.serialize().apply { + this.value = this@StringFilter.value + } +} diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt new file mode 100644 index 00000000..e9ad7264 --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt @@ -0,0 +1,78 @@ +package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking + +import `in`.dragonbra.javasteam.enums.ELobbyType +import `in`.dragonbra.javasteam.types.KeyValue +import `in`.dragonbra.javasteam.types.SteamID +import `in`.dragonbra.javasteam.util.stream.MemoryStream + +/** + * Represents a Steam lobby. + * @param steamID SteamID of the lobby. + * @param lobbyType The type of the lobby. + * @param lobbyFlags The lobby's flags. + * @param ownerSteamID The SteamID of the lobby's owner. Please keep in mind that Steam does not provide lobby + * owner details for lobbies returned in a lobby list. As such, lobbies that have been + * obtained/updated as a result of calling [SteamMatchmaking.getLobbyList] + * may have a null (or non-null but state) owner. + * @param metadata The metadata of the lobby; string key-value pairs. + * @param maxMembers The maximum number of members that can occupy the lobby. + * @param numMembers The number of members that are currently occupying the lobby. + * @param members A list of lobby members. This will only be populated for the user's current lobby. + * @param distance The distance of the lobby. + * @param weight The weight of the lobby. + */ +class Lobby( + val steamID: SteamID, + val lobbyType: ELobbyType, + val lobbyFlags: Int, + val ownerSteamID: SteamID?, + val metadata: Map = mapOf(), + val maxMembers: Int, + val numMembers: Int, + val members: List = listOf(), + val distance: Float?, + val weight: Long?, +) { + companion object { + @JvmStatic + fun encodeMetadata(metadata: Map?): ByteArray { + val keyValue = KeyValue("") + + metadata?.forEach { entry -> + keyValue[entry.key] = KeyValue(null, entry.value) + } + + return MemoryStream().use { ms -> + keyValue.saveToStream(ms.asOutputStream(), true) + ms.toByteArray() + } + } + + @JvmStatic + fun decodeMetadata(buffer: ByteArray?): Map { + if (buffer == null || buffer.isEmpty()) { + return emptyMap() + } + + val keyValue = KeyValue() + + MemoryStream(buffer).use { ms -> + if (!keyValue.tryReadAsBinary(ms)) { + throw NumberFormatException("Lobby metadata is of an unexpected format") + } + } + + val metadata = mutableMapOf() + + keyValue.children.forEach { value -> + if (value.name == null || value.value == null) { + return metadata + } + + metadata[value.name] = value.value + } + + return metadata.toMap() + } + } +} diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/LobbyCache.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/LobbyCache.kt new file mode 100644 index 00000000..a31480a0 --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/LobbyCache.kt @@ -0,0 +1,106 @@ +package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking + +import `in`.dragonbra.javasteam.types.SteamID +import java.util.concurrent.ConcurrentHashMap + +/** + * Cache for managing Steam lobbies. + */ +class LobbyCache { + + private val lobbies: ConcurrentHashMap> = ConcurrentHashMap() + + fun getLobby(appId: Int, lobbySteamId: SteamID): Lobby? = getAppLobbies(appId)[lobbySteamId] + + fun cacheLobby(appId: Int, lobby: Lobby) { + getAppLobbies(appId)[lobby.steamID] = lobby + } + + fun addLobbyMember(appId: Int, lobby: Lobby, memberId: SteamID, personaName: String): Member? { + val existingMember = lobby.members.firstOrNull { it.steamID == memberId } + + if (existingMember != null) { + // Already in lobby + return null + } + + val addedMember = Member(steamID = memberId, personaName = personaName) + + val members = ArrayList(lobby.members.size + 1) + members.addAll(lobby.members) + members.add(addedMember) + + updateLobbyMembers(appId = appId, lobby = lobby, members = members) + + return addedMember + } + + fun removeLobbyMember(appId: Int, lobby: Lobby, memberId: SteamID): Member? { + val removedMember = lobby.members.firstOrNull { it.steamID == memberId } + + if (removedMember == null) { + return null + } + + val members = lobby.members.filter { it != removedMember } + + if (members.isNotEmpty()) { + updateLobbyMembers(appId = appId, lobby = lobby, members = members) + } else { + // Steam deletes lobbies that contain no members + deleteLobby(appId = appId, lobbySteamId = lobby.steamID) + } + + return removedMember + } + + fun clearLobbyMembers(appId: Int, lobbySteamId: SteamID) { + val lobby = getLobby(appId = appId, lobbySteamId = lobbySteamId) + + if (lobby != null) { + updateLobbyMembers(appId = appId, lobby = lobby, owner = null, members = null) + } + } + + fun updateLobbyOwner(appId: Int, lobbySteamId: SteamID, ownerSteamId: SteamID) { + val lobby = getLobby(appId = appId, lobbySteamId = lobbySteamId) + + if (lobby != null) { + updateLobbyMembers(appId = appId, lobby = lobby, owner = ownerSteamId, members = lobby.members) + } + } + + fun updateLobbyMembers(appId: Int, lobby: Lobby, members: List) { + updateLobbyMembers(appId = appId, lobby = lobby, owner = lobby.ownerSteamID, members = members) + } + + fun clear() { + lobbies.clear() + } + + private fun updateLobbyMembers(appId: Int, lobby: Lobby, owner: SteamID?, members: List?) { + cacheLobby( + appId = appId, + lobby = Lobby( + steamID = lobby.steamID, + lobbyType = lobby.lobbyType, + lobbyFlags = lobby.lobbyFlags, + ownerSteamID = owner, + metadata = lobby.metadata, + maxMembers = lobby.maxMembers, + numMembers = lobby.numMembers, + members = members ?: listOf(), + distance = lobby.distance, + weight = lobby.weight + ) + ) + } + + private fun getAppLobbies(appId: Int): ConcurrentHashMap = + lobbies.computeIfAbsent(appId) { ConcurrentHashMap() } + + private fun deleteLobby(appId: Int, lobbySteamId: SteamID): Lobby? { + val appLobbies = lobbies[appId] ?: return null + return appLobbies.remove(lobbySteamId) + } +} diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Member.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Member.kt new file mode 100644 index 00000000..bdbb6534 --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Member.kt @@ -0,0 +1,32 @@ +package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking + +import `in`.dragonbra.javasteam.types.SteamID + +/** + * Represents a Steam user within a lobby. + * @param steamID SteamID of the lobby member. + * @param personaName Steam persona of the lobby member. + * @param metadata Metadata attached to the lobby member. + */ +data class Member( + val steamID: SteamID, + val personaName: String, + val metadata: Map = emptyMap(), +) { + /** + * Checks to see if this lobby member is equal to another. Only the SteamID of the lobby member is taken into account. + * @return true, if obj is [Member] with a matching SteamID. Otherwise, false. + */ + override fun equals(other: Any?): Boolean { + if (other is Member) { + return steamID == other.steamID + } + return false + } + + /** + * Hash code of the lobby member. Only the SteamID of the lobby member is taken into account. + * @return The hash code of this lobby member. + */ + override fun hashCode(): Int = steamID.hashCode() +} diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/CreateLobbyCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/CreateLobbyCallback.kt new file mode 100644 index 00000000..4fa3e5f0 --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/CreateLobbyCallback.kt @@ -0,0 +1,24 @@ +package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback + +import `in`.dragonbra.javasteam.enums.EResult +import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg +import `in`.dragonbra.javasteam.types.JobID +import `in`.dragonbra.javasteam.types.SteamID + +/** + * This callback is fired in response to [CreateLobby]. + * + * @param appID ID of the app the created lobby belongs to. + * @param result The result of the request. + * @param lobbySteamID The SteamID of the created lobby. + */ +class CreateLobbyCallback( + jobID: JobID, + val appID: Int, + val result: EResult, + val lobbySteamID: SteamID, +) : CallbackMsg() { + init { + this.jobID = jobID + } +} diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/GetLobbyListCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/GetLobbyListCallback.kt new file mode 100644 index 00000000..d535cbd2 --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/GetLobbyListCallback.kt @@ -0,0 +1,24 @@ +package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback + +import `in`.dragonbra.javasteam.enums.EResult +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.Lobby +import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg +import `in`.dragonbra.javasteam.types.JobID + +/** + * This callback is fired in response to . + * + * @param appID ID of the app the lobbies belongs to. + * @param result The result of the request. + * @param lobbies The list of lobbies matching the criteria specified with [GetLobbyList]. + */ +class GetLobbyListCallback( + jobID: JobID, + val appID: Int, + val result: EResult, + val lobbies: List, +) : CallbackMsg() { + init { + this.jobID = jobID + } +} diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/JoinLobbyCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/JoinLobbyCallback.kt new file mode 100644 index 00000000..5fc730c8 --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/JoinLobbyCallback.kt @@ -0,0 +1,24 @@ +package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback + +import `in`.dragonbra.javasteam.enums.EChatRoomEnterResponse +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.Lobby +import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg +import `in`.dragonbra.javasteam.types.JobID + +/** + * This callback is fired in response to [JoinLobby]. + * + * @param appID ID of the app the targeted lobby belongs to. + * @param chatRoomEnterResponse The result of the request. + * @param lobby The joined [Lobby], when [chatRoomEnterResponse] equals [EChatRoomEnterResponse.Success], otherwise null + */ +class JoinLobbyCallback( + jobID: JobID, + val appID: Int, + val chatRoomEnterResponse: EChatRoomEnterResponse, + val lobby: Lobby?, +) : CallbackMsg() { + init { + this.jobID = jobID + } +} diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LeaveLobbyCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LeaveLobbyCallback.kt new file mode 100644 index 00000000..b7928eea --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LeaveLobbyCallback.kt @@ -0,0 +1,24 @@ +package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback + +import `in`.dragonbra.javasteam.enums.EResult +import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg +import `in`.dragonbra.javasteam.types.JobID +import `in`.dragonbra.javasteam.types.SteamID + +/** + * This callback is fired in response to [LeaveLobby]. + * + * @param appID ID of the app the targeted lobby belongs to. + * @param result The result of the request. + * @param lobbySteamID The SteamID of the targeted Lobby. + */ +class LeaveLobbyCallback( + jobID: JobID, + val appID: Int, + val result: EResult, + val lobbySteamID: SteamID, +) : CallbackMsg() { + init { + this.jobID = jobID + } +} diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LobbyDataCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LobbyDataCallback.kt new file mode 100644 index 00000000..0bd4dae3 --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LobbyDataCallback.kt @@ -0,0 +1,21 @@ +package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback + +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.Lobby +import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg +import `in`.dragonbra.javasteam.types.JobID + +/** + * This callback is fired in response to [GetLobbyData], as well as whenever Steam sends us updated lobby data. + * + * @param appID ID of the app the updated lobby belongs to. + * @param lobby The lobby that was updated. + */ +class LobbyDataCallback( + jobID: JobID, + val appID: Int, + val lobby: Lobby, +) : CallbackMsg() { + init { + this.jobID = jobID + } +} diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyDataCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyDataCallback.kt new file mode 100644 index 00000000..e6766d5d --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyDataCallback.kt @@ -0,0 +1,24 @@ +package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback + +import `in`.dragonbra.javasteam.enums.EResult +import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg +import `in`.dragonbra.javasteam.types.JobID +import `in`.dragonbra.javasteam.types.SteamID + +/** + * This callback is fired in response to [SetLobbyData]. + * + * @param appID ID of the app the targeted lobby belongs to. + * @param result The result of the request. + * @param lobbySteamID The SteamID of the targeted Lobby. + */ +class SetLobbyDataCallback( + jobID: JobID, + val appID: Int, + val result: EResult, + val lobbySteamID: SteamID, +) : CallbackMsg() { + init { + this.jobID = jobID + } +} diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyOwnerCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyOwnerCallback.kt new file mode 100644 index 00000000..30fc28d9 --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyOwnerCallback.kt @@ -0,0 +1,24 @@ +package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback + +import `in`.dragonbra.javasteam.enums.EResult +import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg +import `in`.dragonbra.javasteam.types.JobID +import `in`.dragonbra.javasteam.types.SteamID + +/** + * This callback is fired in response to [SetLobbyOwner]. + * + * @param appID ID of the app the targeted lobby belongs to. + * @param result The result of the request. + * @param lobbySteamID The SteamID of the targeted Lobby. + */ +class SetLobbyOwnerCallback( + jobID: JobID, + val appID: Int, + val result: EResult, + val lobbySteamID: SteamID, +) : CallbackMsg() { + init { + this.jobID = jobID + } +} diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserJoinedLobbyCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserJoinedLobbyCallback.kt new file mode 100644 index 00000000..5ac97813 --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserJoinedLobbyCallback.kt @@ -0,0 +1,20 @@ +package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback + +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.Member +import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg +import `in`.dragonbra.javasteam.types.SteamID + +/** + * This callback is fired whenever Steam informs us a user has joined a lobby. + * + * @param appID ID of the app the lobby belongs to. + * @param lobbySteamID The SteamID of the lobby that a member joined. + * @param user The lobby member that joined. + */ +class UserJoinedLobbyCallback( + val appID: Int, + val lobbySteamID: SteamID, + val user: Member, +) : CallbackMsg() { + // No job set declared in SK. +} diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserLeftLobbyCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserLeftLobbyCallback.kt new file mode 100644 index 00000000..bc66021d --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserLeftLobbyCallback.kt @@ -0,0 +1,20 @@ +package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback + +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.Member +import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg +import `in`.dragonbra.javasteam.types.SteamID + +/** + * This callback is fired whenever Steam informs us a user has left a lobby. + * + * @param appID ID of the app the lobby belongs to. + * @param lobbySteamID The SteamID of the lobby that a member left. + * @param user The lobby member that left. + */ +class UserLeftLobbyCallback( + val appID: Int, + val lobbySteamID: SteamID, + val user: Member, +) : CallbackMsg() { + // No job set declared in SK. +} diff --git a/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt b/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt index 229d7ade..3eab6f6f 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt @@ -12,6 +12,7 @@ import `in`.dragonbra.javasteam.steam.handlers.steamfriends.SteamFriends import `in`.dragonbra.javasteam.steam.handlers.steamgamecoordinator.SteamGameCoordinator import `in`.dragonbra.javasteam.steam.handlers.steamgameserver.SteamGameServer import `in`.dragonbra.javasteam.steam.handlers.steammasterserver.SteamMasterServer +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.SteamMatchmaking import `in`.dragonbra.javasteam.steam.handlers.steamnetworking.SteamNetworking import `in`.dragonbra.javasteam.steam.handlers.steamnotifications.SteamNotifications import `in`.dragonbra.javasteam.steam.handlers.steamscreenshots.SteamScreenshots @@ -78,6 +79,7 @@ class SteamClient @JvmOverloads constructor( addHandlerCore(SteamWorkshop()) addHandlerCore(SteamUnifiedMessages()) addHandlerCore(SteamScreenshots()) + addHandlerCore(SteamMatchmaking()) addHandlerCore(SteamNetworking()) addHandlerCore(SteamNotifications()) addHandlerCore(SteamUserStats()) @@ -266,12 +268,14 @@ class SteamClient @JvmOverloads constructor( jobManager.setTimeoutsEnabled(false) - // clearHandlerCaches() + clearHandlerCaches() postCallback(DisconnectedCallback(userInitiated)) } - // fun clearHandlerCaches() + fun clearHandlerCaches() { + getHandler()?.clearLobbyCache() + } private fun handleJobHeartbeat(packetMsg: IPacketMsg) { JobID(packetMsg.getTargetJobID()).let(jobManager::heartbeatJob) diff --git a/src/test/java/in/dragonbra/javasteam/steam/steamclient/SteamClientTest.java b/src/test/java/in/dragonbra/javasteam/steam/steamclient/SteamClientTest.java index 405163e4..f8eb6024 100644 --- a/src/test/java/in/dragonbra/javasteam/steam/steamclient/SteamClientTest.java +++ b/src/test/java/in/dragonbra/javasteam/steam/steamclient/SteamClientTest.java @@ -8,6 +8,7 @@ import in.dragonbra.javasteam.steam.handlers.steamgamecoordinator.SteamGameCoordinator; import in.dragonbra.javasteam.steam.handlers.steamgameserver.SteamGameServer; import in.dragonbra.javasteam.steam.handlers.steammasterserver.SteamMasterServer; +import in.dragonbra.javasteam.steam.handlers.steammatchmaking.SteamMatchmaking; import in.dragonbra.javasteam.steam.handlers.steamnetworking.SteamNetworking; import in.dragonbra.javasteam.steam.handlers.steamnotifications.SteamNotifications; import in.dragonbra.javasteam.steam.handlers.steamscreenshots.SteamScreenshots; @@ -40,6 +41,7 @@ public void constructorSetsInitialHandlers() { Assertions.assertNotNull(client.getHandler(SteamWorkshop.class)); Assertions.assertNotNull(client.getHandler(SteamUnifiedMessages.class)); Assertions.assertNotNull(client.getHandler(SteamScreenshots.class)); + Assertions.assertNotNull(client.getHandler(SteamMatchmaking.class)); Assertions.assertNotNull(client.getHandler(SteamNetworking.class)); Assertions.assertNotNull(client.getHandler(SteamNotifications.class)); Assertions.assertNotNull(client.getHandler(SteamUserStats.class)); From f630244f8e5e3bda17d1e757339cff7adba4e82c Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Wed, 21 May 2025 16:59:28 -0500 Subject: [PATCH 2/9] Push current work of SteamMatchmaking.kt --- .../steammatchmaking/SteamMatchmaking.kt | 391 ++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt new file mode 100644 index 00000000..c4f249a1 --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt @@ -0,0 +1,391 @@ +package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking + +import com.google.protobuf.ByteString +import com.google.protobuf.GeneratedMessage +import `in`.dragonbra.javasteam.base.AClientMsgProtobuf +import `in`.dragonbra.javasteam.base.ClientMsgProtobuf +import `in`.dragonbra.javasteam.base.IPacketMsg +import `in`.dragonbra.javasteam.enums.ELobbyType +import `in`.dragonbra.javasteam.enums.EMsg +import `in`.dragonbra.javasteam.enums.EResult +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSGetLobbyData +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSLeaveLobby +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSInviteToLobby +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSCreateLobby +import `in`.dragonbra.javasteam.steam.handlers.ClientMsgHandler +import `in`.dragonbra.javasteam.steam.handlers.steamfriends.SteamFriends +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback.CreateLobbyCallback +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback.LeaveLobbyCallback +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback.LobbyDataCallback +import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg +import `in`.dragonbra.javasteam.types.AsyncJob +import `in`.dragonbra.javasteam.types.AsyncJobSingle +import `in`.dragonbra.javasteam.types.JobID +import `in`.dragonbra.javasteam.types.SteamID +import `in`.dragonbra.javasteam.util.NetHelpers +import java.util.concurrent.ConcurrentHashMap + +/** + * This handler is used for creating, joining and obtaining lobby information. + */ +class SteamMatchmaking : ClientMsgHandler() { + + companion object { + private fun getHandler(packetMsg: IPacketMsg): IPacketMsg? { + return when (packetMsg.msgType) { + EMsg.ClientMMSCreateLobbyResponse -> handleCreateLobbyResponse() + EMsg.ClientMMSSetLobbyDataResponse -> ::handleSetLobbyDataResponse + EMsg.ClientMMSSetLobbyOwnerResponse -> ::handleSetLobbyOwnerResponse + EMsg.ClientMMSLobbyData -> ::handleLobbyData + EMsg.ClientMMSGetLobbyListResponse -> ::handleGetLobbyListResponse + EMsg.ClientMMSJoinLobbyResponse -> ::handleJoinLobbyResponse + EMsg.ClientMMSLeaveLobbyResponse -> ::handleLeaveLobbyResponse + EMsg.ClientMMSUserJoinedLobby -> ::handleUserJoinedLobby + EMsg.ClientMMSUserLeftLobby -> ::handleUserLeftLobby + else -> null + } + } + } + + private val lobbyManipulationRequests: ConcurrentHashMap = + ConcurrentHashMap() // TODO Value + + private val lobbyCache: LobbyCache = LobbyCache() + + + /// + /// Sends a request to create a lobby. + /// + /// ID of the app the lobby will belong to. + /// The lobby type. + /// The maximum number of members that may occupy the lobby. + /// The lobby flags. Defaults to 0. + /// The metadata for the lobby. Defaults to null (treated as an empty dictionary). + /// null, if the request could not be submitted i.e. not yet logged in. Otherwise, an . + @JvmOverloads + fun createLobby( + appId: Int, + lobbyType: ELobbyType, + maxMembers: Int, + lobbyFlags: Int = 0, + metadata: Map? = null + ): AsyncJobSingle? { + if (client.cellID == null) { + return null + } + + val personaName = client.getHandler()!!.getPersonaName() + + val createLobby = ClientMsgProtobuf( + CMsgClientMMSCreateLobby::class.java, + EMsg.ClientMMSCreateLobby + ).apply { + body.appId = appId + body.lobbyType = lobbyType.code() + body.maxMembers = maxMembers + body.lobbyFlags = lobbyFlags + body.metadata = ByteString.copyFrom(Lobby.encodeMetadata(metadata)) + body.cellId = client.cellID!! + body.publicIp = NetHelpers.getMsgIPAddress(client.publicIP) + body.personaNameOwner = personaName + + sourceJobID = client.getNextJobID() + } + + send(createLobby, appId) + + lobbyManipulationRequests[createLobby.sourceJobID] = createLobby.body.build() + return attachIncompleteManipulationHandler( + job = AsyncJobSingle(client, createLobby.sourceJobID) + ) + } + + /// + /// Sends a request to update a lobby. + /// + /// ID of app the lobby belongs to. + /// The SteamID of the lobby that should be updated. + /// The lobby type. + /// The maximum number of members that may occupy the lobby. + /// The lobby flags. Defaults to 0. + /// The metadata for the lobby. Defaults to null (treated as an empty dictionary). + /// An . + public AsyncJob SetLobbyData( appId: Int, lobbySteamId: SteamID, ELobbyType lobbyType, int maxMembers, int lobbyFlags = 0, + IReadOnlyDictionary? metadata = null ) + { + val setLobbyData = ClientMsgProtobuf(EMsg.ClientMMSSetLobbyData) + { + Body = + { + app_id = appId, + steam_id_lobby = lobbySteamId, + steam_id_member = 0, + lobby_type = (int) lobbyType, + max_members = maxMembers, + lobby_flags = lobbyFlags, + metadata = Lobby.EncodeMetadata(metadata), + }, + SourceJobID = client.getNextJobID() + } + + Send(setLobbyData, appId) + + lobbyManipulationRequests[setLobbyData.sourceJobID] = setLobbyData.Body + return AttachIncompleteManipulationHandler( + AsyncJobSingle( + client, + setLobbyData.sourceJobID + ) + ) + } + + /// + /// Sends a request to update the current user's lobby metadata. + /// + /// ID of app the lobby belongs to. + /// The SteamID of the lobby that should be updated. + /// The metadata for the lobby. + /// null, if the request could not be submitted i.e. not yet logged in. Otherwise, an . + public AsyncJob? SetLobbyMemberData( appId: Int, lobbySteamId: SteamID, IReadOnlyDictionary metadata ) + { + if (client.SteamID == null) { + return null + } + + val setLobbyData = ClientMsgProtobuf(EMsg.ClientMMSSetLobbyData) + { + Body = + { + app_id = appId, + steam_id_lobby = lobbySteamId, + steam_id_member = client.SteamID, + metadata = Lobby.EncodeMetadata(metadata) + }, + SourceJobID = client.getNextJobID() + } + + Send(setLobbyData, appId) + + lobbyManipulationRequests[setLobbyData.sourceJobID] = setLobbyData.Body + return AttachIncompleteManipulationHandler( + AsyncJobSingle( + client, + setLobbyData.sourceJobID + ) + ) + } + + /// + /// Sends a request to update the owner of a lobby. + /// + /// ID of app the lobby belongs to. + /// The SteamID of the lobby that should have its owner updated. + /// The SteamID of the owner. + /// An . + public AsyncJob SetLobbyOwner( appId: Int, lobbySteamId: SteamID, SteamID newOwner ) + { + val setLobbyOwner = ClientMsgProtobuf(EMsg.ClientMMSSetLobbyOwner) + { + Body = + { + app_id = appId, + steam_id_lobby = lobbySteamId, + steam_id_new_owner = newOwner + }, + SourceJobID = client.getNextJobID() + } + + Send(setLobbyOwner, appId) + + lobbyManipulationRequests[setLobbyOwner.sourceJobID] = setLobbyOwner.Body + return AttachIncompleteManipulationHandler( + AsyncJobSingle( + client, + setLobbyOwner.sourceJobID + ) + ) + } + + /// + /// Sends a request to obtains a list of lobbies matching the specified criteria. + /// + /// The ID of app for which we're requesting a list of lobbies. + /// An optional list of filters. + /// An optional maximum number of lobbies that will be returned. + /// null, if the request could not be submitted i.e. not yet logged in. Otherwise, an . + public AsyncJob? GetLobbyList( appId: Int, List? filters = null, int maxLobbies = -1 ) + { + if (client.CellID == null) { + return null + } + + val getLobbies = ClientMsgProtobuf(EMsg.ClientMMSGetLobbyList) + { + Body = + { + app_id = appId, + cell_id = client.CellID.Value, + public_ip = NetHelpers.GetMsgIPAddress(client.PublicIP!), + num_lobbies_requested = maxLobbies + }, + SourceJobID = client.getNextJobID() + } + + if (filters != null) { + foreach(val filter in filters ) + { + getLobbies.Body.filters.Add(filter.Serialize()) + } + } + + Send(getLobbies, appId) + + return AsyncJobSingle(client, getLobbies.sourceJobID) + } + + /// + /// Sends a request to join a lobby. + /// + /// ID of app the lobby belongs to. + /// The SteamID of the lobby that should be joined. + /// null, if the request could not be submitted i.e. not yet logged in. Otherwise, an . + public AsyncJob? JoinLobby( appId: Int, lobbySteamId: SteamID ) + { + val personaName = client.GetHandler()?.GetPersonaName() + + if (personaName == null) { + return null + } + + val joinLobby = ClientMsgProtobuf(EMsg.ClientMMSJoinLobby) + { + Body = + { + app_id = appId, + persona_name = personaName, + steam_id_lobby = lobbySteamId + }, + SourceJobID = client.getNextJobID() + } + + Send(joinLobby, appId) + + return AsyncJobSingle(client, joinLobby.sourceJobID) + } + + /// + /// Sends a request to leave a lobby. + /// + /// ID of app the lobby belongs to. + /// The SteamID of the lobby that should be left. + /// An . + fun leaveLobby(appId: Int, lobbySteamId: SteamID): AsyncJobSingle { + val leaveLobby = ClientMsgProtobuf( + CMsgClientMMSLeaveLobby::class.java, + EMsg . ClientMMSLeaveLobby + ).apply { + body. appId = appId + body. steamIdLobby = lobbySteamId.convertToUInt64() + + sourceJobID = client.getNextJobID() + } + + send(leaveLobby, appId) + + return AsyncJobSingle(client, leaveLobby.sourceJobID) + } + + /// + /// Sends a request to obtain a lobby's data. + /// + /// The ID of app which we're attempting to obtain lobby data for. + /// The SteamID of the lobby whose data is being requested. + /// An . + fun getLobbyData(appId: Int, lobbySteamId: SteamID): AsyncJobSingle { + val getLobbyData = ClientMsgProtobuf( + CMsgClientMMSGetLobbyData::class.java, + EMsg.ClientMMSGetLobbyData + ).apply { + body.appId = appId + body.steamIdLobby = lobbySteamId.convertToUInt64() + + sourceJobID = client.getNextJobID() + } + + send(getLobbyData, appId) + + return AsyncJobSingle(client, getLobbyData.sourceJobID) + } + + /// + /// Sends a lobby invite request. + /// NOTE: Steam provides no functionality to determine if the user was successfully invited. + /// + /// The ID of app which owns the lobby we're inviting a user to. + /// The SteamID of the lobby we're inviting a user to. + /// The SteamID of the user we're inviting. + fun inviteToLobby(appId: Int, lobbySteamId: SteamID, userSteamId: SteamID) { + val getLobbyData = ClientMsgProtobuf( + CMsgClientMMSInviteToLobby::class.java, + EMsg.ClientMMSInviteToLobby + ).apply { + body.appId = appId + body.steamIdLobby = lobbySteamId.convertToUInt64() + body.steamIdUserInvited = userSteamId.convertToUInt64() + } + + send(getLobbyData, appId) + } + + /// + /// Obtains a , by its SteamID, if the data is cached locally. + /// This method does not send a network request. + /// + /// The ID of app which we're attempting to obtain a lobby for. + /// The SteamID of the lobby that should be returned. + /// The corresponding with the specified app and lobby ID, if cached. Otherwise, null. + fun getLobby(appId: Int, lobbySteamId: SteamID): Lobby? = lobbyCache.getLobby(appId, lobbySteamId) + + /// + /// Sends a matchmaking message for a specific app. + /// + /// The matchmaking message to send. + /// The ID of the app this message pertains to. + fun send(msg: AClientMsgProtobuf, appId: Int) { + msg.protoHeader.routingAppid = appId + client.send(msg) + } + + /** + * Handles a client message. This should not be called directly. + * @param packetMsg The packet message that contains the data. + */ + // TODO this is using the old CB style + override fun handleMsg(packetMsg: IPacketMsg) { + val handler = getHandler(packetMsg) ?: return + + // handler?.Invoke(packetMsg) + } + + internal fun clearLobbyCache() { + lobbyCache.clear() + } + + // TODO verify... + private fun attachIncompleteManipulationHandler(job: AsyncJobSingle): AsyncJobSingle { + // Manipulation requests typically complete (and are removed from lobbyManipulationRequests) when + // a message is handled. However, jobs can also be faulted, or be cancelled (e.g. when SteamClient + // disconnects.) Thus, when a job fails we remove the JobID/request from lobbyManipulationRequests. + job.toFuture().exceptionally { task -> + lobbyManipulationRequests.remove(job.jobID) + null + } + return job + } + + // region ClientMsg Handlers + + // TODO + + // endregion +} From 71144455e14464aea403764987b40b76d236eb54 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Wed, 21 May 2025 20:17:26 -0500 Subject: [PATCH 3/9] Finish porting SteamMatchmaking --- .../dragonbra/javasteam/steam/CMClient.java | 35 +- .../steam/handlers/steammatchmaking/Lobby.kt | 11 +- .../handlers/steammatchmaking/LobbyCache.kt | 15 + .../steammatchmaking/SteamMatchmaking.kt | 758 +++++++++++++----- 4 files changed, 601 insertions(+), 218 deletions(-) diff --git a/src/main/java/in/dragonbra/javasteam/steam/CMClient.java b/src/main/java/in/dragonbra/javasteam/steam/CMClient.java index 1351b746..2860c497 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/CMClient.java +++ b/src/main/java/in/dragonbra/javasteam/steam/CMClient.java @@ -18,10 +18,7 @@ import in.dragonbra.javasteam.steam.discovery.SmartCMServerList; import in.dragonbra.javasteam.steam.steamclient.configuration.SteamConfiguration; import in.dragonbra.javasteam.types.SteamID; -import in.dragonbra.javasteam.util.IDebugNetworkListener; -import in.dragonbra.javasteam.util.MsgUtil; -import in.dragonbra.javasteam.util.NetHookNetworkListener; -import in.dragonbra.javasteam.util.Strings; +import in.dragonbra.javasteam.util.*; import in.dragonbra.javasteam.util.event.EventArgs; import in.dragonbra.javasteam.util.event.EventHandler; import in.dragonbra.javasteam.util.event.ScheduledFunction; @@ -48,6 +45,12 @@ public abstract class CMClient { private final SteamConfiguration configuration; + @Nullable + private InetAddress publicIP; + + @Nullable + private String ipCountryCode; + private boolean isConnected; private long sessionToken; @@ -425,6 +428,8 @@ private void handleLogOnResponse(IPacketMsg packetMsg) { steamID = new SteamID(logonResp.getProtoHeader().getSteamid()); cellID = logonResp.getBody().getCellId(); + publicIP = NetHelpers.getIPAddress(logonResp.getBody().getPublicIp()); + ipCountryCode = logonResp.getBody().getIpCountryCode(); // restart heartbeat heartBeatFunc.stop(); @@ -445,6 +450,8 @@ private void handleLoggedOff(IPacketMsg packetMsg) { steamID = null; cellID = null; + publicIP = null; + ipCountryCode = null; heartBeatFunc.stop(); @@ -519,6 +526,26 @@ public SmartCMServerList getServers() { return connection.getCurrentEndPoint(); } + /** + * Gets the public IP address of this client. This value is assigned after a logon attempt has succeeded. + * This value will be null if the client is logged off of Steam. + * + * @return The {@link InetSocketAddress} public ip + */ + public @Nullable InetAddress getPublicIP() { + return publicIP; + } + + /** + * Gets the country code of our public IP address according to Steam. This value is assigned after a logon attempt has succeeded. + * This value will be null if the client is logged off of Steam. + * + * @return the ip country code. + */ + public @Nullable String getIpCountryCode() { + return ipCountryCode; + } + /** * Gets the universe of this client. * diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt index e9ad7264..060ce8ba 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt @@ -1,5 +1,6 @@ package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking +import com.google.protobuf.ByteString import `in`.dragonbra.javasteam.enums.ELobbyType import `in`.dragonbra.javasteam.types.KeyValue import `in`.dragonbra.javasteam.types.SteamID @@ -34,8 +35,11 @@ class Lobby( val weight: Long?, ) { companion object { + + internal fun ByteArray.toByteString(): ByteString = ByteString.copyFrom(this) + @JvmStatic - fun encodeMetadata(metadata: Map?): ByteArray { + internal fun encodeMetadata(metadata: Map?): ByteArray { val keyValue = KeyValue("") metadata?.forEach { entry -> @@ -49,7 +53,10 @@ class Lobby( } @JvmStatic - fun decodeMetadata(buffer: ByteArray?): Map { + internal fun decodeMetadata(buffer: ByteString?): Map = decodeMetadata(buffer?.toByteArray()) + + @JvmStatic + internal fun decodeMetadata(buffer: ByteArray?): Map { if (buffer == null || buffer.isEmpty()) { return emptyMap() } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/LobbyCache.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/LobbyCache.kt index a31480a0..49b466b9 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/LobbyCache.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/LobbyCache.kt @@ -6,16 +6,21 @@ import java.util.concurrent.ConcurrentHashMap /** * Cache for managing Steam lobbies. */ +@Suppress("unused") class LobbyCache { private val lobbies: ConcurrentHashMap> = ConcurrentHashMap() + fun getLobby(appId: Int, lobbySteamId: Long): Lobby? = getLobby(appId, SteamID(lobbySteamId)) + fun getLobby(appId: Int, lobbySteamId: SteamID): Lobby? = getAppLobbies(appId)[lobbySteamId] fun cacheLobby(appId: Int, lobby: Lobby) { getAppLobbies(appId)[lobby.steamID] = lobby } + fun addLobbyMember(appId: Int, lobby: Lobby, memberId: Long, personaName: String): Member? = addLobbyMember(appId, lobby, SteamID(memberId), personaName) + fun addLobbyMember(appId: Int, lobby: Lobby, memberId: SteamID, personaName: String): Member? { val existingMember = lobby.members.firstOrNull { it.steamID == memberId } @@ -35,6 +40,8 @@ class LobbyCache { return addedMember } + fun removeLobbyMember(appId: Int, lobby: Lobby, memberId: Long): Member? = removeLobbyMember(appId, lobby, SteamID(memberId)) + fun removeLobbyMember(appId: Int, lobby: Lobby, memberId: SteamID): Member? { val removedMember = lobby.members.firstOrNull { it.steamID == memberId } @@ -54,6 +61,10 @@ class LobbyCache { return removedMember } + fun clearLobbyMembers(appId: Int, lobbySteamId: Long) { + clearLobbyMembers(appId, SteamID(lobbySteamId)) + } + fun clearLobbyMembers(appId: Int, lobbySteamId: SteamID) { val lobby = getLobby(appId = appId, lobbySteamId = lobbySteamId) @@ -62,6 +73,10 @@ class LobbyCache { } } + fun updateLobbyOwner(appId: Int, lobbySteamId: Long, ownerSteamId: Long) { + updateLobbyOwner(appId = appId, lobbySteamId = SteamID(lobbySteamId), ownerSteamId = SteamID(ownerSteamId)) + } + fun updateLobbyOwner(appId: Int, lobbySteamId: SteamID, ownerSteamId: SteamID) { val lobby = getLobby(appId = appId, lobbySteamId = lobbySteamId) diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt index c4f249a1..9f839113 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt @@ -1,24 +1,42 @@ package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking -import com.google.protobuf.ByteString import com.google.protobuf.GeneratedMessage -import `in`.dragonbra.javasteam.base.AClientMsgProtobuf import `in`.dragonbra.javasteam.base.ClientMsgProtobuf import `in`.dragonbra.javasteam.base.IPacketMsg +import `in`.dragonbra.javasteam.enums.EChatRoomEnterResponse import `in`.dragonbra.javasteam.enums.ELobbyType import `in`.dragonbra.javasteam.enums.EMsg import `in`.dragonbra.javasteam.enums.EResult +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSCreateLobby +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSCreateLobbyResponse import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSGetLobbyData -import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSLeaveLobby +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSGetLobbyList +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSGetLobbyListResponse import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSInviteToLobby -import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSCreateLobby +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSJoinLobby +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSJoinLobbyResponse +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSLeaveLobby +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSLeaveLobbyResponse +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSLobbyData +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSSetLobbyData +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSSetLobbyDataResponse +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSSetLobbyOwner +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSSetLobbyOwnerResponse +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSUserJoinedLobby +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverMms.CMsgClientMMSUserLeftLobby import `in`.dragonbra.javasteam.steam.handlers.ClientMsgHandler import `in`.dragonbra.javasteam.steam.handlers.steamfriends.SteamFriends +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.Lobby.Companion.toByteString import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback.CreateLobbyCallback +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback.GetLobbyListCallback +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback.JoinLobbyCallback import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback.LeaveLobbyCallback import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback.LobbyDataCallback +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback.SetLobbyDataCallback +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback.SetLobbyOwnerCallback +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback.UserJoinedLobbyCallback +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback.UserLeftLobbyCallback import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg -import `in`.dragonbra.javasteam.types.AsyncJob import `in`.dragonbra.javasteam.types.AsyncJobSingle import `in`.dragonbra.javasteam.types.JobID import `in`.dragonbra.javasteam.types.SteamID @@ -28,47 +46,42 @@ import java.util.concurrent.ConcurrentHashMap /** * This handler is used for creating, joining and obtaining lobby information. */ +@Suppress("unused") class SteamMatchmaking : ClientMsgHandler() { - companion object { - private fun getHandler(packetMsg: IPacketMsg): IPacketMsg? { - return when (packetMsg.msgType) { - EMsg.ClientMMSCreateLobbyResponse -> handleCreateLobbyResponse() - EMsg.ClientMMSSetLobbyDataResponse -> ::handleSetLobbyDataResponse - EMsg.ClientMMSSetLobbyOwnerResponse -> ::handleSetLobbyOwnerResponse - EMsg.ClientMMSLobbyData -> ::handleLobbyData - EMsg.ClientMMSGetLobbyListResponse -> ::handleGetLobbyListResponse - EMsg.ClientMMSJoinLobbyResponse -> ::handleJoinLobbyResponse - EMsg.ClientMMSLeaveLobbyResponse -> ::handleLeaveLobbyResponse - EMsg.ClientMMSUserJoinedLobby -> ::handleUserJoinedLobby - EMsg.ClientMMSUserLeftLobby -> ::handleUserLeftLobby - else -> null - } - } + private fun getHandler(packetMsg: IPacketMsg): ((IPacketMsg) -> Unit)? = when (packetMsg.msgType) { + EMsg.ClientMMSCreateLobbyResponse -> ::handleCreateLobbyResponse + EMsg.ClientMMSSetLobbyDataResponse -> ::handleSetLobbyDataResponse + EMsg.ClientMMSSetLobbyOwnerResponse -> ::handleSetLobbyOwnerResponse + EMsg.ClientMMSLobbyData -> ::handleLobbyData + EMsg.ClientMMSGetLobbyListResponse -> ::handleGetLobbyListResponse + EMsg.ClientMMSJoinLobbyResponse -> ::handleJoinLobbyResponse + EMsg.ClientMMSLeaveLobbyResponse -> ::handleLeaveLobbyResponse + EMsg.ClientMMSUserJoinedLobby -> ::handleUserJoinedLobby + EMsg.ClientMMSUserLeftLobby -> ::handleUserLeftLobby + else -> null } - private val lobbyManipulationRequests: ConcurrentHashMap = - ConcurrentHashMap() // TODO Value + private val lobbyManipulationRequests: ConcurrentHashMap = ConcurrentHashMap() private val lobbyCache: LobbyCache = LobbyCache() - - /// - /// Sends a request to create a lobby. - /// - /// ID of the app the lobby will belong to. - /// The lobby type. - /// The maximum number of members that may occupy the lobby. - /// The lobby flags. Defaults to 0. - /// The metadata for the lobby. Defaults to null (treated as an empty dictionary). - /// null, if the request could not be submitted i.e. not yet logged in. Otherwise, an . + // / + // / Sends a request to create a lobby. + // / + // / ID of the app the lobby will belong to. + // / The lobby type. + // / The maximum number of members that may occupy the lobby. + // / The lobby flags. Defaults to 0. + // / The metadata for the lobby. Defaults to null (treated as an empty dictionary). + // / null, if the request could not be submitted i.e., not yet logged in. Otherwise, an . @JvmOverloads fun createLobby( appId: Int, lobbyType: ELobbyType, maxMembers: Int, lobbyFlags: Int = 0, - metadata: Map? = null + metadata: Map? = null, ): AsyncJobSingle? { if (client.cellID == null) { return null @@ -84,223 +97,229 @@ class SteamMatchmaking : ClientMsgHandler() { body.lobbyType = lobbyType.code() body.maxMembers = maxMembers body.lobbyFlags = lobbyFlags - body.metadata = ByteString.copyFrom(Lobby.encodeMetadata(metadata)) + body.metadata = Lobby.encodeMetadata(metadata).toByteString() body.cellId = client.cellID!! - body.publicIp = NetHelpers.getMsgIPAddress(client.publicIP) + body.publicIp = NetHelpers.getMsgIPAddress(client.publicIP!!) body.personaNameOwner = personaName sourceJobID = client.getNextJobID() } - send(createLobby, appId) + send(msg = createLobby, appId = appId) lobbyManipulationRequests[createLobby.sourceJobID] = createLobby.body.build() return attachIncompleteManipulationHandler( - job = AsyncJobSingle(client, createLobby.sourceJobID) + job = AsyncJobSingle(client, createLobby.sourceJobID) ) } - /// - /// Sends a request to update a lobby. - /// - /// ID of app the lobby belongs to. - /// The SteamID of the lobby that should be updated. - /// The lobby type. - /// The maximum number of members that may occupy the lobby. - /// The lobby flags. Defaults to 0. - /// The metadata for the lobby. Defaults to null (treated as an empty dictionary). - /// An . - public AsyncJob SetLobbyData( appId: Int, lobbySteamId: SteamID, ELobbyType lobbyType, int maxMembers, int lobbyFlags = 0, - IReadOnlyDictionary? metadata = null ) - { - val setLobbyData = ClientMsgProtobuf(EMsg.ClientMMSSetLobbyData) - { - Body = - { - app_id = appId, - steam_id_lobby = lobbySteamId, - steam_id_member = 0, - lobby_type = (int) lobbyType, - max_members = maxMembers, - lobby_flags = lobbyFlags, - metadata = Lobby.EncodeMetadata(metadata), - }, - SourceJobID = client.getNextJobID() + // / + // / Sends a request to update a lobby. + // / + // / ID of app the lobby belongs to. + // / The SteamID of the lobby that should be updated. + // / The lobby type. + // / The maximum number of members that may occupy the lobby. + // / The lobby flags. Defaults to 0. + // / The metadata for the lobby. Defaults to null (treated as an empty dictionary). + // / An . + @JvmOverloads + fun setLobbyData( + appId: Int, + lobbySteamId: SteamID, + lobbyType: ELobbyType, + maxMembers: Int, + lobbyFlags: Int = 0, + metadata: Map? = null, + ): AsyncJobSingle { + val setLobbyData = ClientMsgProtobuf( + CMsgClientMMSSetLobbyData::class.java, + EMsg.ClientMMSSetLobbyData + ).apply { + body.appId = appId + body.steamIdLobby = lobbySteamId.convertToUInt64() + body.steamIdMember = 0 + body.lobbyType = lobbyType.code() + body.maxMembers = maxMembers + body.lobbyFlags = lobbyFlags + body.metadata = Lobby.encodeMetadata(metadata).toByteString() + + sourceJobID = client.getNextJobID() } - Send(setLobbyData, appId) + send(msg = setLobbyData, appId = appId) - lobbyManipulationRequests[setLobbyData.sourceJobID] = setLobbyData.Body - return AttachIncompleteManipulationHandler( - AsyncJobSingle( - client, - setLobbyData.sourceJobID - ) + lobbyManipulationRequests[setLobbyData.sourceJobID] = setLobbyData.body.build() + return attachIncompleteManipulationHandler( + job = AsyncJobSingle(client, setLobbyData.sourceJobID) ) } - /// - /// Sends a request to update the current user's lobby metadata. - /// - /// ID of app the lobby belongs to. - /// The SteamID of the lobby that should be updated. - /// The metadata for the lobby. - /// null, if the request could not be submitted i.e. not yet logged in. Otherwise, an . - public AsyncJob? SetLobbyMemberData( appId: Int, lobbySteamId: SteamID, IReadOnlyDictionary metadata ) - { - if (client.SteamID == null) { + // / + // / Sends a request to update the current user's lobby metadata. + // / + // / ID of app the lobby belongs to. + // / The SteamID of the lobby that should be updated. + // / The metadata for the lobby. + // / null, if the request could not be submitted i.e. not yet logged in. Otherwise, an . + fun setLobbyMemberData( + appId: Int, + lobbySteamId: SteamID, + metadata: Map, + ): AsyncJobSingle? { + if (client.steamID == null) { return null } - val setLobbyData = ClientMsgProtobuf(EMsg.ClientMMSSetLobbyData) - { - Body = - { - app_id = appId, - steam_id_lobby = lobbySteamId, - steam_id_member = client.SteamID, - metadata = Lobby.EncodeMetadata(metadata) - }, - SourceJobID = client.getNextJobID() + val setLobbyData = ClientMsgProtobuf( + CMsgClientMMSSetLobbyData::class.java, + EMsg.ClientMMSSetLobbyData + ).apply { + body.appId = appId + body.steamIdLobby = lobbySteamId.convertToUInt64() + body.steamIdMember = client.steamID!!.convertToUInt64() + body.metadata = Lobby.encodeMetadata(metadata).toByteString() + + sourceJobID = client.getNextJobID() } - Send(setLobbyData, appId) + send(msg = setLobbyData, appId = appId) - lobbyManipulationRequests[setLobbyData.sourceJobID] = setLobbyData.Body - return AttachIncompleteManipulationHandler( - AsyncJobSingle( - client, - setLobbyData.sourceJobID - ) + lobbyManipulationRequests[setLobbyData.sourceJobID] = setLobbyData.body.build() + return attachIncompleteManipulationHandler( + job = AsyncJobSingle(client, setLobbyData.sourceJobID) ) } - /// - /// Sends a request to update the owner of a lobby. - /// - /// ID of app the lobby belongs to. - /// The SteamID of the lobby that should have its owner updated. - /// The SteamID of the owner. - /// An . - public AsyncJob SetLobbyOwner( appId: Int, lobbySteamId: SteamID, SteamID newOwner ) - { - val setLobbyOwner = ClientMsgProtobuf(EMsg.ClientMMSSetLobbyOwner) - { - Body = - { - app_id = appId, - steam_id_lobby = lobbySteamId, - steam_id_new_owner = newOwner - }, - SourceJobID = client.getNextJobID() + // / + // / Sends a request to update the owner of a lobby. + // / + // / ID of app the lobby belongs to. + // / The SteamID of the lobby that should have its owner updated. + // / The SteamID of the owner. + // / An . + fun setLobbyOwner( + appId: Int, + lobbySteamId: SteamID, + newOwner: SteamID, + ): AsyncJobSingle { + val setLobbyOwner = ClientMsgProtobuf( + CMsgClientMMSSetLobbyOwner::class.java, + EMsg.ClientMMSSetLobbyOwner + ).apply { + body.appId = appId + body.steamIdLobby = lobbySteamId.convertToUInt64() + body.steamIdNewOwner = newOwner.convertToUInt64() + + sourceJobID = client.getNextJobID() } - Send(setLobbyOwner, appId) + send(msg = setLobbyOwner, appId = appId) - lobbyManipulationRequests[setLobbyOwner.sourceJobID] = setLobbyOwner.Body - return AttachIncompleteManipulationHandler( - AsyncJobSingle( - client, - setLobbyOwner.sourceJobID - ) + lobbyManipulationRequests[setLobbyOwner.sourceJobID] = setLobbyOwner.body.build() + return attachIncompleteManipulationHandler( + job = AsyncJobSingle(client, setLobbyOwner.sourceJobID) ) } - /// - /// Sends a request to obtains a list of lobbies matching the specified criteria. - /// - /// The ID of app for which we're requesting a list of lobbies. - /// An optional list of filters. - /// An optional maximum number of lobbies that will be returned. - /// null, if the request could not be submitted i.e. not yet logged in. Otherwise, an . - public AsyncJob? GetLobbyList( appId: Int, List? filters = null, int maxLobbies = -1 ) - { - if (client.CellID == null) { + // / + // / Sends a request to obtain a list of lobbies matching the specified criteria. + // / + // / The ID of app for which we're requesting a list of lobbies. + // / An optional list of filters. + // / An optional maximum number of lobbies that will be returned. + // / null, if the request could not be submitted i.e. not yet logged in. Otherwise, an . + @JvmOverloads + fun getLobbyList( + appId: Int, + filters: List? = null, + maxLobbies: Int = -1, + ): AsyncJobSingle? { + if (client.cellID == null) { return null } - val getLobbies = ClientMsgProtobuf(EMsg.ClientMMSGetLobbyList) - { - Body = - { - app_id = appId, - cell_id = client.CellID.Value, - public_ip = NetHelpers.GetMsgIPAddress(client.PublicIP!), - num_lobbies_requested = maxLobbies - }, - SourceJobID = client.getNextJobID() + val getLobbies = ClientMsgProtobuf( + CMsgClientMMSGetLobbyList::class.java, + EMsg.ClientMMSGetLobbyList + ).apply { + body.appId = appId + body.cellId = client.cellID!! + body.publicIp = NetHelpers.getMsgIPAddress(client.publicIP!!) + body.numLobbiesRequested = maxLobbies + + sourceJobID = client.getNextJobID() } - if (filters != null) { - foreach(val filter in filters ) - { - getLobbies.Body.filters.Add(filter.Serialize()) - } + filters?.forEach { filter -> + getLobbies.body.addFilters(filter.serialize()) } - Send(getLobbies, appId) + send(msg = getLobbies, appId = appId) - return AsyncJobSingle(client, getLobbies.sourceJobID) + return AsyncJobSingle(client, getLobbies.sourceJobID) } - /// - /// Sends a request to join a lobby. - /// - /// ID of app the lobby belongs to. - /// The SteamID of the lobby that should be joined. - /// null, if the request could not be submitted i.e. not yet logged in. Otherwise, an . - public AsyncJob? JoinLobby( appId: Int, lobbySteamId: SteamID ) - { - val personaName = client.GetHandler()?.GetPersonaName() + // / + // / Sends a request to join a lobby. + // / + // / ID of app the lobby belongs to. + // / The SteamID of the lobby that should be joined. + // / null, if the request could not be submitted i.e. not yet logged in. Otherwise, an . + fun joinLobby( + appId: Int, + lobbySteamId: SteamID, + ): AsyncJobSingle? { + val personaName = client.getHandler()?.getPersonaName() if (personaName == null) { return null } - val joinLobby = ClientMsgProtobuf(EMsg.ClientMMSJoinLobby) - { - Body = - { - app_id = appId, - persona_name = personaName, - steam_id_lobby = lobbySteamId - }, - SourceJobID = client.getNextJobID() + val joinLobby = ClientMsgProtobuf( + CMsgClientMMSJoinLobby::class.java, + EMsg.ClientMMSJoinLobby + ).apply { + body.appId = appId + body.personaName = personaName + body.steamIdLobby = lobbySteamId.convertToUInt64() + + sourceJobID = client.getNextJobID() } - Send(joinLobby, appId) + send(msg = joinLobby, appId = appId) - return AsyncJobSingle(client, joinLobby.sourceJobID) + return AsyncJobSingle(client, joinLobby.sourceJobID) } - /// - /// Sends a request to leave a lobby. - /// - /// ID of app the lobby belongs to. - /// The SteamID of the lobby that should be left. - /// An . + // / + // / Sends a request to leave a lobby. + // / + // / ID of app the lobby belongs to. + // / The SteamID of the lobby that should be left. + // / An . fun leaveLobby(appId: Int, lobbySteamId: SteamID): AsyncJobSingle { val leaveLobby = ClientMsgProtobuf( CMsgClientMMSLeaveLobby::class.java, - EMsg . ClientMMSLeaveLobby + EMsg.ClientMMSLeaveLobby ).apply { - body. appId = appId - body. steamIdLobby = lobbySteamId.convertToUInt64() + body.appId = appId + body.steamIdLobby = lobbySteamId.convertToUInt64() sourceJobID = client.getNextJobID() } - send(leaveLobby, appId) + send(msg = leaveLobby, appId = appId) return AsyncJobSingle(client, leaveLobby.sourceJobID) } - /// - /// Sends a request to obtain a lobby's data. - /// - /// The ID of app which we're attempting to obtain lobby data for. - /// The SteamID of the lobby whose data is being requested. - /// An . + // / + // / Sends a request to obtain a lobby's data. + // / + // / The ID of app which we're attempting to obtain lobby data for. + // / The SteamID of the lobby whose data is being requested. + // / An . fun getLobbyData(appId: Int, lobbySteamId: SteamID): AsyncJobSingle { val getLobbyData = ClientMsgProtobuf( CMsgClientMMSGetLobbyData::class.java, @@ -312,18 +331,18 @@ class SteamMatchmaking : ClientMsgHandler() { sourceJobID = client.getNextJobID() } - send(getLobbyData, appId) + send(msg = getLobbyData, appId = appId) return AsyncJobSingle(client, getLobbyData.sourceJobID) } - /// - /// Sends a lobby invite request. - /// NOTE: Steam provides no functionality to determine if the user was successfully invited. - /// - /// The ID of app which owns the lobby we're inviting a user to. - /// The SteamID of the lobby we're inviting a user to. - /// The SteamID of the user we're inviting. + // / + // / Sends a lobby invite request. + // / NOTE: Steam provides no functionality to determine if the user was successfully invited. + // / + // / The ID of app which owns the lobby we're inviting a user to. + // / The SteamID of the lobby we're inviting a user to. + // / The SteamID of the user we're inviting. fun inviteToLobby(appId: Int, lobbySteamId: SteamID, userSteamId: SteamID) { val getLobbyData = ClientMsgProtobuf( CMsgClientMMSInviteToLobby::class.java, @@ -334,24 +353,24 @@ class SteamMatchmaking : ClientMsgHandler() { body.steamIdUserInvited = userSteamId.convertToUInt64() } - send(getLobbyData, appId) + send(msg = getLobbyData, appId = appId) } - /// - /// Obtains a , by its SteamID, if the data is cached locally. - /// This method does not send a network request. - /// - /// The ID of app which we're attempting to obtain a lobby for. - /// The SteamID of the lobby that should be returned. - /// The corresponding with the specified app and lobby ID, if cached. Otherwise, null. + // / + // / Obtains a , by its SteamID, if the data is cached locally. + // / This method does not send a network request. + // / + // / The ID of app which we're attempting to obtain a lobby for. + // / The SteamID of the lobby that should be returned. + // / The corresponding with the specified app and lobby ID, if cached. Otherwise, null. fun getLobby(appId: Int, lobbySteamId: SteamID): Lobby? = lobbyCache.getLobby(appId, lobbySteamId) - /// - /// Sends a matchmaking message for a specific app. - /// - /// The matchmaking message to send. - /// The ID of the app this message pertains to. - fun send(msg: AClientMsgProtobuf, appId: Int) { + // / + // / Sends a matchmaking message for a specific app. + // / + // / The matchmaking message to send. + // / The ID of the app this message pertains to. + fun > send(msg: ClientMsgProtobuf, appId: Int) { msg.protoHeader.routingAppid = appId client.send(msg) } @@ -360,11 +379,8 @@ class SteamMatchmaking : ClientMsgHandler() { * Handles a client message. This should not be called directly. * @param packetMsg The packet message that contains the data. */ - // TODO this is using the old CB style override fun handleMsg(packetMsg: IPacketMsg) { - val handler = getHandler(packetMsg) ?: return - - // handler?.Invoke(packetMsg) + getHandler(packetMsg)?.invoke(packetMsg) } internal fun clearLobbyCache() { @@ -385,7 +401,325 @@ class SteamMatchmaking : ClientMsgHandler() { // region ClientMsg Handlers - // TODO + private fun handleCreateLobbyResponse(packetMsg: IPacketMsg) { + val createLobbyResponse = ClientMsgProtobuf( + CMsgClientMMSCreateLobbyResponse::class.java, + packetMsg + ) + val body = createLobbyResponse.body + + lobbyManipulationRequests.remove(createLobbyResponse.targetJobID)?.let { request -> + if (body.eresult == EResult.OK.code()) { + val createLobby = request as CMsgClientMMSCreateLobby + val members = List(1) { + Member(client.steamID!!, createLobby.personaNameOwner) + } + + lobbyCache.cacheLobby( + createLobby.appId, + Lobby( + steamID = SteamID(body.steamIdLobby), + lobbyType = ELobbyType.from(createLobby.lobbyType), + lobbyFlags = createLobby.lobbyFlags, + ownerSteamID = client.steamID, + metadata = Lobby.decodeMetadata(createLobby.metadata), + maxMembers = createLobby.maxMembers, + numMembers = 1, + members = members, + distance = null, + weight = null + ) + ) + } + } + + CreateLobbyCallback( + jobID = createLobbyResponse.targetJobID, + appID = body.appId, + result = EResult.from(body.eresult), + lobbySteamID = SteamID(body.steamIdLobby) + ).also(client::postCallback) + } + + fun handleSetLobbyDataResponse(packetMsg: IPacketMsg) { + val setLobbyDataResponse = ClientMsgProtobuf( + CMsgClientMMSSetLobbyDataResponse::class.java, + packetMsg + ) + val body = setLobbyDataResponse.body + + lobbyManipulationRequests.remove(setLobbyDataResponse.targetJobID)?.let { request -> + if (body.eresult == EResult.OK.code()) { + val setLobbyData = request as CMsgClientMMSSetLobbyData + val lobby = lobbyCache.getLobby(appId = setLobbyData.appId, lobbySteamId = setLobbyData.steamIdLobby) + + if (lobby != null) { + val metadata = Lobby.decodeMetadata(setLobbyData.metadata) + + if (setLobbyData.steamIdMember == 0L) { + lobbyCache.cacheLobby( + appId = setLobbyData.appId, + lobby = Lobby( + steamID = lobby.steamID, + lobbyType = ELobbyType.from(setLobbyData.lobbyType), + lobbyFlags = setLobbyData.lobbyFlags, + ownerSteamID = lobby.ownerSteamID, + metadata = metadata, + maxMembers = setLobbyData.maxMembers, + numMembers = lobby.numMembers, + members = lobby.members, + distance = lobby.distance, + weight = lobby.weight + ) + ) + } else { + val members = lobby.members.map { m -> + if (m.steamID.convertToUInt64() == setLobbyData.steamIdMember) { + Member(steamID = m.steamID, personaName = m.personaName, metadata = metadata) + } else { + m + } + } + + lobbyCache.updateLobbyMembers(appId = setLobbyData.appId, lobby = lobby, members = members) + } + } + } + } + + SetLobbyDataCallback( + jobID = setLobbyDataResponse.targetJobID, + appID = body.appId, + result = EResult.from(body.eresult), + lobbySteamID = SteamID(body.steamIdLobby) + ).also(client::postCallback) + } + + fun handleSetLobbyOwnerResponse(packetMsg: IPacketMsg) { + val setLobbyOwnerResponse = ClientMsgProtobuf( + CMsgClientMMSSetLobbyOwnerResponse::class.java, + packetMsg + ) + val body = setLobbyOwnerResponse.body + + lobbyManipulationRequests.remove(setLobbyOwnerResponse.targetJobID)?.let { request -> + if (body.eresult == EResult.OK.code()) { + val setLobbyOwner = request as CMsgClientMMSSetLobbyOwner + lobbyCache.updateLobbyOwner( + appId = body.appId, + lobbySteamId = body.steamIdLobby, + ownerSteamId = setLobbyOwner.steamIdNewOwner + ) + } + } + + SetLobbyOwnerCallback( + jobID = setLobbyOwnerResponse.targetJobID, + appID = body.appId, + result = EResult.from(body.eresult), + lobbySteamID = SteamID(body.steamIdLobby) + ).also(client::postCallback) + } + + fun handleGetLobbyListResponse(packetMsg: IPacketMsg) { + val lobbyListResponse = ClientMsgProtobuf( + CMsgClientMMSGetLobbyListResponse::class.java, + packetMsg + ) + val body = lobbyListResponse.body + + val lobbyList = body.lobbiesList.map { lobby -> + val existingLobby = lobbyCache.getLobby(appId = body.appId, lobbySteamId = lobby.steamId) + val members = existingLobby?.members + Lobby( + steamID = SteamID(lobby.steamId), + lobbyType = ELobbyType.from(lobby.lobbyType), + lobbyFlags = lobby.lobbyFlags, + ownerSteamID = existingLobby?.ownerSteamID, + metadata = Lobby.decodeMetadata(lobby.metadata), + maxMembers = lobby.maxMembers, + numMembers = lobby.numMembers, + members = members ?: listOf(), + distance = lobby.distance, + weight = lobby.weight + ) + } + + lobbyList.forEach { lobby -> + lobbyCache.cacheLobby(appId = body.appId, lobby = lobby) + } + + GetLobbyListCallback( + jobID = lobbyListResponse.targetJobID, + appID = body.appId, + result = EResult.from(body.eresult), + lobbies = lobbyList + ).also(client::postCallback) + } + + fun handleJoinLobbyResponse(packetMsg: IPacketMsg) { + val joinLobbyResponse = ClientMsgProtobuf( + CMsgClientMMSJoinLobbyResponse::class.java, + packetMsg + ) + val body = joinLobbyResponse.body + + var joinedLobby: Lobby? = null + + if (body.hasSteamIdLobby()) { + val members = body.membersList.map { member -> + Member( + steamID = SteamID(member.steamId), + personaName = member.personaName, + metadata = Lobby.decodeMetadata(member.metadata), + ) + } + + val cachedLobby = lobbyCache.getLobby(appId = body.appId, lobbySteamId = body.steamIdLobby) + + joinedLobby = Lobby( + steamID = SteamID(body.steamIdLobby), + lobbyType = ELobbyType.from(body.lobbyType), + lobbyFlags = body.lobbyFlags, + ownerSteamID = SteamID(body.steamIdOwner), + metadata = Lobby.decodeMetadata(body.metadata), + maxMembers = body.maxMembers, + numMembers = members.size, + members = members, + distance = cachedLobby?.distance, + weight = cachedLobby?.weight + ) + + lobbyCache.cacheLobby(appId = body.appId, lobby = joinedLobby) + } + + JoinLobbyCallback( + jobID = joinLobbyResponse.targetJobID, + appID = body.appId, + chatRoomEnterResponse = EChatRoomEnterResponse.from(body.chatRoomEnterResponse), + lobby = joinedLobby + ).also(client::postCallback) + } + + fun handleLeaveLobbyResponse(packetMsg: IPacketMsg) { + val leaveLobbyResponse = ClientMsgProtobuf( + CMsgClientMMSLeaveLobbyResponse::class.java, + packetMsg + ) + val body = leaveLobbyResponse.body + + if (body.eresult == EResult.OK.code()) { + lobbyCache.clearLobbyMembers(appId = body.appId, lobbySteamId = body.steamIdLobby) + } + + LeaveLobbyCallback( + jobID = leaveLobbyResponse.targetJobID, + appID = body.appId, + result = EResult.from(body.eresult), + lobbySteamID = SteamID(body.steamIdLobby) + ).also(client::postCallback) + } + + fun handleLobbyData(packetMsg: IPacketMsg) { + val lobbyDataResponse = ClientMsgProtobuf( + CMsgClientMMSLobbyData::class.java, + packetMsg + ) + val body = lobbyDataResponse.body + + val cachedLobby = lobbyCache.getLobby(appId = body.appId, lobbySteamId = body.steamIdLobby) + val members = if (body.membersList.isEmpty()) { + cachedLobby?.members + } else { + body.membersList.map { member -> + Member( + steamID = SteamID(member.steamId), + personaName = member.personaName, + metadata = Lobby.decodeMetadata(member.metadata) + ) + } + } + + val updatedLobby = Lobby( + steamID = SteamID(body.steamIdLobby), + lobbyType = ELobbyType.from(body.lobbyType), + lobbyFlags = body.lobbyFlags, + ownerSteamID = SteamID(body.steamIdOwner), + metadata = Lobby.decodeMetadata(body.metadata), + maxMembers = body.maxMembers, + numMembers = body.numMembers, + members = members ?: listOf(), + distance = cachedLobby?.distance, + weight = cachedLobby?.weight + ) + + lobbyCache.cacheLobby(appId = body.appId, lobby = updatedLobby) + + LobbyDataCallback( + jobID = lobbyDataResponse.targetJobID, + appID = body.appId, + lobby = updatedLobby + ).also(client::postCallback) + } + + fun handleUserJoinedLobby(packetMsg: IPacketMsg) { + val userJoinedLobby = ClientMsgProtobuf( + CMsgClientMMSUserJoinedLobby::class.java, + packetMsg + ) + val body = userJoinedLobby.body + + val lobby = lobbyCache.getLobby(appId = body.appId, lobbySteamId = body.steamIdLobby) + + if (lobby != null && lobby.members.isNotEmpty()) { + val joiningMember = lobbyCache.addLobbyMember( + appId = body.appId, + lobby = lobby, + memberId = body.steamIdUser, + personaName = body.personaName + ) + + if (joiningMember != null) { + UserJoinedLobbyCallback( + appID = body.appId, + lobbySteamID = SteamID(body.steamIdLobby), + user = joiningMember + ).also(client::postCallback) + } + } + } + + fun handleUserLeftLobby(packetMsg: IPacketMsg) { + val userLeftLobby = ClientMsgProtobuf( + CMsgClientMMSUserLeftLobby::class.java, + packetMsg + ) + val body = userLeftLobby.body + + val lobby = lobbyCache.getLobby(appId = body.appId, lobbySteamId = body.steamIdLobby) + + if (lobby != null && lobby.members.isNotEmpty()) { + val leavingMember = lobbyCache.removeLobbyMember( + appId = body.appId, + lobby = lobby, + memberId = body.steamIdUser + ) + + if (leavingMember == null) { + return + } + + if (leavingMember.steamID == client.steamID) { + lobbyCache.clearLobbyMembers(appId = body.appId, lobbySteamId = body.steamIdLobby) + } + + UserLeftLobbyCallback( + appID = body.appId, + lobbySteamID = SteamID(body.steamIdLobby), + user = leavingMember + ).also(client::postCallback) + } + } // endregion } From cffe1aa5813d6355c40fced43c9a8a71062966b6 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Wed, 21 May 2025 20:43:06 -0500 Subject: [PATCH 4/9] Finish kdoc, tidy up some other docs. --- .../callback/ChatMemberInfoCallback.kt | 2 +- .../steam/handlers/steammatchmaking/Filter.kt | 4 +- .../steammatchmaking/SteamMatchmaking.kt | 154 +++++++++--------- .../callback/CreateLobbyCallback.kt | 3 +- .../callback/GetLobbyListCallback.kt | 5 +- .../callback/JoinLobbyCallback.kt | 3 +- .../callback/LeaveLobbyCallback.kt | 3 +- .../callback/LobbyDataCallback.kt | 4 +- .../callback/SetLobbyDataCallback.kt | 3 +- .../callback/SetLobbyOwnerCallback.kt | 3 +- 10 files changed, 97 insertions(+), 87 deletions(-) diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatMemberInfoCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatMemberInfoCallback.kt index 4fab55d4..549d451d 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatMemberInfoCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamfriends/callback/ChatMemberInfoCallback.kt @@ -35,7 +35,7 @@ class ChatMemberInfoCallback(packetMsg: IPacketMsg) : CallbackMsg() { val type: EChatInfoType /** - * Gets the state change info for member info updates. + * Gets the state change info for [EChatInfoType.StateChange] member info updates. */ var stateChangeInfo: StateChangeDetails? = null diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt index 61106b07..a5871abf 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt @@ -1,3 +1,5 @@ +@file:Suppress("unused") + package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking import `in`.dragonbra.javasteam.enums.ELobbyComparison @@ -8,7 +10,7 @@ import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverM /** * The lobby filter base class. * - * @constructor Base constructor for all filter sub-classes. + * @constructor Base constructor for all filter subclasses. * @param filterType The type of filter. * @param key The metadata key this filter pertains to. * @param comparison The comparison method used by this filter. diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt index 9f839113..2ae82b0e 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt @@ -66,15 +66,15 @@ class SteamMatchmaking : ClientMsgHandler() { private val lobbyCache: LobbyCache = LobbyCache() - // / - // / Sends a request to create a lobby. - // / - // / ID of the app the lobby will belong to. - // / The lobby type. - // / The maximum number of members that may occupy the lobby. - // / The lobby flags. Defaults to 0. - // / The metadata for the lobby. Defaults to null (treated as an empty dictionary). - // / null, if the request could not be submitted i.e., not yet logged in. Otherwise, an . + /** + * Sends a request to create a lobby. + * @param appId ID of the app the lobby will belong to. + * @param lobbyType The lobby type. + * @param maxMembers The maximum number of members that may occupy the lobby. + * @param lobbyFlags The lobby flags. Defaults to 0. + * @param metadata The metadata for the lobby. Defaults to null (treated as an empty dictionary). + * @return null, if the request could not be submitted i.e., not yet logged in. Otherwise, an [CreateLobbyCallback]. + */ @JvmOverloads fun createLobby( appId: Int, @@ -113,16 +113,16 @@ class SteamMatchmaking : ClientMsgHandler() { ) } - // / - // / Sends a request to update a lobby. - // / - // / ID of app the lobby belongs to. - // / The SteamID of the lobby that should be updated. - // / The lobby type. - // / The maximum number of members that may occupy the lobby. - // / The lobby flags. Defaults to 0. - // / The metadata for the lobby. Defaults to null (treated as an empty dictionary). - // / An . + /** + * Sends a request to update a lobby. + * @param appId ID of app the lobby belongs to. + * @param lobbySteamId The SteamID of the lobby that should be updated. + * @param lobbyType The lobby type. + * @param maxMembers The maximum number of members that may occupy the lobby. + * @param lobbyFlags The lobby flags. Defaults to 0. + * @param metadata The metadata for the lobby. Defaults to null (treated as an empty dictionary). + * @return An [SetLobbyDataCallback]. + */ @JvmOverloads fun setLobbyData( appId: Int, @@ -155,13 +155,13 @@ class SteamMatchmaking : ClientMsgHandler() { ) } - // / - // / Sends a request to update the current user's lobby metadata. - // / - // / ID of app the lobby belongs to. - // / The SteamID of the lobby that should be updated. - // / The metadata for the lobby. - // / null, if the request could not be submitted i.e. not yet logged in. Otherwise, an . + /** + * Sends a request to update the current user's lobby metadata. + * @param appId ID of app the lobby belongs to. + * @param lobbySteamId The SteamID of the lobby that should be updated. + * @param metadata The metadata for the lobby. + * @return null, if the request could not be submitted i.e. not yet logged in. Otherwise, an [SetLobbyDataCallback]. + */ fun setLobbyMemberData( appId: Int, lobbySteamId: SteamID, @@ -191,13 +191,13 @@ class SteamMatchmaking : ClientMsgHandler() { ) } - // / - // / Sends a request to update the owner of a lobby. - // / - // / ID of app the lobby belongs to. - // / The SteamID of the lobby that should have its owner updated. - // / The SteamID of the owner. - // / An . + /** + * Sends a request to update the owner of a lobby. + * @param appId ID of app the lobby belongs to. + * @param lobbySteamId The SteamID of the lobby that should have its owner updated. + * @param newOwner The SteamID of the owner. + * @return An [SetLobbyOwnerCallback]. + */ fun setLobbyOwner( appId: Int, lobbySteamId: SteamID, @@ -222,13 +222,13 @@ class SteamMatchmaking : ClientMsgHandler() { ) } - // / - // / Sends a request to obtain a list of lobbies matching the specified criteria. - // / - // / The ID of app for which we're requesting a list of lobbies. - // / An optional list of filters. - // / An optional maximum number of lobbies that will be returned. - // / null, if the request could not be submitted i.e. not yet logged in. Otherwise, an . + /** + * Sends a request to obtain a list of lobbies matching the specified criteria. + * @param appId The ID of app for which we're requesting a list of lobbies. + * @param filters An optional list of filters. + * @param maxLobbies An optional maximum number of lobbies that will be returned. + * @return null, if the request could not be submitted i.e. not yet logged in. Otherwise, an [GetLobbyListCallback]. + */ @JvmOverloads fun getLobbyList( appId: Int, @@ -260,12 +260,12 @@ class SteamMatchmaking : ClientMsgHandler() { return AsyncJobSingle(client, getLobbies.sourceJobID) } - // / - // / Sends a request to join a lobby. - // / - // / ID of app the lobby belongs to. - // / The SteamID of the lobby that should be joined. - // / null, if the request could not be submitted i.e. not yet logged in. Otherwise, an . + /** + * Sends a request to join a lobby. + * @param appId ID of app the lobby belongs to. + * @param lobbySteamId The SteamID of the lobby that should be joined. + * @return null, if the request could not be submitted i.e. not yet logged in. Otherwise, an [JoinLobbyCallback]. + */ fun joinLobby( appId: Int, lobbySteamId: SteamID, @@ -292,12 +292,12 @@ class SteamMatchmaking : ClientMsgHandler() { return AsyncJobSingle(client, joinLobby.sourceJobID) } - // / - // / Sends a request to leave a lobby. - // / - // / ID of app the lobby belongs to. - // / The SteamID of the lobby that should be left. - // / An . + /** + * Sends a request to leave a lobby. + * @param appId ID of app the lobby belongs to. + * @param lobbySteamId The SteamID of the lobby that should be left. + * @return An [LeaveLobbyCallback]. + */ fun leaveLobby(appId: Int, lobbySteamId: SteamID): AsyncJobSingle { val leaveLobby = ClientMsgProtobuf( CMsgClientMMSLeaveLobby::class.java, @@ -314,12 +314,12 @@ class SteamMatchmaking : ClientMsgHandler() { return AsyncJobSingle(client, leaveLobby.sourceJobID) } - // / - // / Sends a request to obtain a lobby's data. - // / - // / The ID of app which we're attempting to obtain lobby data for. - // / The SteamID of the lobby whose data is being requested. - // / An . + /** + * Sends a request to obtain a lobby's data. + * @param appId The ID of app which we're attempting to obtain lobby data for. + * @param lobbySteamId The SteamID of the lobby whose data is being requested. + * @return An [LobbyDataCallback]. + */ fun getLobbyData(appId: Int, lobbySteamId: SteamID): AsyncJobSingle { val getLobbyData = ClientMsgProtobuf( CMsgClientMMSGetLobbyData::class.java, @@ -336,13 +336,13 @@ class SteamMatchmaking : ClientMsgHandler() { return AsyncJobSingle(client, getLobbyData.sourceJobID) } - // / - // / Sends a lobby invite request. - // / NOTE: Steam provides no functionality to determine if the user was successfully invited. - // / - // / The ID of app which owns the lobby we're inviting a user to. - // / The SteamID of the lobby we're inviting a user to. - // / The SteamID of the user we're inviting. + /** + * Sends a lobby invite request. + * NOTE: Steam provides no functionality to determine if the user was successfully invited. + * @param appId The ID of app which owns the lobby we're inviting a user to. + * @param lobbySteamId The SteamID of the lobby we're inviting a user to. + * @param userSteamId The SteamID of the user we're inviting. + */ fun inviteToLobby(appId: Int, lobbySteamId: SteamID, userSteamId: SteamID) { val getLobbyData = ClientMsgProtobuf( CMsgClientMMSInviteToLobby::class.java, @@ -356,20 +356,20 @@ class SteamMatchmaking : ClientMsgHandler() { send(msg = getLobbyData, appId = appId) } - // / - // / Obtains a , by its SteamID, if the data is cached locally. - // / This method does not send a network request. - // / - // / The ID of app which we're attempting to obtain a lobby for. - // / The SteamID of the lobby that should be returned. - // / The corresponding with the specified app and lobby ID, if cached. Otherwise, null. + /** + * Obtains a [Lobby] by its [SteamID], if the data is cached locally. + * This method does not send a network request. + * @param appId The ID of app which we're attempting to obtain a lobby for. + * @param lobbySteamId The SteamID of the lobby that should be returned. + * @return The [Lobby] corresponding with the specified app and lobby ID, if cached. Otherwise, null. + */ fun getLobby(appId: Int, lobbySteamId: SteamID): Lobby? = lobbyCache.getLobby(appId, lobbySteamId) - // / - // / Sends a matchmaking message for a specific app. - // / - // / The matchmaking message to send. - // / The ID of the app this message pertains to. + /** + * Sends a matchmaking message for a specific app. + * @param msg The matchmaking message to send. + * @param appId The ID of the app this message pertains to. + */ fun > send(msg: ClientMsgProtobuf, appId: Int) { msg.protoHeader.routingAppid = appId client.send(msg) diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/CreateLobbyCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/CreateLobbyCallback.kt index 4fa3e5f0..975b2854 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/CreateLobbyCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/CreateLobbyCallback.kt @@ -1,12 +1,13 @@ package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback import `in`.dragonbra.javasteam.enums.EResult +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.SteamMatchmaking import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg import `in`.dragonbra.javasteam.types.JobID import `in`.dragonbra.javasteam.types.SteamID /** - * This callback is fired in response to [CreateLobby]. + * This callback is fired in response to [SteamMatchmaking.createLobby]. * * @param appID ID of the app the created lobby belongs to. * @param result The result of the request. diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/GetLobbyListCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/GetLobbyListCallback.kt index d535cbd2..ecd9f6c5 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/GetLobbyListCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/GetLobbyListCallback.kt @@ -2,15 +2,16 @@ package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback import `in`.dragonbra.javasteam.enums.EResult import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.Lobby +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.SteamMatchmaking import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg import `in`.dragonbra.javasteam.types.JobID /** - * This callback is fired in response to . + * This callback is fired in response to [SteamMatchmaking.getLobbyList] * * @param appID ID of the app the lobbies belongs to. * @param result The result of the request. - * @param lobbies The list of lobbies matching the criteria specified with [GetLobbyList]. + * @param lobbies The list of lobbies matching the criteria specified with [SteamMatchmaking.getLobbyList]. */ class GetLobbyListCallback( jobID: JobID, diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/JoinLobbyCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/JoinLobbyCallback.kt index 5fc730c8..1829c7e9 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/JoinLobbyCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/JoinLobbyCallback.kt @@ -2,11 +2,12 @@ package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback import `in`.dragonbra.javasteam.enums.EChatRoomEnterResponse import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.Lobby +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.SteamMatchmaking import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg import `in`.dragonbra.javasteam.types.JobID /** - * This callback is fired in response to [JoinLobby]. + * This callback is fired in response to [SteamMatchmaking.joinLobby]. * * @param appID ID of the app the targeted lobby belongs to. * @param chatRoomEnterResponse The result of the request. diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LeaveLobbyCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LeaveLobbyCallback.kt index b7928eea..f28f455a 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LeaveLobbyCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LeaveLobbyCallback.kt @@ -1,12 +1,13 @@ package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback import `in`.dragonbra.javasteam.enums.EResult +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.SteamMatchmaking import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg import `in`.dragonbra.javasteam.types.JobID import `in`.dragonbra.javasteam.types.SteamID /** - * This callback is fired in response to [LeaveLobby]. + * This callback is fired in response to [SteamMatchmaking.leaveLobby]. * * @param appID ID of the app the targeted lobby belongs to. * @param result The result of the request. diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LobbyDataCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LobbyDataCallback.kt index 0bd4dae3..0c9cb74f 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LobbyDataCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LobbyDataCallback.kt @@ -1,11 +1,13 @@ package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.Lobby +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.SteamMatchmaking import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg import `in`.dragonbra.javasteam.types.JobID /** - * This callback is fired in response to [GetLobbyData], as well as whenever Steam sends us updated lobby data. + * This callback is fired in response to [SteamMatchmaking.getLobbyData], + * as well as whenever Steam sends us updated lobby data. * * @param appID ID of the app the updated lobby belongs to. * @param lobby The lobby that was updated. diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyDataCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyDataCallback.kt index e6766d5d..7d2d5c6b 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyDataCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyDataCallback.kt @@ -1,12 +1,13 @@ package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback import `in`.dragonbra.javasteam.enums.EResult +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.SteamMatchmaking import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg import `in`.dragonbra.javasteam.types.JobID import `in`.dragonbra.javasteam.types.SteamID /** - * This callback is fired in response to [SetLobbyData]. + * This callback is fired in response to [SteamMatchmaking.setLobbyData]. * * @param appID ID of the app the targeted lobby belongs to. * @param result The result of the request. diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyOwnerCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyOwnerCallback.kt index 30fc28d9..c7e4f020 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyOwnerCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyOwnerCallback.kt @@ -1,12 +1,13 @@ package `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.callback import `in`.dragonbra.javasteam.enums.EResult +import `in`.dragonbra.javasteam.steam.handlers.steammatchmaking.SteamMatchmaking import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg import `in`.dragonbra.javasteam.types.JobID import `in`.dragonbra.javasteam.types.SteamID /** - * This callback is fired in response to [SetLobbyOwner]. + * This callback is fired in response to [SteamMatchmaking.setLobbyOwner]. * * @param appID ID of the app the targeted lobby belongs to. * @param result The result of the request. From c33714c104eb3c9b0096cd353f259cd4e15960e9 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Wed, 21 May 2025 23:26:58 -0500 Subject: [PATCH 5/9] Update kdoc --- .../steam/handlers/steammatchmaking/Filter.kt | 18 ++++++++++++++++++ .../steam/handlers/steammatchmaking/Lobby.kt | 3 +++ .../handlers/steammatchmaking/LobbyCache.kt | 3 +++ .../steam/handlers/steammatchmaking/Member.kt | 3 +++ .../steammatchmaking/SteamMatchmaking.kt | 3 +++ .../callback/CreateLobbyCallback.kt | 3 +++ .../callback/GetLobbyListCallback.kt | 3 +++ .../callback/JoinLobbyCallback.kt | 3 +++ .../callback/LeaveLobbyCallback.kt | 3 +++ .../callback/LobbyDataCallback.kt | 3 +++ .../callback/SetLobbyDataCallback.kt | 3 +++ .../callback/SetLobbyOwnerCallback.kt | 3 +++ .../callback/UserJoinedLobbyCallback.kt | 3 +++ .../callback/UserLeftLobbyCallback.kt | 3 +++ 14 files changed, 57 insertions(+) diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt index a5871abf..c23184ae 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt @@ -18,6 +18,9 @@ import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverM * @property filterType The type of filter. * @property key The metadata key this filter pertains to. Under certain circumstances e.g. a distance filter, this will be an empty string. * @property comparison The comparison method used by this filter. + * + * @author Lossy + * @since 2025-05-21 */ abstract class Filter( val filterType: ELobbyFilterType, @@ -43,6 +46,9 @@ abstract class Filter( * @param value Steam distance filter value. * * @property value Steam distance filter value. + * + * @author Lossy + * @since 2025-05-21 */ class DistanceFilter( val value: ELobbyDistanceFilter, @@ -70,6 +76,9 @@ class DistanceFilter( * @param value Integer value to compare against * * @param value Integer value that lobbies' metadata value should be close to. + * + * @author Lossy + * @since 2025-05-21 */ sealed class NearValueFilter( key: String, @@ -98,6 +107,9 @@ sealed class NearValueFilter( * @param value Integer value to compare against. * * @property value Integer value to compare against. + * + * @author Lossy + * @since 2025-05-21 */ sealed class NumericalFilter( key: String, @@ -125,6 +137,9 @@ sealed class NumericalFilter( * @param slotsAvailable Integer value to compare against. * * @property slotsAvailable Minimum number of slots available in the lobby. + * + * @author Lossy + * @since 2025-05-21 */ sealed class SlotsAvailableFilter( val slotsAvailable: Int, @@ -152,6 +167,9 @@ sealed class SlotsAvailableFilter( * @param value String value to compare against. * * @property value String value to compare against. + * + * @author Lossy + * @since 2025-05-21 */ sealed class StringFilter( key: String, diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt index 060ce8ba..7b950dbb 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Lobby.kt @@ -21,6 +21,9 @@ import `in`.dragonbra.javasteam.util.stream.MemoryStream * @param members A list of lobby members. This will only be populated for the user's current lobby. * @param distance The distance of the lobby. * @param weight The weight of the lobby. + * + * @author Lossy + * @since 2025-05-21 */ class Lobby( val steamID: SteamID, diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/LobbyCache.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/LobbyCache.kt index 49b466b9..e8bcaf35 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/LobbyCache.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/LobbyCache.kt @@ -5,6 +5,9 @@ import java.util.concurrent.ConcurrentHashMap /** * Cache for managing Steam lobbies. + * + * @author Lossy + * @since 2025-05-21 */ @Suppress("unused") class LobbyCache { diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Member.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Member.kt index bdbb6534..7efbabb3 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Member.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Member.kt @@ -7,6 +7,9 @@ import `in`.dragonbra.javasteam.types.SteamID * @param steamID SteamID of the lobby member. * @param personaName Steam persona of the lobby member. * @param metadata Metadata attached to the lobby member. + * + * @author Lossy + * @since 2025-05-21 */ data class Member( val steamID: SteamID, diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt index 2ae82b0e..841d3b52 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt @@ -45,6 +45,9 @@ import java.util.concurrent.ConcurrentHashMap /** * This handler is used for creating, joining and obtaining lobby information. + * + * @author Lossy + * @since 2025-05-21 */ @Suppress("unused") class SteamMatchmaking : ClientMsgHandler() { diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/CreateLobbyCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/CreateLobbyCallback.kt index 975b2854..45a9e88a 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/CreateLobbyCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/CreateLobbyCallback.kt @@ -12,6 +12,9 @@ import `in`.dragonbra.javasteam.types.SteamID * @param appID ID of the app the created lobby belongs to. * @param result The result of the request. * @param lobbySteamID The SteamID of the created lobby. + * + * @author Lossy + * @since 2025-05-21 */ class CreateLobbyCallback( jobID: JobID, diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/GetLobbyListCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/GetLobbyListCallback.kt index ecd9f6c5..753f1d19 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/GetLobbyListCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/GetLobbyListCallback.kt @@ -12,6 +12,9 @@ import `in`.dragonbra.javasteam.types.JobID * @param appID ID of the app the lobbies belongs to. * @param result The result of the request. * @param lobbies The list of lobbies matching the criteria specified with [SteamMatchmaking.getLobbyList]. + * + * @author Lossy + * @since 2025-05-21 */ class GetLobbyListCallback( jobID: JobID, diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/JoinLobbyCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/JoinLobbyCallback.kt index 1829c7e9..392418eb 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/JoinLobbyCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/JoinLobbyCallback.kt @@ -12,6 +12,9 @@ import `in`.dragonbra.javasteam.types.JobID * @param appID ID of the app the targeted lobby belongs to. * @param chatRoomEnterResponse The result of the request. * @param lobby The joined [Lobby], when [chatRoomEnterResponse] equals [EChatRoomEnterResponse.Success], otherwise null + * + * @author Lossy + * @since 2025-05-21 */ class JoinLobbyCallback( jobID: JobID, diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LeaveLobbyCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LeaveLobbyCallback.kt index f28f455a..662ece78 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LeaveLobbyCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LeaveLobbyCallback.kt @@ -12,6 +12,9 @@ import `in`.dragonbra.javasteam.types.SteamID * @param appID ID of the app the targeted lobby belongs to. * @param result The result of the request. * @param lobbySteamID The SteamID of the targeted Lobby. + * + * @author Lossy + * @since 2025-05-21 */ class LeaveLobbyCallback( jobID: JobID, diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LobbyDataCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LobbyDataCallback.kt index 0c9cb74f..9f4f5895 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LobbyDataCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/LobbyDataCallback.kt @@ -11,6 +11,9 @@ import `in`.dragonbra.javasteam.types.JobID * * @param appID ID of the app the updated lobby belongs to. * @param lobby The lobby that was updated. + * + * @author Lossy + * @since 2025-05-21 */ class LobbyDataCallback( jobID: JobID, diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyDataCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyDataCallback.kt index 7d2d5c6b..e4e5cbe9 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyDataCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyDataCallback.kt @@ -12,6 +12,9 @@ import `in`.dragonbra.javasteam.types.SteamID * @param appID ID of the app the targeted lobby belongs to. * @param result The result of the request. * @param lobbySteamID The SteamID of the targeted Lobby. + * + * @author Lossy + * @since 2025-05-21 */ class SetLobbyDataCallback( jobID: JobID, diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyOwnerCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyOwnerCallback.kt index c7e4f020..ebb40662 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyOwnerCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/SetLobbyOwnerCallback.kt @@ -12,6 +12,9 @@ import `in`.dragonbra.javasteam.types.SteamID * @param appID ID of the app the targeted lobby belongs to. * @param result The result of the request. * @param lobbySteamID The SteamID of the targeted Lobby. + * + * @author Lossy + * @since 2025-05-21 */ class SetLobbyOwnerCallback( jobID: JobID, diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserJoinedLobbyCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserJoinedLobbyCallback.kt index 5ac97813..ea408af5 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserJoinedLobbyCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserJoinedLobbyCallback.kt @@ -10,6 +10,9 @@ import `in`.dragonbra.javasteam.types.SteamID * @param appID ID of the app the lobby belongs to. * @param lobbySteamID The SteamID of the lobby that a member joined. * @param user The lobby member that joined. + * + * @author Lossy + * @since 2025-05-21 */ class UserJoinedLobbyCallback( val appID: Int, diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserLeftLobbyCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserLeftLobbyCallback.kt index bc66021d..23ce300d 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserLeftLobbyCallback.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/callback/UserLeftLobbyCallback.kt @@ -10,6 +10,9 @@ import `in`.dragonbra.javasteam.types.SteamID * @param appID ID of the app the lobby belongs to. * @param lobbySteamID The SteamID of the lobby that a member left. * @param user The lobby member that left. + * + * @author Lossy + * @since 2025-05-21 */ class UserLeftLobbyCallback( val appID: Int, From fe4f8ae76f171533003c11f56c25bce1ece58f0b Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Thu, 22 May 2025 00:23:53 -0500 Subject: [PATCH 6/9] Fix filters --- .../steam/handlers/steammatchmaking/Filter.kt | 18 +++++++++--------- .../steammatchmaking/SteamMatchmaking.kt | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt index c23184ae..c4849d5a 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/Filter.kt @@ -33,9 +33,9 @@ abstract class Filter( */ open fun serialize(): SteammessagesClientserverMms.CMsgClientMMSGetLobbyList.Filter.Builder = SteammessagesClientserverMms.CMsgClientMMSGetLobbyList.Filter.newBuilder().apply { - this.filterType = filterType - this.key = key - this.comparision = comparison.code() + this.filterType = this@Filter.filterType.code() + this.key = this@Filter.key + this.comparision = this@Filter.comparison.code() } } @@ -80,7 +80,7 @@ class DistanceFilter( * @author Lossy * @since 2025-05-21 */ -sealed class NearValueFilter( +class NearValueFilter( key: String, val value: Int, ) : Filter( @@ -111,10 +111,10 @@ sealed class NearValueFilter( * @author Lossy * @since 2025-05-21 */ -sealed class NumericalFilter( +class NumericalFilter( key: String, - comparison: ELobbyComparison, val value: Int, + comparison: ELobbyComparison, ) : Filter( filterType = ELobbyFilterType.Numerical, key = key, @@ -141,7 +141,7 @@ sealed class NumericalFilter( * @author Lossy * @since 2025-05-21 */ -sealed class SlotsAvailableFilter( +class SlotsAvailableFilter( val slotsAvailable: Int, ) : Filter( filterType = ELobbyFilterType.SlotsAvailable, @@ -171,10 +171,10 @@ sealed class SlotsAvailableFilter( * @author Lossy * @since 2025-05-21 */ -sealed class StringFilter( +class StringFilter( key: String, - comparison: ELobbyComparison, val value: String, + comparison: ELobbyComparison, ) : Filter( filterType = ELobbyFilterType.String, key = key, diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt index 841d3b52..139f2c09 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steammatchmaking/SteamMatchmaking.kt @@ -255,7 +255,7 @@ class SteamMatchmaking : ClientMsgHandler() { } filters?.forEach { filter -> - getLobbies.body.addFilters(filter.serialize()) + getLobbies.body.addFilters(filter.serialize().build()) } send(msg = getLobbies, appId = appId) From e7fa1e3b3db41facafca15128d800350eae73e64 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Thu, 22 May 2025 00:25:57 -0500 Subject: [PATCH 7/9] Add sample app. --- .../SampleSteamMatchmaking.java | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_014_steammatchmaking/SampleSteamMatchmaking.java diff --git a/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_014_steammatchmaking/SampleSteamMatchmaking.java b/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_014_steammatchmaking/SampleSteamMatchmaking.java new file mode 100644 index 00000000..565fb6dd --- /dev/null +++ b/javasteam-samples/src/main/java/in/dragonbra/javasteamsamples/_014_steammatchmaking/SampleSteamMatchmaking.java @@ -0,0 +1,247 @@ +package in.dragonbra.javasteamsamples._014_steammatchmaking; + +import in.dragonbra.javasteam.enums.ELobbyComparison; +import in.dragonbra.javasteam.enums.ELobbyDistanceFilter; +import in.dragonbra.javasteam.enums.EResult; +import in.dragonbra.javasteam.steam.authentication.*; +import in.dragonbra.javasteam.steam.handlers.steammatchmaking.*; +import in.dragonbra.javasteam.steam.handlers.steamuser.LogOnDetails; +import in.dragonbra.javasteam.steam.handlers.steamuser.SteamUser; +import in.dragonbra.javasteam.steam.handlers.steamuser.callback.LoggedOffCallback; +import in.dragonbra.javasteam.steam.handlers.steamuser.callback.LoggedOnCallback; +import in.dragonbra.javasteam.steam.steamclient.SteamClient; +import in.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackManager; +import in.dragonbra.javasteam.steam.steamclient.callbacks.ConnectedCallback; +import in.dragonbra.javasteam.steam.steamclient.callbacks.DisconnectedCallback; +import in.dragonbra.javasteam.util.log.DefaultLogListener; +import in.dragonbra.javasteam.util.log.LogManager; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CancellationException; + +/** + * @author lossy + * @since 2025-05-21 + */ +@SuppressWarnings("FieldCanBeLocal") +public class SampleSteamMatchmaking implements Runnable { + + private final Integer appID = 480; // Team Fortress 2 + + private SteamClient steamClient; + + private CallbackManager manager; + + private SteamUser steamUser; + + private SteamMatchmaking steamMatchmaking; + + private boolean isRunning; + + private final String user; + + private final String pass; + + private List subscriptions; + + public SampleSteamMatchmaking(String user, String pass) { + this.user = user; + this.pass = pass; + } + + public static void main(String[] args) { + if (args.length < 2) { + System.out.println("Sample1: No username and password specified!"); + return; + } + + LogManager.addListener(new DefaultLogListener()); + + new SampleSteamMatchmaking(args[0], args[1]).run(); + } + + @Override + public void run() { + // create our steamclient instance using default configuration + steamClient = new SteamClient(); + + // create the callback manager which will route callbacks to function calls + manager = new CallbackManager(steamClient); + + // get the steamuser handler, which is used for logging on after successfully connecting + steamUser = steamClient.getHandler(SteamUser.class); + + // get the steammatchmaking handler. + steamMatchmaking = steamClient.getHandler(SteamMatchmaking.class); + + // The callbacks are a closeable, and to properly fix + // "'Closeable' used without 'try'-with-resources statement", they should be closed once done. + // Usually putting them in a list and close each of them once the client is finished is recommended. + subscriptions = new ArrayList<>(); + + // register a few callbacks we're interested in + // these are registered upon creation to a callback manager, which will then route the callbacks + // to the functions specified + subscriptions.add(manager.subscribe(ConnectedCallback.class, this::onConnected)); + subscriptions.add(manager.subscribe(DisconnectedCallback.class, this::onDisconnected)); + subscriptions.add(manager.subscribe(LoggedOnCallback.class, this::onLoggedOn)); + subscriptions.add(manager.subscribe(LoggedOffCallback.class, this::onLoggedOff)); + + isRunning = true; + + System.out.println("Connecting to steam..."); + + // initiate the connection + steamClient.connect(); + + // create our callback handling loop + while (isRunning) { + // in order for the callbacks to get routed, they need to be handled by the manager + manager.runWaitCallbacks(1000L); + } + + // Close the subscriptions when done. + System.out.println("Closing " + subscriptions.size() + " callbacks"); + for (var subscription : subscriptions) { + try { + subscription.close(); + } catch (IOException e) { + System.out.println("Couldn't close a callback."); + } + } + } + + @SuppressWarnings("DanglingJavadoc") + private void onConnected(ConnectedCallback callback) { + System.out.println("Connected to Steam! Logging in " + user + "..."); + + var shouldRememberPassword = false; + + AuthSessionDetails authDetails = new AuthSessionDetails(); + authDetails.username = user; + authDetails.password = pass; + authDetails.persistentSession = shouldRememberPassword; + + /** + * {@link UserConsoleAuthenticator} is the default authenticator implementation provided by JavaSteam + * for ease of use which blocks the thread and asks for user input to enter the code. + * However, if you require special handling (e.g. you have the TOTP secret and can generate codes on the fly), + * you can implement your own {@link IAuthenticator}. + */ + authDetails.authenticator = new UserConsoleAuthenticator(); + + try { + // Begin authenticating via credentials. + var authSession = steamClient.getAuthentication().beginAuthSessionViaCredentials(authDetails).get(); + + // Note: This is blocking, it would be up to you to make it non-blocking for Java. + // Note: Kotlin uses should use ".pollingWaitForResult()" as its a suspending function. + AuthPollResult pollResponse = authSession.pollingWaitForResult().get(); + + // Logon to Steam with the access token we have received + // Note that we are using RefreshToken for logging on here + LogOnDetails details = new LogOnDetails(); + details.setUsername(pollResponse.getAccountName()); + details.setAccessToken(pollResponse.getRefreshToken()); + + // Set LoginID to a non-zero value if you have another client connected using the same account, + // the same private ip, and same public ip. + details.setLoginID(149); + + steamUser.logOn(details); + + } catch (Exception e) { + // List a couple of exceptions that could be important to handle. + if (e instanceof AuthenticationException) { + System.err.println("An Authentication error has occurred. " + e.getMessage()); + } else if (e instanceof CancellationException) { + System.err.println("An Cancellation exception was raised. Usually means a timeout occurred. " + e.getMessage()); + } else { + System.err.println("An error occurred:" + e.getMessage()); + } + + steamUser.logOff(); + } + } + + private void onDisconnected(DisconnectedCallback callback) { + System.out.println("Disconnected from Steam. User initialized: " + callback.isUserInitiated()); + + // If the disconnection was not user initiated, we will retry connecting to steam again after a short delay. + if (callback.isUserInitiated()) { + isRunning = false; + } else { + try { + Thread.sleep(2000L); + steamClient.connect(); + } catch (InterruptedException e) { + System.err.println("An Interrupted exception occurred. " + e.getMessage()); + } + } + } + + private void onLoggedOn(LoggedOnCallback callback) { + if (callback.getResult() != EResult.OK) { + System.out.println("Unable to logon to Steam: " + callback.getResult() + " / " + callback.getExtendedResult()); + + isRunning = false; + return; + } + + System.out.println("Successfully logged on!"); + + // at this point, we'd be able to perform actions on Steam + + try { + var filters = List.of( + new DistanceFilter(ELobbyDistanceFilter.Worldwide), + new StringFilter("CONMETHOD", "P2P", ELobbyComparison.Equal) + ); + var lobbyListCallback = steamMatchmaking.getLobbyList(appID, filters, 20).toFuture().get(); + + System.out.println("App ID: " + lobbyListCallback.getAppID()); + System.out.println("Result: " + lobbyListCallback.getResult()); + System.out.println("Lobby Size: " + lobbyListCallback.getLobbies().size()); + lobbyListCallback.getLobbies().forEach(lobby -> { + System.out.println("\tsteamID: " + lobby.getSteamID().convertToUInt64()); + System.out.println("\tlobbyType: " + lobby.getLobbyType()); + System.out.println("\tlobbyFlags: " + lobby.getLobbyFlags()); + System.out.println("\townerSteamID: " + lobby.getOwnerSteamID()); + + System.out.println("\tMetadata:"); + lobby.getMetadata().forEach((k, v) -> System.out.println("\t\tkey: " + k + " value: " + v)); + + System.out.println("\tmaxMembers: " + lobby.getMaxMembers()); + System.out.println("\tnumMembers: " + lobby.getNumMembers()); + + System.out.println("\tMembers:"); + lobby.getMembers().forEach(member -> { + System.out.println("\t\tsteamID: " + member.getSteamID().convertToUInt64()); + System.out.println("\t\tpersonaName: " + member.getPersonaName()); + System.out.println("\t\tMember Metadata:"); + member.getMetadata().forEach((k, v) -> + System.out.println("\t\t\tkey: " + k + " value: " + v)); + }); + + System.out.println("\tdistance: " + lobby.getDistance()); + System.out.println("\tweight: " + lobby.getWeight()); + System.out.println("\n"); + }); + + + } catch (Exception e) { + System.err.println(e.getMessage()); + } finally { + steamUser.logOff(); + } + } + + private void onLoggedOff(LoggedOffCallback callback) { + System.out.println("Logged off of Steam: " + callback.getResult()); + + isRunning = false; + } +} From 7981a079c61511a74ea3fbc1674042efe80d4a06 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Thu, 22 May 2025 11:53:36 -0500 Subject: [PATCH 8/9] Fix handlers_count size --- .../in/dragonbra/javasteam/steam/steamclient/SteamClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt b/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt index 3eab6f6f..84be65dc 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/steamclient/SteamClient.kt @@ -288,6 +288,6 @@ class SteamClient @JvmOverloads constructor( companion object { private val logger: Logger = LogManager.getLogger(SteamClient::class.java) - private const val HANDLERS_COUNT = 14 + private const val HANDLERS_COUNT = 15 } } From 9b6d5a948830731a74229f31e4d76712a8d11ef1 Mon Sep 17 00:00:00 2001 From: LossyDragon Date: Thu, 22 May 2025 11:54:13 -0500 Subject: [PATCH 9/9] Add basic test to ensure handlers array size matches handlers count --- .../steam/steamclient/SteamClientTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/test/java/in/dragonbra/javasteam/steam/steamclient/SteamClientTest.java b/src/test/java/in/dragonbra/javasteam/steam/steamclient/SteamClientTest.java index f8eb6024..59bac2f1 100644 --- a/src/test/java/in/dragonbra/javasteam/steam/steamclient/SteamClientTest.java +++ b/src/test/java/in/dragonbra/javasteam/steam/steamclient/SteamClientTest.java @@ -20,6 +20,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.HashMap; + class SteamClientTest { private SteamClient client; @@ -29,6 +31,28 @@ public void setUp() { client = new SteamClient(); } + /** + * Check to make sure the allocated handlers size matches the initial handlers account. + */ + @Test + public void handlersCountCheck() { + try { + // get the private 'handlers' variable + var handlersField = SteamClient.class.getDeclaredField("handlers"); + handlersField.setAccessible(true); + var handlers = (HashMap) handlersField.get(client); + + // get the private static 'HANDLERS_COUNT' variable + var handlersCountField = SteamClient.class.getDeclaredField("HANDLERS_COUNT"); + handlersCountField.setAccessible(true); + var handlersCount = (Integer) handlersCountField.get(null); + + Assertions.assertEquals(handlersCount, handlers.size(), "Handlers size should match HANDLERS_COUNT"); + } catch (Exception e) { + Assertions.fail(e); + } + } + @Test public void constructorSetsInitialHandlers() { Assertions.assertNotNull(client.getHandler(SteamFriends.class));