diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c9e72842..3236a389 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ commons-validator = "1.9.0" # https://mvnrepository.com/artifact/commons-validat gson = "2.13.1" # https://mvnrepository.com/artifact/com.google.code.gson/gson jacoco = "0.8.13" # https://www.eclemma.org/jacoco kotlin-coroutines = "1.10.2" # https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core +ktor = "3.2.1" # https://mvnrepository.com/artifact/io.ktor/ktor-client-cio okHttp = "5.0.0-alpha.14" # https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp protobuf = "4.30.2" # https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java protobuf-gradle = "0.9.5" # https://mvnrepository.com/artifact/com.google.protobuf/protobuf-gradle-plugin @@ -40,9 +41,9 @@ commons-validator = { module = "commons-validator:commons-validator", version.re gson = { module = "com.google.code.gson:gson", version.ref = "gson" } kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" } kotlin-stdib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } -ktor-client-cio = { module = "io.ktor:ktor-client-cio", version = "3.0.3" } -ktor-client-core = { module = "io.ktor:ktor-client-core", version = "3.0.3" } -ktor-client-websocket = { module = "io.ktor:ktor-client-websockets", version = "3.0.3" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-websocket = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" } okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okHttp" } protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/AchievementBlocks.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/AchievementBlocks.kt new file mode 100644 index 00000000..94a57dfd --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/AchievementBlocks.kt @@ -0,0 +1,10 @@ +package `in`.dragonbra.javasteam.steam.handlers.steamuserstats + +// JavaSteam Addition +/** + * A Block of achievements with the timestamp of when the achievement (in order of the schema) is unlocked. + * @param achievementId the achievement id. + * @param unlockTime a [List] of integers containing when an achievement was unlocked. + * An unlockTime of 0 means it has not been achieved, unlocked achievements are displayed as valve-timestamps. + */ +data class AchievementBlocks(val achievementId: Int, val unlockTime: List) diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/Stats.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/Stats.kt new file mode 100644 index 00000000..0c5ac8ba --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/Stats.kt @@ -0,0 +1,10 @@ +package `in`.dragonbra.javasteam.steam.handlers.steamuserstats + +// JavaSteam Addition +/** + * A Class representing stat values of a game. + * This data is commonly used for richer stats in games that support it. For example: Left 4 Dead 2. + * @param statId The id of the stat. This is used to reference the id in the schema. + * @param statValue The value of the stat. + */ +data class Stats(val statId: Int, val statValue: Int) diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/SteamUserStats.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/SteamUserStats.kt index 5666a59c..722d2c53 100644 --- a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/SteamUserStats.kt +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/SteamUserStats.kt @@ -9,12 +9,15 @@ import `in`.dragonbra.javasteam.enums.EMsg import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserver2.CMsgDPGetNumberOfCurrentPlayers import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverLbs.CMsgClientLBSFindOrCreateLB import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverLbs.CMsgClientLBSGetLBEntries +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverUserstats.CMsgClientGetUserStats import `in`.dragonbra.javasteam.steam.handlers.ClientMsgHandler import `in`.dragonbra.javasteam.steam.handlers.steamuserstats.callback.FindOrCreateLeaderboardCallback import `in`.dragonbra.javasteam.steam.handlers.steamuserstats.callback.LeaderboardEntriesCallback import `in`.dragonbra.javasteam.steam.handlers.steamuserstats.callback.NumberOfPlayersCallback +import `in`.dragonbra.javasteam.steam.handlers.steamuserstats.callback.UserStatsCallback import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg import `in`.dragonbra.javasteam.types.AsyncJobSingle +import `in`.dragonbra.javasteam.types.SteamID /** * This handler handles Steam user statistic related actions. @@ -147,6 +150,29 @@ class SteamUserStats : ClientMsgHandler() { return AsyncJobSingle(this.client, msg.sourceJobID) } + // JavaSteam addition. + /** + * Gets the Stats-Schema for the specified app. This schema includes Global Achievements and Stats, + * @param appId The appID of the game. + * @param steamID The [SteamID] that owns the game. Note the SteamID user has to have a public profile. + * @return The Job ID of the request. This can be used to find the appropriate [UserStatsCallback]. + */ + fun getUserStats(appId: Int, steamID: SteamID): AsyncJobSingle { + val msg = ClientMsgProtobuf( + CMsgClientGetUserStats::class.java, + EMsg.ClientGetUserStats + ).apply { + sourceJobID = client.getNextJobID() + + body.gameId = appId.toLong() + body.steamIdForUser = steamID.convertToUInt64() + } + + client.send(msg) + + return AsyncJobSingle(this.client, msg.sourceJobID) + } + /** * Handles a client message. This should not be called directly. * @param packetMsg The packet message that contains the data. @@ -163,6 +189,7 @@ class SteamUserStats : ClientMsgHandler() { EMsg.ClientGetNumberOfCurrentPlayersDPResponse -> NumberOfPlayersCallback(packetMsg) EMsg.ClientLBSFindOrCreateLBResponse -> FindOrCreateLeaderboardCallback(packetMsg) EMsg.ClientLBSGetLBEntriesResponse -> LeaderboardEntriesCallback(packetMsg) + EMsg.ClientGetUserStatsResponse -> UserStatsCallback(packetMsg) else -> null } } diff --git a/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/callback/UserStatsCallback.kt b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/callback/UserStatsCallback.kt new file mode 100644 index 00000000..de8eca6b --- /dev/null +++ b/src/main/java/in/dragonbra/javasteam/steam/handlers/steamuserstats/callback/UserStatsCallback.kt @@ -0,0 +1,80 @@ +package `in`.dragonbra.javasteam.steam.handlers.steamuserstats.callback + +import com.google.protobuf.ByteString +import `in`.dragonbra.javasteam.base.ClientMsgProtobuf +import `in`.dragonbra.javasteam.base.IPacketMsg +import `in`.dragonbra.javasteam.enums.EResult +import `in`.dragonbra.javasteam.protobufs.steamclient.SteammessagesClientserverUserstats.CMsgClientGetUserStatsResponse +import `in`.dragonbra.javasteam.steam.handlers.steamuserstats.AchievementBlocks +import `in`.dragonbra.javasteam.steam.handlers.steamuserstats.Stats +import `in`.dragonbra.javasteam.steam.handlers.steamuserstats.SteamUserStats +import `in`.dragonbra.javasteam.steam.steamclient.callbackmgr.CallbackMsg +import `in`.dragonbra.javasteam.types.KeyValue +import `in`.dragonbra.javasteam.util.stream.MemoryStream + +// JavaSteam Addition +/** + * This callback is fired in response to [SteamUserStats.getUserStats]. + */ +class UserStatsCallback(packetMsg: IPacketMsg?) : CallbackMsg() { + + /** + * Gets the result. + */ + val result: EResult + + /** + * The game id of the stats. + */ + val gameId: Long + + /** + * The crc of the stats. + */ + val crcStats: Int + + /** + * The raw schema in [ByteString]. + */ + val schema: ByteString + + /** + * A [List] of [Stats]. + */ + val stats: List + + /** + * A [List] of [AchievementBlocks]. + */ + val achievementBlocks: List + + /** + * The schema converted to [KeyValue]. + */ + val schemaKeyValues: KeyValue = KeyValue() + + init { + val msg = ClientMsgProtobuf( + CMsgClientGetUserStatsResponse::class.java, + packetMsg + ) + val resp = msg.body + + jobID = msg.targetJobID + result = EResult.from(resp.eresult) + + gameId = resp.gameId + crcStats = resp.crcStats + schema = resp.schema + stats = resp.statsList.map { + Stats(statId = it.statId, statValue = it.statValue) + } + achievementBlocks = resp.achievementBlocksList.map { + AchievementBlocks(achievementId = it.achievementId, unlockTime = it.unlockTimeList) + } + + MemoryStream(schema.toByteArray()).use { + schemaKeyValues.tryReadAsBinary(it) + } + } +}