From 146e4bac0f4e390bace5f874bd9a83f2dab86eb0 Mon Sep 17 00:00:00 2001 From: Azalea <22280294+hykilpikonna@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:19:11 -0500 Subject: [PATCH] [+] Recruit lobby --- .../icu/samnyan/aqua/net/utils/PathProps.kt | 2 + .../sega/maimai2/worldslink/FutariLobby.kt | 92 +++++++++++++++++++ .../sega/maimai2/worldslink/FutariModels.kt | 39 ++++++++ 3 files changed, 133 insertions(+) create mode 100644 src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariLobby.kt create mode 100644 src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariModels.kt diff --git a/src/main/java/icu/samnyan/aqua/net/utils/PathProps.kt b/src/main/java/icu/samnyan/aqua/net/utils/PathProps.kt index 5318bf60..edc9e09e 100644 --- a/src/main/java/icu/samnyan/aqua/net/utils/PathProps.kt +++ b/src/main/java/icu/samnyan/aqua/net/utils/PathProps.kt @@ -15,12 +15,14 @@ class PathProps { var mai2Plays: String = "data/upload/mai2/plays" var mai2Portrait: String = "data/upload/mai2/portrait" var aquaNetPortrait: String = "data/upload/net/portrait" + var recruitLog: String = "data/futari/recruit.log" @PostConstruct fun init() { mai2Plays = mai2Plays.path().apply { toFile().mkdirs() }.toString() mai2Portrait = mai2Portrait.path().apply { toFile().mkdirs() }.toString() aquaNetPortrait = aquaNetPortrait.path().apply { toFile().mkdirs() }.toString() + recruitLog = recruitLog.path().apply { toFile().parentFile.mkdirs() }.toString() } } diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariLobby.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariLobby.kt new file mode 100644 index 00000000..b1fd8b29 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariLobby.kt @@ -0,0 +1,92 @@ + +package icu.samnyan.aqua.sega.maimai2.worldslink + +import ext.* +import icu.samnyan.aqua.net.utils.PathProps +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.io.BufferedWriter +import java.io.File +import java.io.FileOutputStream +import java.time.LocalDateTime +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + + +// KotlinX Serialization +@OptIn(ExperimentalSerializationApi::class) +private val KJson = Json { + ignoreUnknownKeys = true + isLenient = true + explicitNulls = false + coerceInputValues = true +} + +// Maximum time to live for a recruit record +const val MAX_TTL = 3 * 60 * 1000 + +data class RecruitRecord(val d: RecruitInfo, val time: Long = millis()) + +@RestController +@RequestMapping(path = ["/mai2-futari"]) +class FutariLobby(paths: PathProps) { + // + val recruits = mutableMapOf() + // Append writer + lateinit var writer: BufferedWriter + val mutex = ReentrantLock() + val log = logger() + + init { + paths.init() + writer = FileOutputStream(File(paths.recruitLog), true).bufferedWriter() + } + + fun log(data: String) = mutex.withLock { + log.info(data) + writer.write(data) + writer.newLine() + writer.flush() + } + + fun log(data: StartRecruit, msg: String) = + log("${LocalDateTime.now().isoDateTime()}: $msg: ${data.RecruitInfo.toJson()}") + + val StartRecruit.ip get() = RecruitInfo.MechaInfo.IpAddress + + @API("recruit/start") + fun startRecruit(@RB data: String) { + val d = parsing { KJson.decodeFromString(data) } + val exists = d.ip in recruits + recruits[d.ip] = RecruitRecord(d.RecruitInfo) + + if (!exists) log(d, "StartRecruit") + } + + @API("recruit/finish") + fun finishRecruit(@RB data: String) { + val d = parsing { KJson.decodeFromString(data) } + if (d.ip !in recruits) 400 - "Recruit not found" + recruits.remove(d.ip) + log(d, "EndRecruit") + } + + @API("recruit/list") + fun listRecruit(): String { + val time = millis() + recruits.filterValues { time - it.time > MAX_TTL }.keys.forEach { recruits.remove(it) } + return recruits.values.toList().joinToString("\n") { KJson.encodeToString(it.d) } + } +} + +fun main(args: Array) { + val json = """{"RecruitInfo":{"MechaInfo":{"IsJoin":true,"IpAddress":1820162433,"MusicID":11692,"Entrys":[true,false],"UserIDs":[281474976710657,281474976710657],"UserNames":["GUEST","GUEST"],"IconIDs":[1,1],"FumenDifs":[0,-1],"Rateing":[0,0],"ClassValue":[0,0],"MaxClassValue":[0,0],"UserType":[0,0]},"MusicID":11692,"GroupID":0,"EventModeID":false,"JoinNumber":1,"PartyStance":0,"_startTimeTicks":638725464510308001,"_recvTimeTicks":0}}""" + println(json.jsonMap().toJson()) + val data = KJson.decodeFromString(json) + println(json) + println(KJson.encodeToString(StartRecruit.serializer(), data)) + println(data) +} diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariModels.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariModels.kt new file mode 100644 index 00000000..9bb87fd9 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/worldslink/FutariModels.kt @@ -0,0 +1,39 @@ +@file:Suppress("PropertyName") + +package icu.samnyan.aqua.sega.maimai2.worldslink + +import ext.Bool +import kotlinx.serialization.Serializable + +@Serializable +data class MechaInfo( + val IsJoin: Bool, + val IpAddress: UInt, + val MusicID: Int, + val Entrys: List, + val UserIDs: List, + val UserNames: List, + val IconIDs: List, + val FumenDifs: List, + val Rateing: List, + val ClassValue: List, + val MaxClassValue: List, + val UserType: List +) + +@Serializable +data class RecruitInfo( + val MechaInfo: MechaInfo, + val MusicID: Int, + val GroupID: Int, + val EventModeID: Boolean, + val JoinNumber: Int, + val PartyStance: Int, + val _startTimeTicks: Long, + val _recvTimeTicks: Long +) + +@Serializable +data class StartRecruit( + val RecruitInfo: RecruitInfo, +)