feat: 서버 상태 모드 기능 개선

- Essentials 닉네임 동기화 기능 추가

- AuthCommand 명령어 추가

- 플레이어 데이터 저장소 개선
This commit is contained in:
Caadiq 2025-12-22 14:57:35 +09:00
parent f2fb0ad324
commit 67f9cac26f
5 changed files with 337 additions and 96 deletions

View file

@ -5,13 +5,19 @@ import com.beemer.essentials.nickname.NicknameDataStore
import java.time.ZoneId import java.time.ZoneId
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import net.minecraft.ChatFormatting
import net.minecraft.network.chat.ClickEvent
import net.minecraft.network.chat.Component import net.minecraft.network.chat.Component
import net.minecraft.network.chat.MutableComponent
import net.minecraft.network.chat.Style
import net.minecraft.server.level.ServerPlayer import net.minecraft.server.level.ServerPlayer
object ChatUtils { object ChatUtils {
private const val SECTION = '\u00a7' private const val SECTION = '\u00a7'
private val legacyPattern = Regex("&([0-9A-FK-ORa-fk-or])") private val legacyPattern = Regex("&([0-9A-FK-ORa-fk-or])")
private val hexPattern = Regex("&#([A-Fa-f0-9]{6})") private val hexPattern = Regex("&#([A-Fa-f0-9]{6})")
private val urlPattern =
Regex("(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)", RegexOption.IGNORE_CASE)
private val timeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm") private val timeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
private fun translateChatColors(text: String?): String { private fun translateChatColors(text: String?): String {
@ -56,6 +62,42 @@ object ChatUtils {
return translateChatColors(withPlaceholders) return translateChatColors(withPlaceholders)
} }
/** 문자열에서 URL을 찾아 클릭 가능한 Component로 변환 */
private fun parseWithUrls(text: String): MutableComponent {
val result: MutableComponent = Component.empty()
var lastEnd = 0
urlPattern.findAll(text).forEach { match ->
// URL 앞의 일반 텍스트 추가
if (match.range.first > lastEnd) {
result.append(Component.literal(text.substring(lastEnd, match.range.first)))
}
// URL 부분 - 클릭 가능하게 + 밑줄 + 파란색
val url = match.value
val urlComponent =
Component.literal(url)
.withStyle(
Style.EMPTY
.withClickEvent(
ClickEvent(ClickEvent.Action.OPEN_URL, url)
)
.withColor(ChatFormatting.AQUA)
.withUnderlined(true)
)
result.append(urlComponent)
lastEnd = match.range.last + 1
}
// 마지막 URL 이후의 텍스트 추가
if (lastEnd < text.length) {
result.append(Component.literal(text.substring(lastEnd)))
}
return result
}
fun broadcastJoin(player: ServerPlayer, firstTime: Boolean) { fun broadcastJoin(player: ServerPlayer, firstTime: Boolean) {
val key = if (firstTime) "first-time-join" else "join" val key = if (firstTime) "first-time-join" else "join"
val template = ChatConfig.getFormat(key) ?: defaultJoinTemplate(firstTime) val template = ChatConfig.getFormat(key) ?: defaultJoinTemplate(firstTime)
@ -72,7 +114,10 @@ object ChatUtils {
fun broadcastChat(player: ServerPlayer, rawMessage: String) { fun broadcastChat(player: ServerPlayer, rawMessage: String) {
val template = ChatConfig.getFormat("chat") ?: "%name% &7&l: &f%message%" val template = ChatConfig.getFormat("chat") ?: "%name% &7&l: &f%message%"
val formatted = chatFormatting(template, player, rawMessage) val formatted = chatFormatting(template, player, rawMessage)
player.server.playerList.broadcastSystemMessage(Component.literal(formatted), false)
// URL을 클릭 가능한 링크로 변환
val component = parseWithUrls(formatted)
player.server.playerList.broadcastSystemMessage(component, false)
} }
private fun defaultJoinTemplate(firstTime: Boolean): String = private fun defaultJoinTemplate(firstTime: Boolean): String =

View file

@ -1,30 +1,30 @@
package co.caadiq.serverstatus package co.caadiq.serverstatus
import co.caadiq.serverstatus.command.AuthCommand
import co.caadiq.serverstatus.config.ModConfig import co.caadiq.serverstatus.config.ModConfig
import co.caadiq.serverstatus.config.PlayerDataStore import co.caadiq.serverstatus.config.PlayerDataStore
import co.caadiq.serverstatus.data.PlayerStatsCollector
import co.caadiq.serverstatus.data.PlayerTracker import co.caadiq.serverstatus.data.PlayerTracker
import co.caadiq.serverstatus.data.ServerDataCollector import co.caadiq.serverstatus.data.ServerDataCollector
import co.caadiq.serverstatus.data.WorldDataCollector import co.caadiq.serverstatus.data.WorldDataCollector
import co.caadiq.serverstatus.data.PlayerStatsCollector
import co.caadiq.serverstatus.network.HttpApiServer import co.caadiq.serverstatus.network.HttpApiServer
import net.neoforged.bus.api.IEventBus import net.neoforged.bus.api.IEventBus
import net.neoforged.bus.api.SubscribeEvent
import net.neoforged.fml.ModContainer import net.neoforged.fml.ModContainer
import net.neoforged.fml.common.Mod import net.neoforged.fml.common.Mod
import net.neoforged.fml.event.lifecycle.FMLDedicatedServerSetupEvent import net.neoforged.fml.event.lifecycle.FMLDedicatedServerSetupEvent
import net.neoforged.neoforge.common.NeoForge import net.neoforged.neoforge.common.NeoForge
import net.neoforged.neoforge.event.RegisterCommandsEvent
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
/** /** 메인 모드 클래스 서버 상태 정보를 HTTP API로 제공 */
* 메인 모드 클래스
* 서버 상태 정보를 HTTP API로 제공
*/
@Mod(ServerStatusMod.MOD_ID) @Mod(ServerStatusMod.MOD_ID)
class ServerStatusMod(modBus: IEventBus, container: ModContainer) { class ServerStatusMod(modBus: IEventBus, container: ModContainer) {
companion object { companion object {
const val MOD_ID = "serverstatus" const val MOD_ID = "serverstatus"
val LOGGER = LogManager.getLogger(MOD_ID) val LOGGER = LogManager.getLogger(MOD_ID)
lateinit var config: ModConfig lateinit var config: ModConfig
private set private set
lateinit var playerDataStore: PlayerDataStore lateinit var playerDataStore: PlayerDataStore
@ -38,38 +38,43 @@ class ServerStatusMod(modBus: IEventBus, container: ModContainer) {
lateinit var httpApiServer: HttpApiServer lateinit var httpApiServer: HttpApiServer
private set private set
} }
init { init {
LOGGER.info("[$MOD_ID] 모드 초기화 중...") LOGGER.info("[$MOD_ID] 모드 초기화 중...")
// 설정 초기화 // 설정 초기화
config = ModConfig.load() config = ModConfig.load()
playerDataStore = PlayerDataStore.load() playerDataStore = PlayerDataStore.load()
// 서버 전용 이벤트 등록 // 서버 전용 이벤트 등록
modBus.addListener(::onServerSetup) modBus.addListener(::onServerSetup)
// 플레이어 이벤트 리스너 등록 // 플레이어 이벤트 및 명령어 리스너 등록
NeoForge.EVENT_BUS.register(PlayerTracker) NeoForge.EVENT_BUS.register(PlayerTracker)
NeoForge.EVENT_BUS.register(this)
LOGGER.info("[$MOD_ID] 모드 초기화 완료") LOGGER.info("[$MOD_ID] 모드 초기화 완료")
} }
/** /** 명령어 등록 이벤트 */
* 전용 서버 설정 이벤트 @SubscribeEvent
*/ fun onRegisterCommands(event: RegisterCommandsEvent) {
AuthCommand.register(event.dispatcher)
}
/** 전용 서버 설정 이벤트 */
private fun onServerSetup(event: FMLDedicatedServerSetupEvent) { private fun onServerSetup(event: FMLDedicatedServerSetupEvent) {
LOGGER.info("[$MOD_ID] 서버 설정 중...") LOGGER.info("[$MOD_ID] 서버 설정 중...")
// 데이터 수집기 초기화 // 데이터 수집기 초기화
serverDataCollector = ServerDataCollector() serverDataCollector = ServerDataCollector()
worldDataCollector = WorldDataCollector() worldDataCollector = WorldDataCollector()
playerStatsCollector = PlayerStatsCollector() playerStatsCollector = PlayerStatsCollector()
// HTTP API 서버 시작 // HTTP API 서버 시작
httpApiServer = HttpApiServer(config.httpPort) httpApiServer = HttpApiServer(config.httpPort)
httpApiServer.start() httpApiServer.start()
LOGGER.info("[$MOD_ID] HTTP API 서버 시작됨 (포트: ${config.httpPort})") LOGGER.info("[$MOD_ID] HTTP API 서버 시작됨 (포트: ${config.httpPort})")
} }
} }

View file

@ -0,0 +1,153 @@
package co.caadiq.serverstatus.command
import co.caadiq.serverstatus.ServerStatusMod
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.context.CommandContext
import java.net.HttpURLConnection
import java.net.URI
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
/** /인증 명령어 처리 웹사이트 계정 연동을 위한 명령어 */
object AuthCommand {
private val json = Json {
ignoreUnknownKeys = true
encodeDefaults = true
}
// 웹 서버 연동 API URL (환경 또는 설정에서 가져올 수 있음)
private const val WEB_API_URL = "http://minecraft-status/link/verify"
/** 명령어 등록 */
fun register(dispatcher: CommandDispatcher<CommandSourceStack>) {
// /인증 <토큰>
dispatcher.register(
Commands.literal("인증")
.then(
Commands.argument("token", StringArgumentType.word()).executes {
context ->
executeAuth(context)
}
)
)
ServerStatusMod.LOGGER.info("[${ServerStatusMod.MOD_ID}] /인증 명령어 등록됨")
}
/** 인증 명령어 실행 */
private fun executeAuth(context: CommandContext<CommandSourceStack>): Int {
val source = context.source
val player = source.player
// 플레이어만 사용 가능
if (player == null) {
source.sendFailure(Component.literal("§c플레이어만 이 명령어를 사용할 수 있습니다."))
return 0
}
val token = StringArgumentType.getString(context, "token").uppercase()
// 토큰 검증 (6자리 영숫자)
if (!token.matches(Regex("^[A-Z0-9]{6}$"))) {
source.sendFailure(Component.literal("§c유효하지 않은 인증 코드입니다."))
return 0
}
// 비동기로 웹 API 호출
Thread {
try {
val result = verifyWithWebServer(token, player)
// UI 스레드에서 메시지 전송
player.server.execute {
if (result.success) {
player.sendSystemMessage(
Component.literal("§a§l✓ 계정 연동 완료!\n§7웹사이트에서 프로필이 업데이트됩니다.")
)
} else {
player.sendSystemMessage(
Component.literal(
"§c✗ 연동 실패: ${result.error ?: "알 수 없는 오류"}"
)
)
}
}
} catch (e: Exception) {
ServerStatusMod.LOGGER.error(
"[${ServerStatusMod.MOD_ID}] 인증 오류: ${e.message}"
)
player.server.execute {
player.sendSystemMessage(Component.literal("§c✗ 연동 처리 중 오류가 발생했습니다."))
}
}
}
.start()
// 즉시 응답
source.sendSuccess({ Component.literal("§e인증 처리 중...") }, false)
return 1
}
/** 웹 서버에 연동 요청 */
private fun verifyWithWebServer(token: String, player: ServerPlayer): VerifyResponse {
val requestBody =
json.encodeToString(
VerifyRequest(
token = token,
uuid = player.stringUUID,
name = player.name.string
)
)
val url = URI(WEB_API_URL).toURL()
val conn = url.openConnection() as HttpURLConnection
try {
conn.requestMethod = "POST"
conn.doOutput = true
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8")
conn.connectTimeout = 5000
conn.readTimeout = 5000
// 요청 전송
conn.outputStream.use { os -> os.write(requestBody.toByteArray(Charsets.UTF_8)) }
// 응답 처리
val responseCode = conn.responseCode
val responseBody =
if (responseCode in 200..299) {
conn.inputStream.bufferedReader().use { it.readText() }
} else {
conn.errorStream?.bufferedReader()?.use { it.readText() } ?: ""
}
ServerStatusMod.LOGGER.info(
"[${ServerStatusMod.MOD_ID}] 인증 응답: $responseCode - $responseBody"
)
return try {
json.decodeFromString<VerifyResponse>(responseBody)
} catch (e: Exception) {
VerifyResponse(success = responseCode in 200..299, error = responseBody)
}
} finally {
conn.disconnect()
}
}
@Serializable data class VerifyRequest(val token: String, val uuid: String, val name: String)
@Serializable
data class VerifyResponse(
val success: Boolean = false,
val message: String? = null,
val error: String? = null
)
}

View file

@ -62,6 +62,24 @@ data class PlayerDataStore(
private val configDir = FMLPaths.CONFIGDIR.get().resolve(ServerStatusMod.MOD_ID) private val configDir = FMLPaths.CONFIGDIR.get().resolve(ServerStatusMod.MOD_ID)
private val dataPath = configDir.resolve("players.json") private val dataPath = configDir.resolve("players.json")
// Essentials 닉네임 파일 경로
private val essentialsNicknamePath = FMLPaths.CONFIGDIR.get().resolve("essentials/nicknames.json")
/**
* Essentials 닉네임 조회
*/
private fun loadEssentialsNicknames(): Map<String, String> {
return try {
if (essentialsNicknamePath.exists()) {
json.decodeFromString<Map<String, String>>(essentialsNicknamePath.readText())
} else {
emptyMap()
}
} catch (e: Exception) {
emptyMap()
}
}
/** /**
* 플레이어 데이터 로드 * 플레이어 데이터 로드
*/ */
@ -79,6 +97,34 @@ data class PlayerDataStore(
} }
} }
/**
* 특정 플레이어의 Essentials 닉네임 가져오기 (없으면 저장된 이름)
*/
fun getDisplayName(uuid: String): String {
val essentialsNicks = loadEssentialsNicknames()
return essentialsNicks[uuid] ?: players[uuid]?.name ?: "Unknown"
}
/**
* 모든 플레이어 닉네임 Essentials와 동기화
*/
fun syncNicknamesFromEssentials() {
val essentialsNicks = loadEssentialsNicknames()
var synced = 0
players.forEach { (uuid, player) ->
essentialsNicks[uuid]?.let { nick ->
if (player.name != nick) {
player.name = nick
synced++
}
}
}
if (synced > 0) {
save()
ServerStatusMod.LOGGER.info("[${ServerStatusMod.MOD_ID}] Essentials 닉네임 동기화: ${synced}")
}
}
/** /**
* 데이터 저장 * 데이터 저장
*/ */

View file

@ -1,101 +1,90 @@
package co.caadiq.serverstatus.network package co.caadiq.serverstatus.network
import co.caadiq.serverstatus.ServerStatusMod import co.caadiq.serverstatus.ServerStatusMod
import co.caadiq.serverstatus.data.AdvancementsInfo
import co.caadiq.serverstatus.data.PlayerStats
import co.caadiq.serverstatus.data.WorldInfo import co.caadiq.serverstatus.data.WorldInfo
import com.sun.net.httpserver.HttpExchange import com.sun.net.httpserver.HttpExchange
import com.sun.net.httpserver.HttpHandler import com.sun.net.httpserver.HttpHandler
import com.sun.net.httpserver.HttpServer import com.sun.net.httpserver.HttpServer
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
/** /** HTTP API 서버 Java 내장 HttpServer 사용 - 외부 의존성 없음 */
* HTTP API 서버
* Java 내장 HttpServer 사용 - 외부 의존성 없음
*/
class HttpApiServer(private val port: Int) { class HttpApiServer(private val port: Int) {
private val json = Json { private val json = Json {
prettyPrint = false prettyPrint = false
ignoreUnknownKeys = true ignoreUnknownKeys = true
encodeDefaults = true encodeDefaults = true
} }
private var server: HttpServer? = null private var server: HttpServer? = null
/** /** 서버 시작 */
* 서버 시작
*/
fun start() { fun start() {
try { try {
server = HttpServer.create(InetSocketAddress(port), 0) server = HttpServer.create(InetSocketAddress(port), 0)
server?.executor = Executors.newFixedThreadPool(4) server?.executor = Executors.newFixedThreadPool(4)
// 엔드포인트 등록 // 엔드포인트 등록
server?.createContext("/status", StatusHandler()) server?.createContext("/status", StatusHandler())
server?.createContext("/players", PlayersHandler()) server?.createContext("/players", PlayersHandler())
server?.createContext("/player", PlayerHandler()) server?.createContext("/player", PlayerHandler())
server?.createContext("/worlds", WorldsHandler()) server?.createContext("/worlds", WorldsHandler())
server?.start() server?.start()
ServerStatusMod.LOGGER.info("[${ServerStatusMod.MOD_ID}] HTTP API 서버 시작: http://0.0.0.0:$port") ServerStatusMod.LOGGER.info(
"[${ServerStatusMod.MOD_ID}] HTTP API 서버 시작: http://0.0.0.0:$port"
)
} catch (e: Exception) { } catch (e: Exception) {
ServerStatusMod.LOGGER.error("[${ServerStatusMod.MOD_ID}] HTTP 서버 시작 실패: ${e.message}") ServerStatusMod.LOGGER.error("[${ServerStatusMod.MOD_ID}] HTTP 서버 시작 실패: ${e.message}")
} }
} }
/** /** 서버 종료 */
* 서버 종료
*/
fun stop() { fun stop() {
server?.stop(1) server?.stop(1)
} }
/** /** CORS 헤더 추가 및 응답 전송 */
* CORS 헤더 추가 응답 전송
*/
private fun sendJsonResponse(exchange: HttpExchange, response: String, statusCode: Int = 200) { private fun sendJsonResponse(exchange: HttpExchange, response: String, statusCode: Int = 200) {
exchange.responseHeaders.add("Content-Type", "application/json; charset=utf-8") exchange.responseHeaders.add("Content-Type", "application/json; charset=utf-8")
exchange.responseHeaders.add("Access-Control-Allow-Origin", "*") exchange.responseHeaders.add("Access-Control-Allow-Origin", "*")
exchange.responseHeaders.add("Access-Control-Allow-Methods", "GET, OPTIONS") exchange.responseHeaders.add("Access-Control-Allow-Methods", "GET, OPTIONS")
exchange.responseHeaders.add("Access-Control-Allow-Headers", "Content-Type") exchange.responseHeaders.add("Access-Control-Allow-Headers", "Content-Type")
val bytes = response.toByteArray(StandardCharsets.UTF_8) val bytes = response.toByteArray(StandardCharsets.UTF_8)
exchange.sendResponseHeaders(statusCode, bytes.size.toLong()) exchange.sendResponseHeaders(statusCode, bytes.size.toLong())
exchange.responseBody.use { it.write(bytes) } exchange.responseBody.use { it.write(bytes) }
} }
/** /** PlayerData를 PlayerDetail로 변환 (Essentials 닉네임 우선 적용) */
* PlayerData를 PlayerDetail로 변환
*/
private fun toPlayerDetail(player: co.caadiq.serverstatus.config.PlayerData): PlayerDetail { private fun toPlayerDetail(player: co.caadiq.serverstatus.config.PlayerData): PlayerDetail {
// Essentials에서 닉네임 조회 (없으면 저장된 이름 사용)
val displayName = ServerStatusMod.playerDataStore.getDisplayName(player.uuid)
return PlayerDetail( return PlayerDetail(
uuid = player.uuid, uuid = player.uuid,
name = player.name, name = displayName,
firstJoin = player.firstJoin, firstJoin = player.firstJoin,
lastJoin = player.lastJoin, lastJoin = player.lastJoin,
lastLeave = player.lastLeave, lastLeave = player.lastLeave,
isOnline = player.isOnline, isOnline = player.isOnline,
currentSessionMs = player.getCurrentSessionMs(), currentSessionMs = player.getCurrentSessionMs(),
totalPlayTimeMs = player.totalPlayTimeMs totalPlayTimeMs = player.totalPlayTimeMs
) )
} }
/** /** GET /status - 서버 상태 */
* GET /status - 서버 상태
*/
private inner class StatusHandler : HttpHandler { private inner class StatusHandler : HttpHandler {
override fun handle(exchange: HttpExchange) { override fun handle(exchange: HttpExchange) {
if (exchange.requestMethod == "OPTIONS") { if (exchange.requestMethod == "OPTIONS") {
sendJsonResponse(exchange, "", 204) sendJsonResponse(exchange, "", 204)
return return
} }
try { try {
val status = ServerStatusMod.serverDataCollector.collectStatus() val status = ServerStatusMod.serverDataCollector.collectStatus()
val response = json.encodeToString(status) val response = json.encodeToString(status)
@ -106,19 +95,18 @@ class HttpApiServer(private val port: Int) {
} }
} }
} }
/** /** GET /players - 전체 플레이어 목록 */
* GET /players - 전체 플레이어 목록
*/
private inner class PlayersHandler : HttpHandler { private inner class PlayersHandler : HttpHandler {
override fun handle(exchange: HttpExchange) { override fun handle(exchange: HttpExchange) {
if (exchange.requestMethod == "OPTIONS") { if (exchange.requestMethod == "OPTIONS") {
sendJsonResponse(exchange, "", 204) sendJsonResponse(exchange, "", 204)
return return
} }
try { try {
val players = ServerStatusMod.playerDataStore.getAllPlayers().map { toPlayerDetail(it) } val players =
ServerStatusMod.playerDataStore.getAllPlayers().map { toPlayerDetail(it) }
val response = json.encodeToString(AllPlayersResponse(players)) val response = json.encodeToString(AllPlayersResponse(players))
sendJsonResponse(exchange, response) sendJsonResponse(exchange, response)
} catch (e: Exception) { } catch (e: Exception) {
@ -127,11 +115,10 @@ class HttpApiServer(private val port: Int) {
} }
} }
} }
/** /**
* GET /player/{uuid} - 특정 플레이어 정보 * GET /player/{uuid} - 특정 플레이어 정보 GET /player/{uuid}/stats - 플레이어 통계 GET
* GET /player/{uuid}/stats - 플레이어 통계 * /player/{uuid}/advancements - 플레이어 도전과제
* GET /player/{uuid}/advancements - 플레이어 도전과제
*/ */
private inner class PlayerHandler : HttpHandler { private inner class PlayerHandler : HttpHandler {
override fun handle(exchange: HttpExchange) { override fun handle(exchange: HttpExchange) {
@ -139,39 +126,49 @@ class HttpApiServer(private val port: Int) {
sendJsonResponse(exchange, "", 204) sendJsonResponse(exchange, "", 204)
return return
} }
try { try {
val path = exchange.requestURI.path val path = exchange.requestURI.path
val parts = path.removePrefix("/player/").split("/") val parts = path.removePrefix("/player/").split("/")
val uuid = parts.getOrNull(0)?.takeIf { it.isNotBlank() } val uuid = parts.getOrNull(0)?.takeIf { it.isNotBlank() }
val subPath = parts.getOrNull(1) val subPath = parts.getOrNull(1)
if (uuid == null) { if (uuid == null) {
sendJsonResponse(exchange, """{"error": "UUID required"}""", 400) sendJsonResponse(exchange, """{"error": "UUID required"}""", 400)
return return
} }
when (subPath) { when (subPath) {
"stats" -> { "stats" -> {
// 온라인이면 실시간 통계, 오프라인이면 저장된 통계 // 온라인이면 실시간 통계, 오프라인이면 저장된 통계
val isOnline = ServerStatusMod.playerDataStore.isPlayerOnline(uuid) val isOnline = ServerStatusMod.playerDataStore.isPlayerOnline(uuid)
val stats = if (isOnline) { val stats =
ServerStatusMod.playerStatsCollector.collectStats(uuid) if (isOnline) {
} else { ServerStatusMod.playerStatsCollector.collectStats(uuid)
ServerStatusMod.playerDataStore.getSavedStats(uuid) } else {
} ServerStatusMod.playerDataStore.getSavedStats(uuid)
}
if (stats != null) { if (stats != null) {
sendJsonResponse(exchange, json.encodeToString(stats)) sendJsonResponse(exchange, json.encodeToString(stats))
} else { } else {
sendJsonResponse(exchange, """{"error": "Player stats not available"}""", 404) sendJsonResponse(
exchange,
"""{"error": "Player stats not available"}""",
404
)
} }
} }
"advancements" -> { "advancements" -> {
val advancements = ServerStatusMod.playerStatsCollector.collectAdvancements(uuid) val advancements =
ServerStatusMod.playerStatsCollector.collectAdvancements(uuid)
if (advancements != null) { if (advancements != null) {
sendJsonResponse(exchange, json.encodeToString(advancements)) sendJsonResponse(exchange, json.encodeToString(advancements))
} else { } else {
sendJsonResponse(exchange, """{"error": "Player not online or not found"}""", 404) sendJsonResponse(
exchange,
"""{"error": "Player not online or not found"}""",
404
)
} }
} }
else -> { else -> {
@ -190,17 +187,15 @@ class HttpApiServer(private val port: Int) {
} }
} }
} }
/** /** GET /worlds - 월드 정보 */
* GET /worlds - 월드 정보
*/
private inner class WorldsHandler : HttpHandler { private inner class WorldsHandler : HttpHandler {
override fun handle(exchange: HttpExchange) { override fun handle(exchange: HttpExchange) {
if (exchange.requestMethod == "OPTIONS") { if (exchange.requestMethod == "OPTIONS") {
sendJsonResponse(exchange, "", 204) sendJsonResponse(exchange, "", 204)
return return
} }
try { try {
val worlds = ServerStatusMod.worldDataCollector.collectWorlds() val worlds = ServerStatusMod.worldDataCollector.collectWorlds()
val response = json.encodeToString(WorldsResponse(worlds)) val response = json.encodeToString(WorldsResponse(worlds))
@ -213,7 +208,4 @@ class HttpApiServer(private val port: Int) {
} }
} }
@Serializable @Serializable data class WorldsResponse(val worlds: List<WorldInfo>)
data class WorldsResponse(
val worlds: List<WorldInfo>
)