From a9ea232e138c7f598ae1d9951a13d5cec2eeeacf Mon Sep 17 00:00:00 2001 From: yenon Date: Sun, 19 Nov 2023 06:59:26 +0100 Subject: [PATCH] Refactoring of fields and more race management. --- src/main/java/Main.kt | 12 ++++- src/main/java/RaceHolder.kt | 56 ++++++++++++++-------- src/main/java/RacePage.kt | 21 ++++++++ src/main/java/SearchPage.kt | 6 +-- src/main/java/TrackPage.kt | 14 ++++-- src/main/java/Velocidrone.kt | 93 +++++++++++++++++++++++++++++------- 6 files changed, 156 insertions(+), 46 deletions(-) diff --git a/src/main/java/Main.kt b/src/main/java/Main.kt index 46d4998..6a7b3f8 100644 --- a/src/main/java/Main.kt +++ b/src/main/java/Main.kt @@ -4,11 +4,13 @@ import io.ktor.server.engine.* import io.ktor.server.response.* import io.ktor.server.routing.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.nio.file.Files +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit import kotlin.io.path.Path - suspend fun main() { Settings.load() val path = Path("data.json") @@ -24,6 +26,14 @@ suspend fun main() { Files.readString(Path("token")) }) + Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate({ + runBlocking { + RaceHolder.races.forEach { + it.rescanAll() + } + } + }, 0, 5, TimeUnit.MINUTES) + val server = embeddedServer(CIO, port = 8080){ routing { get("/"){ diff --git a/src/main/java/RaceHolder.kt b/src/main/java/RaceHolder.kt index 70ea94a..44ce69b 100644 --- a/src/main/java/RaceHolder.kt +++ b/src/main/java/RaceHolder.kt @@ -1,3 +1,7 @@ +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -5,28 +9,34 @@ import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardOpenOption +@Serializable +data class Track( + val trackId: Long, + val endTime: LocalDateTime? = null, + var leaderboard: Velocidrone.Leaderboard = Velocidrone.Leaderboard( + true, "", "", + arrayOf() + ) +) + @Serializable data class RaceData(var name: String, var description: String) { - private val trackList = arrayListOf() - private val leaderboardMap = hashMapOf() + private val trackList = arrayListOf() private var totalScores = arrayListOf>() - fun addTrack(trackId: Long) { - trackList.add(trackId) + fun addTrack(track: Track) { + trackList.add(track) } - fun removeTrack(trackId: Long) { - trackList.remove(trackId) - leaderboardMap.remove(trackId) + fun removeTrack(track: Track) { + trackList.remove(track) } - private suspend fun rescanLeaderboard(trackId: Long, newLeaderboardCallback: (Velocidrone.Leaderboard) -> Unit) { - if (trackList.contains(trackId)) { - Velocidrone.getLeaderboardForId(trackId).getOrNull()?.let { newLeaderboard -> - if (leaderboardMap[trackId]?.equals(newLeaderboard) == false) { - leaderboardMap[trackId] = newLeaderboard - newLeaderboardCallback(newLeaderboard) - } + private suspend fun rescanLeaderboard(track: Track, newLeaderboardCallback: (Velocidrone.Leaderboard) -> Unit) { + Velocidrone.getLeaderboardForId(track.trackId).getOrNull()?.let { newLeaderboard -> + if (track.leaderboard != newLeaderboard) { + track.leaderboard = newLeaderboard + newLeaderboardCallback(newLeaderboard) } } } @@ -34,8 +44,10 @@ data class RaceData(var name: String, var description: String) { suspend fun rescanAll() { var changes = false trackList.forEach { - rescanLeaderboard(it) { - changes = true + if ((it.endTime?.toInstant(TimeZone.UTC)?.compareTo(Clock.System.now()) ?: -1) < 0) { + rescanLeaderboard(it) { + changes = true + } } } if (changes) { @@ -43,11 +55,11 @@ data class RaceData(var name: String, var description: String) { } } - fun calculateTotalScores() { + private fun calculateTotalScores() { val scoreMap = hashMapOf() - leaderboardMap.forEach { leaderboardEntry -> - leaderboardEntry.value.tracktimes.sortedBy { it.lap_time }.forEachIndexed { index, it -> - scoreMap[it.playername] = scoreMap.getOrDefault(it.playername, 0L) + 1 + index + trackList.forEach { track -> + track.leaderboard.trackTimes.sortedBy { it.lapTime }.forEachIndexed { index, it -> + scoreMap[it.playerName] = scoreMap.getOrDefault(it.playerName, 0L) + 1 + index } } synchronized(totalScores) { @@ -55,6 +67,10 @@ data class RaceData(var name: String, var description: String) { scoreMap.asIterable().sortedByDescending { it.value }.map { it.toPair() }.toCollection(totalScores) } } + + fun getTracks(): ArrayList { + return trackList + } } object RaceHolder { diff --git a/src/main/java/RacePage.kt b/src/main/java/RacePage.kt index 7794982..e628582 100644 --- a/src/main/java/RacePage.kt +++ b/src/main/java/RacePage.kt @@ -9,13 +9,33 @@ fun Routing.racePage() { get("/race/list") { raceOverviewPage(RaceHolder.races) } + + get("/races/{id}/show") { + + } + + get("/races/{raceId}/remove/{trackId}") { + val raceId = call.parameters["raceId"] + val trackId = call.parameters["trackId"]?.toLong() + + RaceHolder.races.find { it.name == raceId }?.let { raceData -> + val toBeRemovedTrack = raceData.getTracks().find { it.trackId == trackId } + toBeRemovedTrack?.let { + raceData.removeTrack(it) + } + } + call.respondRedirect("/races/$raceId/show") + } + get("/race/new") { newRacePage() } + post("/race/new") { val parameters = call.receiveParameters() val name = parameters["name"] val description = parameters["description"] + if (name == null || description == null) { newRacePage("Not all parameters given.", name ?: "", description ?: "") return@post @@ -24,6 +44,7 @@ fun Routing.racePage() { newRacePage("Race with the same name already exists.", name, description) return@post } + RaceHolder.races.add(RaceData(name, description)) call.respondRedirect("/race/list") } diff --git a/src/main/java/SearchPage.kt b/src/main/java/SearchPage.kt index 6c6b885..f203a9d 100644 --- a/src/main/java/SearchPage.kt +++ b/src/main/java/SearchPage.kt @@ -68,7 +68,7 @@ private suspend fun PipelineContext.searchResultPage(track } } } - trackResults.user_tracks.forEach { + trackResults.userTracks.forEach { tr { td { form("/track/" + it.id + "/add") { @@ -81,10 +81,10 @@ private suspend fun PipelineContext.searchResultPage(track } } td { - +it.track_name + +it.trackName } td { - +it.playername + +it.playerName } } } diff --git a/src/main/java/TrackPage.kt b/src/main/java/TrackPage.kt index 9785d0a..594e36d 100644 --- a/src/main/java/TrackPage.kt +++ b/src/main/java/TrackPage.kt @@ -3,6 +3,7 @@ import io.ktor.server.html.* import io.ktor.server.request.* import io.ktor.server.routing.* import io.ktor.util.pipeline.* +import kotlinx.datetime.toLocalDateTime import kotlinx.html.* import java.net.URLDecoder import java.net.URLEncoder @@ -22,11 +23,12 @@ fun Routing.trackPage() { } } post("/track/{id}/add") { + val endTime = call.receiveParameters()["endTime"]?.toLocalDateTime() call.parameters["id"]?.toLong()?.let { trackId -> val param = call.receiveParameters() param["race"]?.let { raceString -> val decoded = URLDecoder.decode(raceString, Charsets.UTF_8) - RaceHolder.races.find { it.name == decoded }?.addTrack(trackId) + RaceHolder.races.find { it.name == decoded }?.addTrack(Track(trackId, endTime)) } } } @@ -47,10 +49,10 @@ private suspend fun PipelineContext.trackPage(leaderboard td { +"Name" } } } - leaderboard.tracktimes.sortedBy { it.lap_time }.forEach { + leaderboard.trackTimes.sortedBy { it.lapTime }.forEach { tr { - td { +it.lap_time.toString() } - td { +it.playername } + td { +it.lapTime.toString() } + td { +it.playerName } } } } @@ -70,6 +72,10 @@ private suspend fun PipelineContext.addTrackPage(trackId: } } } + p { + +"Optional end time:" + dateTimeLocalInput(name = "endTime") + } submitInput() } } diff --git a/src/main/java/Velocidrone.kt b/src/main/java/Velocidrone.kt index f232705..f629d5b 100644 --- a/src/main/java/Velocidrone.kt +++ b/src/main/java/Velocidrone.kt @@ -2,6 +2,7 @@ import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.cio.* import io.ktor.client.request.* +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import java.net.URLEncoder @@ -13,38 +14,67 @@ import javax.crypto.spec.SecretKeySpec object Velocidrone{ private val key = "BatCaveGGevaCtaB".toByteArray(Charset.defaultCharset()) - fun decodeVeloBase64(input: ByteArray): Result { + private fun decodeVeloBase64(input: ByteArray): Result { val decoded = Base64.getDecoder().decode(input) val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding") cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES")) return Result.success(String(cipher.doFinal(decoded))) } - fun encodeVeloBase64(input: String): ByteArray { + private fun encodeVeloBase64(input: String): ByteArray { val encoded: ByteArray = input.toByteArray() val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding") cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES")) return Base64.getEncoder().encode(cipher.doFinal(encoded)) } - val client = HttpClient(CIO) + private val client = HttpClient(CIO) @Serializable data class Leaderboard( val success:Boolean, - val message_title:String, + @SerialName("message_title") + val messageTitle: String, val message:String, - val tracktimes:Array - ) + @SerialName("tracktimes") + val trackTimes: Array + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Leaderboard + + if (success != other.success) return false + if (messageTitle != other.messageTitle) return false + if (message != other.message) return false + if (!trackTimes.contentEquals(other.trackTimes)) return false + + return true + } + + override fun hashCode(): Int { + var result = success.hashCode() + result = 31 * result + messageTitle.hashCode() + result = 31 * result + message.hashCode() + result = 31 * result + trackTimes.contentHashCode() + return result + } + } @Serializable data class TrackTime( - val lap_time:Float, - val playername: String, - val model_id: Long, + @SerialName("lap_time") + val lapTime: Float, + @SerialName("playername") + val playerName: String, + @SerialName("model_id") + val modelId: Long, val country: String, - val sim_version: String, - val device_type: Long + @SerialName("sim_version") + val simVersion: String, + @SerialName("device_type") + val deviceType: Long ) suspend fun getLeaderboardForId(id:Long): Result { @@ -66,17 +96,44 @@ object Velocidrone{ data class TrackResults( val success:Boolean, val detail:String, - val user_tracks:Array - ) + @SerialName("user_tracks") + val userTracks: Array + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TrackResults + + if (success != other.success) return false + if (detail != other.detail) return false + if (!userTracks.contentEquals(other.userTracks)) return false + + return true + } + + override fun hashCode(): Int { + var result = success.hashCode() + result = 31 * result + detail.hashCode() + result = 31 * result + userTracks.contentHashCode() + return result + } + } + @Serializable data class Track( val id:Long, - val scenery_id:Long, - val track_name:String, - val track_type:String, - val playername:String, + @SerialName("scenery_id") + val sceneryId: Long, + @SerialName("track_name") + val trackName: String, + @SerialName("track_type") + val trackType: String, + @SerialName("playername") + val playerName: String, val rating:Float, - val total_ratings:Long, + @SerialName("total_ratings") + val totalRatings: Long, val date:String )