Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 37 additions & 9 deletions Notebook.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import com.hopskipnfall.*\n",
"import java.io.File\n",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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": {
Expand Down
53 changes: 51 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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"
Expand All @@ -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<Test> {
Expand All @@ -46,8 +67,21 @@ tasks.withType<Test> {
)
}

// 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/")
Expand All @@ -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") }
83 changes: 83 additions & 0 deletions src/main/proto/lag.proto
Original file line number Diff line number Diff line change
@@ -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;
}
Loading