diff --git a/Notebook.ipynb b/Notebook.ipynb index 64e0991..4be318a 100644 --- a/Notebook.ipynb +++ b/Notebook.ipynb @@ -2,7 +2,9 @@ "cells": [ { "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ "import com.hopskipnfall.*\n", "import java.io.File\n", @@ -48,13 +50,13 @@ "}\n", "server.lagstat(now)\n", "\n" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ "%use kandy, dataframe\n", "import com.github.nwillc.ksvg.RenderMode\n", @@ -75,13 +77,13 @@ " color(\"Client\")\n", " }\n", "}" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ "%use kandy, dataframe\n", "\n", @@ -94,10 +96,36 @@ " }\n", "\n", " layout { title = \"Objective lag experienced by clients\" }\n", - "}\n" - ], + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Optional: Load in a `GameLag` record:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-14T13:59:42.613501Z", + "start_time": "2025-08-14T13:59:42.485496Z" + } + }, "outputs": [], - "execution_count": null + "source": [ + "import java.io.FileInputStream\n", + "import org.emulinker.proto.GameLog\n", + "\n", + "FileInputStream(\"gamelog_3pall1f.bin\").use { fileInputStream ->\n", + " val log = GameLog.parseFrom(fileInputStream.readBytes())\n", + " println(\"Number of events: ${log.eventsList.size}\")\n", + " log.eventsList.take(20).map { it.eventTypeCase }\n", + "}" + ] } ], "metadata": { diff --git a/build.gradle.kts b/build.gradle.kts index 8c9cb76..d90075c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,9 @@ +import build.buf.gradle.BUF_BINARY_CONFIGURATION_NAME +import com.google.protobuf.gradle.id + plugins { + id("com.google.protobuf") version "0.9.5" + id("build.buf") version "0.10.2" id("com.diffplug.spotless") version "6.25.0" application @@ -20,6 +25,10 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlin-statistics-jvm:0.3.0") implementation("com.github.nwillc.ksvg:ksvg:master-SNAPSHOT") + + implementation("com.google.protobuf:protobuf-kotlin:4.31.1") + implementation("com.google.protobuf:protobuf-java:4.31.1") + implementation("com.google.protobuf:protobuf-java-util:4.31.1") } group = "com.hopskipnfall" @@ -30,10 +39,22 @@ version = "0.12.0" kotlin { jvmToolchain(17) } +tasks.processResources { + // Fails to compile without this. + // https://github.com/google/protobuf-gradle-plugin/issues/522#issuecomment-1195266995 + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + sourceSets { - main { kotlin.srcDir("src/main/java") } + main { + proto.srcDir("src/main/proto") + kotlin.srcDir("src/main/java") + } - test { kotlin.srcDir("src/test/java") } + test { + proto.srcDir("src/main/proto") + kotlin.srcDir("src/test/java") + } } tasks.withType { @@ -46,8 +67,21 @@ tasks.withType { ) } +// Disable formatting via buf plugin directly. We just need it for the binary. +buf { enforceFormat = false } + +tasks.named("bufLint") { enabled = false } + // Formatting/linting. spotless { + protobuf { + buf("1.46.0") + .pathToExe( + configurations.getByName(BUF_BINARY_CONFIGURATION_NAME).getSingleFile().getAbsolutePath() + ) + target("src/**/*.proto") + } + kotlin { target("**/*.kt", "**/*.kts") targetExclude("build/", ".git/", ".idea/", ".mvn", "src/main/java-templates/") @@ -61,4 +95,19 @@ spotless { } } +protobuf { + protoc { artifact = "com.google.protobuf:protoc:4.31.1" } + + generateProtoTasks { + ofSourceSet("main").forEach { + it.plugins { + // Generates Kotlin DSL builders. + id("kotlin") {} + } + } + } +} + +tasks.named("compileKotlin") { dependsOn(":generateProto") } + application { mainClass.set("com.hopskipnfall.MainKt") } diff --git a/src/main/proto/lag.proto b/src/main/proto/lag.proto new file mode 100644 index 0000000..124fa4b --- /dev/null +++ b/src/main/proto/lag.proto @@ -0,0 +1,83 @@ +// This file should be kept up-to-date with the copy in the ELK repo: +// https://github.com/hopskipnfall/EmuLinker-K/blob/master/emulinker/src/main/proto/lag.proto +// +// How to use protos: https://protobuf.dev/programming-guides/editions +edition = "2023"; + +package org.emulinker.proto; + +import "google/protobuf/timestamp.proto"; + +option java_multiple_files = true; + +enum PlayerNumber { + // Note: In some languages having multiple enum values with the same + // name causes issues so it's good to prefix with the name. + PLAYER_NUMBER_UNKNOWN = 0; + ONE = 1; + TWO = 2; + THREE = 3; + FOUR = 4; +} + +message Event { + // Number of nanoseconds from an arbitrary point of time in the past. + // This can be used to check elapsed time between events for a single + // game, but not necessarily between events between two games. + int64 timestamp_ns = 1; + + message GameStart { + message PlayerDetails { + PlayerNumber player_number = 1; + + // User's average ping when they joined the server. + double ping_ms = 2; + + int32 frame_delay = 3; + } + + repeated PlayerDetails players = 1; + + google.protobuf.Timestamp timestamp = 2; + } + + message ReceivedGameData { + PlayerNumber received_from = 1; + } + + // Server has collected data from all parties for the frame and will + // now combine and broadcast them back out to each player. + message FanOut {} + + // It might be useful to know what the /lagstat command would say + // as a point of comparison. + message LagstatSummary { + // The duration over which lag is being measured. + int32 window_duration_ms = 1; + + // The measured drift over the window + double game_lag_ms = 2; + + message PlayerAttributedLag { + PlayerNumber player = 1; + + // How much of the drift can be reliably attributed to the + // player. + double attributed_lag_ms = 2; + } + + repeated PlayerAttributedLag player_attributed_lags = 3; + } + + oneof event_type { + GameStart game_start = 2; + ReceivedGameData received_game_data = 3; + FanOut fan_out = 4; + LagstatSummary lagstat_summary = 5; + } +} + +// All metadata stored about the game. +message GameLog { + repeated Event events = 1; +}