feat: 서버 상태 모드 기능 개선
- Essentials 닉네임 동기화 기능 추가 - AuthCommand 명령어 추가 - 플레이어 데이터 저장소 개선
This commit is contained in:
parent
f2fb0ad324
commit
67f9cac26f
5 changed files with 337 additions and 96 deletions
|
|
@ -5,13 +5,19 @@ import com.beemer.essentials.nickname.NicknameDataStore
|
|||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
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.MutableComponent
|
||||
import net.minecraft.network.chat.Style
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
|
||||
object ChatUtils {
|
||||
private const val SECTION = '\u00a7'
|
||||
private val legacyPattern = Regex("&([0-9A-FK-ORa-fk-or])")
|
||||
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 fun translateChatColors(text: String?): String {
|
||||
|
|
@ -56,6 +62,42 @@ object ChatUtils {
|
|||
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) {
|
||||
val key = if (firstTime) "first-time-join" else "join"
|
||||
val template = ChatConfig.getFormat(key) ?: defaultJoinTemplate(firstTime)
|
||||
|
|
@ -72,7 +114,10 @@ object ChatUtils {
|
|||
fun broadcastChat(player: ServerPlayer, rawMessage: String) {
|
||||
val template = ChatConfig.getFormat("chat") ?: "%name% &7&l: &f%message%"
|
||||
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 =
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
package co.caadiq.serverstatus
|
||||
|
||||
import co.caadiq.serverstatus.command.AuthCommand
|
||||
import co.caadiq.serverstatus.config.ModConfig
|
||||
import co.caadiq.serverstatus.config.PlayerDataStore
|
||||
import co.caadiq.serverstatus.data.PlayerStatsCollector
|
||||
import co.caadiq.serverstatus.data.PlayerTracker
|
||||
import co.caadiq.serverstatus.data.ServerDataCollector
|
||||
import co.caadiq.serverstatus.data.WorldDataCollector
|
||||
import co.caadiq.serverstatus.data.PlayerStatsCollector
|
||||
import co.caadiq.serverstatus.network.HttpApiServer
|
||||
import net.neoforged.bus.api.IEventBus
|
||||
import net.neoforged.bus.api.SubscribeEvent
|
||||
import net.neoforged.fml.ModContainer
|
||||
import net.neoforged.fml.common.Mod
|
||||
import net.neoforged.fml.event.lifecycle.FMLDedicatedServerSetupEvent
|
||||
import net.neoforged.neoforge.common.NeoForge
|
||||
import net.neoforged.neoforge.event.RegisterCommandsEvent
|
||||
import org.apache.logging.log4j.LogManager
|
||||
|
||||
/**
|
||||
* 메인 모드 클래스
|
||||
* 서버 상태 정보를 HTTP API로 제공
|
||||
*/
|
||||
/** 메인 모드 클래스 서버 상태 정보를 HTTP API로 제공 */
|
||||
@Mod(ServerStatusMod.MOD_ID)
|
||||
class ServerStatusMod(modBus: IEventBus, container: ModContainer) {
|
||||
|
||||
|
||||
companion object {
|
||||
const val MOD_ID = "serverstatus"
|
||||
val LOGGER = LogManager.getLogger(MOD_ID)
|
||||
|
||||
|
||||
lateinit var config: ModConfig
|
||||
private set
|
||||
lateinit var playerDataStore: PlayerDataStore
|
||||
|
|
@ -38,38 +38,43 @@ class ServerStatusMod(modBus: IEventBus, container: ModContainer) {
|
|||
lateinit var httpApiServer: HttpApiServer
|
||||
private set
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
LOGGER.info("[$MOD_ID] 모드 초기화 중...")
|
||||
|
||||
|
||||
// 설정 초기화
|
||||
config = ModConfig.load()
|
||||
playerDataStore = PlayerDataStore.load()
|
||||
|
||||
|
||||
// 서버 전용 이벤트 등록
|
||||
modBus.addListener(::onServerSetup)
|
||||
|
||||
// 플레이어 이벤트 리스너 등록
|
||||
|
||||
// 플레이어 이벤트 및 명령어 리스너 등록
|
||||
NeoForge.EVENT_BUS.register(PlayerTracker)
|
||||
|
||||
NeoForge.EVENT_BUS.register(this)
|
||||
|
||||
LOGGER.info("[$MOD_ID] 모드 초기화 완료")
|
||||
}
|
||||
|
||||
/**
|
||||
* 전용 서버 설정 이벤트
|
||||
*/
|
||||
|
||||
/** 명령어 등록 이벤트 */
|
||||
@SubscribeEvent
|
||||
fun onRegisterCommands(event: RegisterCommandsEvent) {
|
||||
AuthCommand.register(event.dispatcher)
|
||||
}
|
||||
|
||||
/** 전용 서버 설정 이벤트 */
|
||||
private fun onServerSetup(event: FMLDedicatedServerSetupEvent) {
|
||||
LOGGER.info("[$MOD_ID] 서버 설정 중...")
|
||||
|
||||
|
||||
// 데이터 수집기 초기화
|
||||
serverDataCollector = ServerDataCollector()
|
||||
worldDataCollector = WorldDataCollector()
|
||||
playerStatsCollector = PlayerStatsCollector()
|
||||
|
||||
|
||||
// HTTP API 서버 시작
|
||||
httpApiServer = HttpApiServer(config.httpPort)
|
||||
httpApiServer.start()
|
||||
|
||||
|
||||
LOGGER.info("[$MOD_ID] HTTP API 서버 시작됨 (포트: ${config.httpPort})")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -62,6 +62,24 @@ data class PlayerDataStore(
|
|||
private val configDir = FMLPaths.CONFIGDIR.get().resolve(ServerStatusMod.MOD_ID)
|
||||
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}명")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 저장
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,101 +1,90 @@
|
|||
package co.caadiq.serverstatus.network
|
||||
|
||||
import co.caadiq.serverstatus.ServerStatusMod
|
||||
import co.caadiq.serverstatus.data.AdvancementsInfo
|
||||
import co.caadiq.serverstatus.data.PlayerStats
|
||||
import co.caadiq.serverstatus.data.WorldInfo
|
||||
import com.sun.net.httpserver.HttpExchange
|
||||
import com.sun.net.httpserver.HttpHandler
|
||||
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.nio.charset.StandardCharsets
|
||||
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) {
|
||||
|
||||
private val json = Json {
|
||||
|
||||
private val json = Json {
|
||||
prettyPrint = false
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
|
||||
private var server: HttpServer? = null
|
||||
|
||||
/**
|
||||
* 서버 시작
|
||||
*/
|
||||
|
||||
/** 서버 시작 */
|
||||
fun start() {
|
||||
try {
|
||||
server = HttpServer.create(InetSocketAddress(port), 0)
|
||||
server?.executor = Executors.newFixedThreadPool(4)
|
||||
|
||||
|
||||
// 엔드포인트 등록
|
||||
server?.createContext("/status", StatusHandler())
|
||||
server?.createContext("/players", PlayersHandler())
|
||||
server?.createContext("/player", PlayerHandler())
|
||||
server?.createContext("/worlds", WorldsHandler())
|
||||
|
||||
|
||||
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) {
|
||||
ServerStatusMod.LOGGER.error("[${ServerStatusMod.MOD_ID}] HTTP 서버 시작 실패: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 서버 종료
|
||||
*/
|
||||
|
||||
/** 서버 종료 */
|
||||
fun stop() {
|
||||
server?.stop(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS 헤더 추가 및 응답 전송
|
||||
*/
|
||||
|
||||
/** CORS 헤더 추가 및 응답 전송 */
|
||||
private fun sendJsonResponse(exchange: HttpExchange, response: String, statusCode: Int = 200) {
|
||||
exchange.responseHeaders.add("Content-Type", "application/json; charset=utf-8")
|
||||
exchange.responseHeaders.add("Access-Control-Allow-Origin", "*")
|
||||
exchange.responseHeaders.add("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
exchange.responseHeaders.add("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
|
||||
val bytes = response.toByteArray(StandardCharsets.UTF_8)
|
||||
exchange.sendResponseHeaders(statusCode, bytes.size.toLong())
|
||||
exchange.responseBody.use { it.write(bytes) }
|
||||
}
|
||||
|
||||
/**
|
||||
* PlayerData를 PlayerDetail로 변환
|
||||
*/
|
||||
|
||||
/** PlayerData를 PlayerDetail로 변환 (Essentials 닉네임 우선 적용) */
|
||||
private fun toPlayerDetail(player: co.caadiq.serverstatus.config.PlayerData): PlayerDetail {
|
||||
// Essentials에서 닉네임 조회 (없으면 저장된 이름 사용)
|
||||
val displayName = ServerStatusMod.playerDataStore.getDisplayName(player.uuid)
|
||||
return PlayerDetail(
|
||||
uuid = player.uuid,
|
||||
name = player.name,
|
||||
firstJoin = player.firstJoin,
|
||||
lastJoin = player.lastJoin,
|
||||
lastLeave = player.lastLeave,
|
||||
isOnline = player.isOnline,
|
||||
currentSessionMs = player.getCurrentSessionMs(),
|
||||
totalPlayTimeMs = player.totalPlayTimeMs
|
||||
uuid = player.uuid,
|
||||
name = displayName,
|
||||
firstJoin = player.firstJoin,
|
||||
lastJoin = player.lastJoin,
|
||||
lastLeave = player.lastLeave,
|
||||
isOnline = player.isOnline,
|
||||
currentSessionMs = player.getCurrentSessionMs(),
|
||||
totalPlayTimeMs = player.totalPlayTimeMs
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /status - 서버 상태
|
||||
*/
|
||||
|
||||
/** GET /status - 서버 상태 */
|
||||
private inner class StatusHandler : HttpHandler {
|
||||
override fun handle(exchange: HttpExchange) {
|
||||
if (exchange.requestMethod == "OPTIONS") {
|
||||
sendJsonResponse(exchange, "", 204)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
val status = ServerStatusMod.serverDataCollector.collectStatus()
|
||||
val response = json.encodeToString(status)
|
||||
|
|
@ -106,19 +95,18 @@ class HttpApiServer(private val port: Int) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /players - 전체 플레이어 목록
|
||||
*/
|
||||
|
||||
/** GET /players - 전체 플레이어 목록 */
|
||||
private inner class PlayersHandler : HttpHandler {
|
||||
override fun handle(exchange: HttpExchange) {
|
||||
if (exchange.requestMethod == "OPTIONS") {
|
||||
sendJsonResponse(exchange, "", 204)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
val players = ServerStatusMod.playerDataStore.getAllPlayers().map { toPlayerDetail(it) }
|
||||
val players =
|
||||
ServerStatusMod.playerDataStore.getAllPlayers().map { toPlayerDetail(it) }
|
||||
val response = json.encodeToString(AllPlayersResponse(players))
|
||||
sendJsonResponse(exchange, response)
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -127,11 +115,10 @@ class HttpApiServer(private val port: Int) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* GET /player/{uuid} - 특정 플레이어 정보
|
||||
* GET /player/{uuid}/stats - 플레이어 통계
|
||||
* GET /player/{uuid}/advancements - 플레이어 도전과제
|
||||
* GET /player/{uuid} - 특정 플레이어 정보 GET /player/{uuid}/stats - 플레이어 통계 GET
|
||||
* /player/{uuid}/advancements - 플레이어 도전과제
|
||||
*/
|
||||
private inner class PlayerHandler : HttpHandler {
|
||||
override fun handle(exchange: HttpExchange) {
|
||||
|
|
@ -139,39 +126,49 @@ class HttpApiServer(private val port: Int) {
|
|||
sendJsonResponse(exchange, "", 204)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
val path = exchange.requestURI.path
|
||||
val parts = path.removePrefix("/player/").split("/")
|
||||
val uuid = parts.getOrNull(0)?.takeIf { it.isNotBlank() }
|
||||
val subPath = parts.getOrNull(1)
|
||||
|
||||
|
||||
if (uuid == null) {
|
||||
sendJsonResponse(exchange, """{"error": "UUID required"}""", 400)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
when (subPath) {
|
||||
"stats" -> {
|
||||
// 온라인이면 실시간 통계, 오프라인이면 저장된 통계
|
||||
val isOnline = ServerStatusMod.playerDataStore.isPlayerOnline(uuid)
|
||||
val stats = if (isOnline) {
|
||||
ServerStatusMod.playerStatsCollector.collectStats(uuid)
|
||||
} else {
|
||||
ServerStatusMod.playerDataStore.getSavedStats(uuid)
|
||||
}
|
||||
val stats =
|
||||
if (isOnline) {
|
||||
ServerStatusMod.playerStatsCollector.collectStats(uuid)
|
||||
} else {
|
||||
ServerStatusMod.playerDataStore.getSavedStats(uuid)
|
||||
}
|
||||
if (stats != null) {
|
||||
sendJsonResponse(exchange, json.encodeToString(stats))
|
||||
} else {
|
||||
sendJsonResponse(exchange, """{"error": "Player stats not available"}""", 404)
|
||||
sendJsonResponse(
|
||||
exchange,
|
||||
"""{"error": "Player stats not available"}""",
|
||||
404
|
||||
)
|
||||
}
|
||||
}
|
||||
"advancements" -> {
|
||||
val advancements = ServerStatusMod.playerStatsCollector.collectAdvancements(uuid)
|
||||
val advancements =
|
||||
ServerStatusMod.playerStatsCollector.collectAdvancements(uuid)
|
||||
if (advancements != null) {
|
||||
sendJsonResponse(exchange, json.encodeToString(advancements))
|
||||
} else {
|
||||
sendJsonResponse(exchange, """{"error": "Player not online or not found"}""", 404)
|
||||
sendJsonResponse(
|
||||
exchange,
|
||||
"""{"error": "Player not online or not found"}""",
|
||||
404
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
|
|
@ -190,17 +187,15 @@ class HttpApiServer(private val port: Int) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /worlds - 월드 정보
|
||||
*/
|
||||
|
||||
/** GET /worlds - 월드 정보 */
|
||||
private inner class WorldsHandler : HttpHandler {
|
||||
override fun handle(exchange: HttpExchange) {
|
||||
if (exchange.requestMethod == "OPTIONS") {
|
||||
sendJsonResponse(exchange, "", 204)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
val worlds = ServerStatusMod.worldDataCollector.collectWorlds()
|
||||
val response = json.encodeToString(WorldsResponse(worlds))
|
||||
|
|
@ -213,7 +208,4 @@ class HttpApiServer(private val port: Int) {
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class WorldsResponse(
|
||||
val worlds: List<WorldInfo>
|
||||
)
|
||||
@Serializable data class WorldsResponse(val worlds: List<WorldInfo>)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue