diff --git a/build.gradle.kts b/build.gradle.kts index 9e7720d..1fc8263 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,12 @@ plugins { - kotlin("jvm") version "2.0.20" + kotlin("jvm") version "2.0.21" `maven-publish` java alias(libs.plugins.grgit) alias(libs.plugins.fabric.loom) + alias(libs.plugins.ktor) + alias(libs.plugins.kotlinx.serialization) } val archivesBaseName = "${project.property("archives_base_name").toString()}+mc${libs.versions.minecraft.get()}" @@ -12,6 +14,7 @@ version = getModVersion() group = project.property("maven_group")!! repositories { + mavenCentral() maven("https://api.modrinth.com/maven") maven("https://maven.terraformersmc.com/") maven("https://maven.parchmentmc.org") @@ -40,6 +43,8 @@ dependencies { modLocalRuntime(libs.bundles.dev.mods) include(modImplementation("gay.asoji:fmw:1.0.0+build.8")!!) // just to avoid the basic long metadata calls + + implementation(libs.bundles.ktor) } // Write the version to the fabric.mod.json @@ -105,6 +110,13 @@ publishing { // } } +application { + mainClass.set("one.devos.nautical.exposeplayers.ExposePlayersKt") + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + fun getModVersion(): String { val modVersion = project.property("mod_version") val buildId = System.getenv("GITHUB_RUN_NUMBER") diff --git a/gradle.properties b/gradle.properties index 23c4758..9e2cf91 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,6 +5,6 @@ org.gradle.parallel=true # Mod Properties mod_version=0.1.0 maven_group=one.devos.nautical -archives_base_name=template +archives_base_name=exposeplayers # Dependencies are handled in ./gradle/libs.versions.toml \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3eca845..b275e1f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,23 +8,36 @@ fabric_language_kotlin = "1.12.2+kotlin.2.0.20" sodium_version = "mc1.21-0.6.0-beta.2-fabric" mod_menu_version = "11.0.2" joml_version = "1.10.5" +ktor = "3.0.0" + [libraries] minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } #quilt_mappings = { module = "org.quiltmc:quilt-mappings", version.ref = "quilt_mappings" } -fabric_loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric_loader" } +fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric_loader" } fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric_api" } -fabric_language_kotlin = { module = "net.fabricmc:fabric-language-kotlin", version.ref = "fabric_language_kotlin" } +fabric-language-kotlin = { module = "net.fabricmc:fabric-language-kotlin", version.ref = "fabric_language_kotlin" } sodium = { module = "maven.modrinth:sodium", version.ref = "sodium_version" } joml = { module = "org.joml:joml", version.ref = "joml_version" } mod_menu = { module = "com.terraformersmc:modmenu", version.ref = "mod_menu_version" } +ktor-server-content-negotiation-jvm = { module = "io.ktor:ktor-server-content-negotiation-jvm", version.ref = "ktor" } +ktor-server-core-jvm = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" } +ktor-serialization-kotlinx-json-jvm = { module = "io.ktor:ktor-serialization-kotlinx-json-jvm", version.ref = "ktor" } +ktor-server-netty-jvm = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" } # If you have multiple similar dependencies, you can declare a dependency bundle and reference it on the build script with "libs.bundles.example". [bundles] dev_mods = [ "joml", "sodium" ] dependencies = [ "mod_menu" ] +ktor = [ + "ktor-server-content-negotiation-jvm", + "ktor-server-core-jvm", + "ktor-serialization-kotlinx-json-jvm", + "ktor-server-netty-jvm" +] [plugins] grgit = { id = "org.ajoberstar.grgit", version = "5.2.2"} fabric_loom = { id = "fabric-loom", version = "1.7-SNAPSHOT" } - +ktor = { id = "io.ktor.plugin", version = "3.0.0" } +kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.0.21" } \ No newline at end of file diff --git a/src/main/java/one/devos/nautical/exposeplayers/mixin/PlayerListMixin.java b/src/main/java/one/devos/nautical/exposeplayers/mixin/PlayerListMixin.java new file mode 100644 index 0000000..165ba12 --- /dev/null +++ b/src/main/java/one/devos/nautical/exposeplayers/mixin/PlayerListMixin.java @@ -0,0 +1,19 @@ +package one.devos.nautical.exposeplayers.mixin; + +import net.minecraft.server.players.PlayerList; +import net.minecraft.stats.ServerStatsCounter; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; +import java.util.UUID; + +@Mixin(PlayerList.class) +public interface PlayerListMixin { + + @Accessor("stats") + default Map getStats() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/one/devos/nautical/template/mixin/ExampleMixin.java b/src/main/java/one/devos/nautical/template/mixin/ExampleMixin.java deleted file mode 100644 index fa1574b..0000000 --- a/src/main/java/one/devos/nautical/template/mixin/ExampleMixin.java +++ /dev/null @@ -1,22 +0,0 @@ -package one.devos.nautical.template.mixin; - -import net.minecraft.client.gui.screens.TitleScreen; -import one.devos.nautical.template.TemplateMod; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -/** - * This is an Example mixin that prints out `This line is printed by the Nautical template mod mixin` on initialization - * of the TitleScreen, but this mixin can be replaced with any other thing you want to mixin. - *

- * Mixins **must** be in Java! They **cannot** be in Kotlin! - */ -@Mixin(TitleScreen.class) -public class ExampleMixin { - @Inject(at = @At("HEAD"), method = "init()V") - private void init(CallbackInfo info) { - TemplateMod.INSTANCE.getLOGGER().info("This line is printed by the Nautical template mod mixin!"); - } -} \ No newline at end of file diff --git a/src/main/kotlin/one/devos/nautical/exposeplayers/ExposePlayers.kt b/src/main/kotlin/one/devos/nautical/exposeplayers/ExposePlayers.kt new file mode 100644 index 0000000..aa2bbf8 --- /dev/null +++ b/src/main/kotlin/one/devos/nautical/exposeplayers/ExposePlayers.kt @@ -0,0 +1,36 @@ +package one.devos.nautical.exposeplayers + +import gay.asoji.fmw.FMW +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import net.fabricmc.api.ModInitializer +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents +import one.devos.nautical.exposeplayers.plugins.configureRouting +import one.devos.nautical.exposeplayers.plugins.configureSerialization +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +object ExposePlayers : ModInitializer { + val MOD_ID: String = "exposeplayers" + val LOGGER: Logger = LoggerFactory.getLogger(MOD_ID) + val MOD_NAME: String = FMW.getName(MOD_ID) + + private var server: EmbeddedServer? = null + + override fun onInitialize() { + LOGGER.info("[${MOD_NAME}] Starting up ExposePlayers") + + ServerLifecycleEvents.SERVER_STARTED.register { server -> + this.server?.stop() + this.server = embeddedServer(Netty, port = 64589, host = "0.0.0.0", module = { + configureRouting(server) + configureSerialization() + }).start(wait = false) + } + + ServerLifecycleEvents.SERVER_STOPPED.register { server -> + this.server?.stop() + this.server = null + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/one/devos/nautical/exposeplayers/plugins/Routing.kt b/src/main/kotlin/one/devos/nautical/exposeplayers/plugins/Routing.kt new file mode 100644 index 0000000..c424641 --- /dev/null +++ b/src/main/kotlin/one/devos/nautical/exposeplayers/plugins/Routing.kt @@ -0,0 +1,70 @@ +package one.devos.nautical.exposeplayers.plugins + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.server.MinecraftServer +import net.minecraft.stats.Stats +import one.devos.nautical.exposeplayers.utils.UUIDSerializer +import one.devos.nautical.exposeplayers.utils.getPlayerStatsByUuid +import java.util.UUID + +fun Application.configureRouting(server: MinecraftServer) { + routing { + get { + call.respondText("Hello world!") + } + + get("/players") { + call.respond(PlayersEndpointResponse( + server.playerCount, + server.playerList.players.map { player -> + PlayerInfo(player.uuid, player.name.string) + } + )) + } + + get("/players/stats/{player_name}") { + val playerName = call.parameters["player_name"] + if (playerName == null) { + call.respond(HttpStatusCode.BadRequest) + return@get + } + + val playerUuid = server.profileCache?.get(playerName)?.get()?.id + if (playerUuid == null) { + call.respond(HttpStatusCode.BadRequest) + return@get + } + + val statsCounter = server.playerList.getPlayerStatsByUuid(playerUuid, playerName) +// call.respond(BuiltInRegistries.STAT_TYPE.map { statisticType -> { +// statisticType.flatMap +// }) + + + } + } +} + +@Serializable +private data class PlayerInfo( + @Serializable(with = UUIDSerializer::class) val uuid: UUID, + val name: String +) + +@Serializable +private data class PlayersEndpointResponse( + val count: Int, + val players: List<@Contextual PlayerInfo> +) + +@Serializable +private data class PlayerStatistic( + val displayName: String, + val value: Any +) \ No newline at end of file diff --git a/src/main/kotlin/one/devos/nautical/exposeplayers/plugins/Serialization.kt b/src/main/kotlin/one/devos/nautical/exposeplayers/plugins/Serialization.kt new file mode 100644 index 0000000..aef7054 --- /dev/null +++ b/src/main/kotlin/one/devos/nautical/exposeplayers/plugins/Serialization.kt @@ -0,0 +1,21 @@ +package one.devos.nautical.exposeplayers.plugins + +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.routing.* +import io.ktor.util.reflect.* + +fun Application.configureSerialization() { + install(ContentNegotiation) { + json() + } + +// routing { +// get("/json/kotlinx-serialization") { +// call.respond( +// mapOf("hello" to "world") +// ) +// } +// } +} \ No newline at end of file diff --git a/src/main/kotlin/one/devos/nautical/exposeplayers/utils/UUIDSerializer.kt b/src/main/kotlin/one/devos/nautical/exposeplayers/utils/UUIDSerializer.kt new file mode 100644 index 0000000..1e87570 --- /dev/null +++ b/src/main/kotlin/one/devos/nautical/exposeplayers/utils/UUIDSerializer.kt @@ -0,0 +1,22 @@ +package one.devos.nautical.exposeplayers.utils + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.util.UUID + +object UUIDSerializer : KSerializer { + + override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): UUID { + return UUID.fromString(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: UUID) { + encoder.encodeString(value.toString()) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/one/devos/nautical/exposeplayers/utils/extensions.kt b/src/main/kotlin/one/devos/nautical/exposeplayers/utils/extensions.kt new file mode 100644 index 0000000..71ebef7 --- /dev/null +++ b/src/main/kotlin/one/devos/nautical/exposeplayers/utils/extensions.kt @@ -0,0 +1,32 @@ +package one.devos.nautical.exposeplayers.utils + +import net.minecraft.FileUtil +import net.minecraft.server.players.PlayerList +import net.minecraft.stats.ServerStatsCounter +import net.minecraft.world.level.storage.LevelResource +import one.devos.nautical.exposeplayers.mixin.PlayerListMixin +import java.io.File +import java.util.UUID + +val PlayerList.stats: MutableMap + get() = (this as PlayerListMixin).stats + +fun PlayerList.getPlayerStatsByUuid(uuid: UUID, name: String): ServerStatsCounter { + var statsCounter = stats[uuid] + if (statsCounter == null) { + val directory = this.server.getWorldPath(LevelResource.PLAYER_STATS_DIR).toFile() + val uuidFile = File(directory, "$uuid.json") + if (!uuidFile.exists()) { + val playerNameFile = File(directory, "$name.json") + val path = playerNameFile.toPath() + if (FileUtil.isPathNormalized(path) && FileUtil.isPathPortable(path) && path.startsWith(directory.path) && playerNameFile.isFile) { + playerNameFile.renameTo(uuidFile); + } + } + + statsCounter = ServerStatsCounter(this.server, uuidFile) + stats[uuid] = statsCounter + } + + return statsCounter!! +} diff --git a/src/main/kotlin/one/devos/nautical/template/TemplateMod.kt b/src/main/kotlin/one/devos/nautical/template/TemplateMod.kt deleted file mode 100644 index dec02f2..0000000 --- a/src/main/kotlin/one/devos/nautical/template/TemplateMod.kt +++ /dev/null @@ -1,39 +0,0 @@ -package one.devos.nautical.template - -import gay.asoji.fmw.FMW -import net.fabricmc.api.ModInitializer -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -/** - * This is your mod's main entrypoint class, this runs on both Client and Server. - * What you put in here will be loaded on mod initialization time - */ -object TemplateMod : ModInitializer { - /** - * This `MOD_ID` value is what your `id` in the fabric.mod.json should be. - * Anything that references your mod should call this value/string. - */ - val MOD_ID: String = "template" - - /** - * This logger is used to write text to the console and the log file. - * It is considered best practice to use your mod id as the logger's name. - * That way, it's clear which mod wrote info, warnings, and errors. - */ - val LOGGER: Logger = LoggerFactory.getLogger(MOD_ID) - - /** - * This `MOD_NAME` gets what your mod's user-friendly name is from the `fabric.mod.json` - */ - val MOD_NAME: String = FMW.getName(MOD_ID) - - /** - * This code runs as soon as Minecraft is in a mod-load-ready state. - * However, some things (like resources) may still be uninitialized. - * Proceed with mild caution. - */ - override fun onInitialize() { - LOGGER.info("[${MOD_NAME}] Hello Fabric world from $MOD_NAME/$MOD_ID") - } -} \ No newline at end of file diff --git a/src/main/kotlin/one/devos/nautical/template/client/TemplateModClient.kt b/src/main/kotlin/one/devos/nautical/template/client/TemplateModClient.kt deleted file mode 100644 index f851530..0000000 --- a/src/main/kotlin/one/devos/nautical/template/client/TemplateModClient.kt +++ /dev/null @@ -1,17 +0,0 @@ -package one.devos.nautical.template.client - -import net.fabricmc.api.ClientModInitializer - -/** - * This entrypoint is suitable for setting up client-specific logic, such as rendering. - */ -object TemplateModClient : ClientModInitializer { - - /** - * This code runs on the Minecraft Client as soon as it's in a mod-load-ready state. - * Just like with the Main entrypoint, some things may be still uninitialized, so proceed with caution. - */ - override fun onInitializeClient() { - - } -} \ No newline at end of file diff --git a/src/main/resources/assets/template/icon.png b/src/main/resources/assets/exposeplayers/icon.png similarity index 100% rename from src/main/resources/assets/template/icon.png rename to src/main/resources/assets/exposeplayers/icon.png diff --git a/src/main/resources/template.mixins.json b/src/main/resources/exposeplayers.mixins.json similarity index 57% rename from src/main/resources/template.mixins.json rename to src/main/resources/exposeplayers.mixins.json index e20c656..6367384 100644 --- a/src/main/resources/template.mixins.json +++ b/src/main/resources/exposeplayers.mixins.json @@ -1,11 +1,9 @@ { "required": true, - "package": "one.devos.nautical.template.mixin", + "package": "one.devos.nautical.exposeplayers.mixin", "compatibilityLevel": "JAVA_21", "mixins": [ - ], - "client": [ - "ExampleMixin" + "PlayerListMixin" ], "injectors": { "defaultRequire": 1 diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index f76831b..d433298 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,19 +1,19 @@ { "schemaVersion": 1, - "id": "template", + "id": "exposeplayers", "version": "${version}", - "name": "Template mod", - "description": "This is an example description! Tell everyone what your mod is about!", + "name": "ExposePlayers", + "description": "A web server mod that exposes basic player stats", "authors": [ "devOS: Sanity Edition", "Team Nautical" ], "contributors": [ - "Put your GitHub username here!" + "asojidev" ], "contact": { "homepage": "https://devos.one/", - "sources": "https://github.com/devOS-Sanity-Edition/fabric-nautical-template-kt" + "sources": "https://github.com/devOS-Sanity-Edition/ExposePlayers" }, "license": "MIT", "icon": "assets/template/icon.png", @@ -22,18 +22,12 @@ "main": [ { "adapter": "kotlin", - "value": "one.devos.nautical.template.TemplateMod" - } - ], - "client": [ - { - "adapter": "kotlin", - "value": "one.devos.nautical.template.client.TemplateModClient" + "value": "one.devos.nautical.exposeplayers.ExposePlayers" } ] }, "mixins": [ - "template.mixins.json" + "exposeplayers.mixins.json" ], "depends": { "fabricloader": ">=0.16.5",