Conversation
surf-stats plugin implementation
Added the GNU General Public License version 3 to the project.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1d37a06d3e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| PlayersTable.upsert { | ||
| it[uuid] = playerUuid | ||
| it[name] = playerName | ||
| it[updatedAt] = CurrentTimestamp |
There was a problem hiding this comment.
Set dataVersion in custom-stat player upsert
saveCustomStats inserts/updates players without assigning dataVersion, but PlayersTable declares dataversion as a required column with no default (PlayersTable.dataVersion). When a plugin writes custom stats for a player who is not already in players, the upsert path will try to insert a row missing dataversion, causing the write to fail instead of persisting the custom stats.
Useful? React with 👍 / 👎.
|
|
||
| // Cancel shared scope to kill any in-flight async coroutines (quit/world-save) | ||
| // before performing the synchronous shutdown save | ||
| pluginScope.cancel("Plugin disabling") |
There was a problem hiding this comment.
Avoid canceling delayed quit saves before shutdown flush
Canceling pluginScope at the start of shutdown can drop valid player saves: PlayerStatsListener schedules quit processing with a 1s delay, so a player disconnecting right before disable has an in-flight coroutine that gets canceled here, and that player is then absent from onlinePlayers for the synchronous shutdown save. In that timing window, the latest stats are never persisted.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR represents a complete architectural rewrite of the surf-stats plugin, migrating from a Java/Spring/Feign HTTP-based system to a Kotlin/Coroutines/R2DBC direct database implementation. The changes transform the plugin from a Bukkit implementation to a Paper-specific plugin with modern async processing.
Changes:
- Complete migration from Java to Kotlin with coroutines for async operations
- Replacement of Spring/Feign HTTP client architecture with direct R2DBC database access
- New modular structure: surf-stats-api (interfaces) → surf-stats-core (implementation) → surf-stats-paper (Paper plugin)
- Added comprehensive test coverage for core functionality
- Removed old surf-stats-bukkit module, replaced with surf-stats-paper module
Reviewed changes
Copilot reviewed 78 out of 130 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| surf-stats-paper/src/main/resources/database.yml | Database configuration with connection pooling settings |
| surf-stats-paper/src/main/resources/config.yml | Server identification configuration |
| surf-stats-paper/src/main/kotlin/.../SurfStatsPlugin.kt | Main plugin lifecycle with service initialization |
| surf-stats-paper/src/main/kotlin/.../listener/* | Event listeners for player quit, world save, and shutdown |
| surf-stats-core/src/main/kotlin/.../service/StatsFileServiceImpl.kt | JSON stats file parsing implementation |
| surf-stats-core/src/main/kotlin/.../database/* | R2DBC database tables and persistence layer |
| surf-stats-core/src/test/kotlin/* | Comprehensive unit tests for core functionality |
| surf-stats-api/src/main/kotlin/* | Public API interfaces and data models |
| gradle/wrapper/gradle-wrapper.properties | Gradle wrapper configuration |
| README.md | Comprehensive documentation of architecture and usage |
Files not reviewed (37)
- .idea/.gitignore: Language not supported
- .idea/compiler.xml: Language not supported
- .idea/discord.xml: Language not supported
- .idea/gradle.xml: Language not supported
- .idea/inspectionProfiles/Project_Default.xml: Language not supported
- .idea/intellij-javadocs-4.0.1.xml: Language not supported
- .idea/jarRepositories.xml: Language not supported
- .idea/jpa-buddy.xml: Language not supported
- .idea/jpa.xml: Language not supported
- .idea/jsLibraryMappings.xml: Language not supported
- .idea/kotlinc.xml: Language not supported
- .idea/material_theme_project_new.xml: Language not supported
- .idea/misc.xml: Language not supported
- .idea/modules.xml: Language not supported
- .idea/modules/surf-stats-api/IdeaProjects.surf-stats.surf-stats-api.main.iml: Language not supported
- .idea/modules/surf-stats-api/IdeaProjects.surf-stats.surf-stats-api.test.iml: Language not supported
- .idea/modules/surf-stats-api/surf-stats-api.surf-stats.surf-stats-api.main.iml: Language not supported
- .idea/modules/surf-stats-api/surf-stats-api.surf-stats.surf-stats-api.test.iml: Language not supported
- .idea/modules/surf-stats-api/surf-stats.surf-stats-api.main.iml: Language not supported
- .idea/modules/surf-stats-api/surf-stats.surf-stats-api.test.iml: Language not supported
- .idea/modules/surf-stats-bukkit/IdeaProjects.surf-stats.surf-stats-bukkit.main.iml: Language not supported
- .idea/modules/surf-stats-bukkit/IdeaProjects.surf-stats.surf-stats-bukkit.test.iml: Language not supported
- .idea/modules/surf-stats-bukkit/surf-stats-bukkit.surf-stats.surf-stats-bukkit.main.iml: Language not supported
- .idea/modules/surf-stats-bukkit/surf-stats-bukkit.surf-stats.surf-stats-bukkit.test.iml: Language not supported
- .idea/modules/surf-stats-bukkit/surf-stats.surf-stats-bukkit.iml: Language not supported
- .idea/modules/surf-stats-bukkit/surf-stats.surf-stats-bukkit.main.iml: Language not supported
- .idea/modules/surf-stats-bukkit/surf-stats.surf-stats-bukkit.test.iml: Language not supported
- .idea/modules/surf-stats-core/IdeaProjects.surf-stats.surf-stats-core.main.iml: Language not supported
- .idea/modules/surf-stats-core/IdeaProjects.surf-stats.surf-stats-core.test.iml: Language not supported
- .idea/modules/surf-stats-core/surf-stats-core.surf-stats.surf-stats-core.main.iml: Language not supported
- .idea/modules/surf-stats-core/surf-stats-core.surf-stats.surf-stats-core.test.iml: Language not supported
- .idea/modules/surf-stats-core/surf-stats.surf-stats-core.main.iml: Language not supported
- .idea/modules/surf-stats-core/surf-stats.surf-stats-core.test.iml: Language not supported
- .idea/prettier.xml: Language not supported
- .idea/surf-stats.iml: Language not supported
- .idea/uiDesigner.xml: Language not supported
- .idea/vcs.xml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // arrive in quick succession. We debounce by cancelling any pending job on each event, | ||
| // ensuring we only process stats once per save cycle. The 5s delay also gives Minecraft | ||
| // time to finish flushing all stats files to disk. | ||
| private var saveJob: Job? = null |
There was a problem hiding this comment.
The saveJob field is accessed from multiple threads (event handler thread and coroutine context) without synchronization. While Job operations are thread-safe, consider using @volatile annotation or AtomicReference to make the intent explicit and ensure proper memory visibility across threads.
| distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip | ||
| networkTimeout=10000 | ||
| validateDistributionUrl=true | ||
| distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-milestone-1-all.zip |
There was a problem hiding this comment.
The Gradle wrapper is configured to use a milestone (pre-release) version "9.5.0-milestone-1". Milestone versions are not stable releases and may contain bugs or breaking changes. Consider using a stable Gradle release version for production code to ensure reliability and compatibility.
| username: my_user | ||
| password: my_password |
There was a problem hiding this comment.
Hardcoded database credentials in a version-controlled configuration file pose a security risk. Consider using environment variables or a secure credential management system instead of committing actual credentials to the repository.
| runBlocking { | ||
| fileService.initialize(statsDirectory) | ||
| } | ||
|
|
||
| // Initialize database | ||
| saveResource("database.yml", false) | ||
| databaseApi = DatabaseApi.create(pluginPath = dataFolder.toPath()) | ||
| val databaseService = StatsDatabaseService(serverName, serverLabel) | ||
| runBlocking { | ||
| databaseService.registerServer() | ||
| } |
There was a problem hiding this comment.
The runBlocking calls in the onEnable lifecycle block the main server thread during plugin initialization. Consider using a launch with appropriate error handling instead, or document why blocking is necessary here. Blocking the main thread during server startup could cause performance issues.
|
|
||
| @EventHandler(priority = EventPriority.MONITOR) | ||
| fun onPluginDisable(event: PluginDisableEvent) { | ||
| if (event.plugin.name != "surf-stats-paper") return |
There was a problem hiding this comment.
The plugin name check uses a hardcoded string "surf-stats-paper" which could break if the plugin name changes. Consider using event.plugin == this@SurfStatsPlugin or storing the plugin reference to compare directly, making the code more robust to refactoring.
| surf-database = "1.3.0" | ||
| kotlinx-serialization = "1.10.0" | ||
| kotlinx-coroutines = "1.9.0" | ||
| junit = "5.11.3" | ||
|
|
||
| [libraries] | ||
| surf-data-api = { module = "dev.slne:surf-data-api", version.ref = "surf-data-api-version" } | ||
| surf-api = { module = "dev.slne.surf:surf-api-bukkit-api", version.ref = "surf-api-version" } | ||
| paper-api = { module = "io.papermc.paper:paper-api", version.ref = "paper-api-version" } | ||
| fast-util = { module = "it.unimi.dsi:fastutil", version.ref = "fast-util-version" } | ||
| maven-settings-plugin = { module = "net.linguica.gradle:maven-settings-plugin", version.ref = "maven-settings-plugin-version" } | ||
| shadow-plugin = { module = "com.github.johnrengelman:shadow", version.ref = "shadow-plugin-version" } | ||
|
|
||
| [plugins] | ||
| run-paper-plugin = { id = "xyz.jpenilla.run-paper", version = "2.2.2" } No newline at end of file | ||
| surf-database-r2dbc = { module = "dev.slne.surf:surf-database-r2dbc", version.ref = "surf-database" } |
There was a problem hiding this comment.
| surf-database = "1.3.0" | |
| kotlinx-serialization = "1.10.0" | |
| kotlinx-coroutines = "1.9.0" | |
| junit = "5.11.3" | |
| [libraries] | |
| surf-data-api = { module = "dev.slne:surf-data-api", version.ref = "surf-data-api-version" } | |
| surf-api = { module = "dev.slne.surf:surf-api-bukkit-api", version.ref = "surf-api-version" } | |
| paper-api = { module = "io.papermc.paper:paper-api", version.ref = "paper-api-version" } | |
| fast-util = { module = "it.unimi.dsi:fastutil", version.ref = "fast-util-version" } | |
| maven-settings-plugin = { module = "net.linguica.gradle:maven-settings-plugin", version.ref = "maven-settings-plugin-version" } | |
| shadow-plugin = { module = "com.github.johnrengelman:shadow", version.ref = "shadow-plugin-version" } | |
| [plugins] | |
| run-paper-plugin = { id = "xyz.jpenilla.run-paper", version = "2.2.2" } | |
| \ No newline at end of file | |
| surf-database-r2dbc = { module = "dev.slne.surf:surf-database-r2dbc", version.ref = "surf-database" } | |
| kotlinx-serialization = "1.10.0" | |
| kotlinx-coroutines = "1.9.0" | |
| junit = "5.11.3" | |
| [libraries] |
Kannst du direkt über surfapi machen in dem Module wo du es brauchst
https://github.com/SLNE-Development/surf-chat/blob/238a6b7ffe5a561cb373cef57fc6906550154003/surf-chat-fallback/build.gradle.kts#L6
| kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } | ||
| kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } | ||
| kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } |
There was a problem hiding this comment.
sollte alles in surf api drin sein
| kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } | |
| kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } | |
| kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } |
| kotlinx-serialization = "1.10.0" | ||
| kotlinx-coroutines = "1.9.0" |
There was a problem hiding this comment.
| kotlinx-serialization = "1.10.0" | |
| kotlinx-coroutines = "1.9.0" |
sollte in surf api drin sein
| /** | ||
| * The server name used for stats attribution. | ||
| */ | ||
| val serverName: String |
There was a problem hiding this comment.
hierfür kannst du surf core nutzen
| import dev.slne.surf.database.libs.org.jetbrains.exposed.v1.javatime.CurrentTimestamp | ||
| import dev.slne.surf.database.libs.org.jetbrains.exposed.v1.javatime.timestamp | ||
|
|
||
| object PlayersTable : Table("players") { |
There was a problem hiding this comment.
Kannst einen AuditableLongIdTable nutzen, da ist updated und createdAt direkt bei
| logLevel: DEBUG | ||
| credentials: | ||
| host: localhost | ||
| port: 3306 | ||
| database: my_database | ||
| username: my_user | ||
| password: my_password | ||
| pool: | ||
| sizing: | ||
| initialSize: 10 | ||
| minIdle: 0 | ||
| maxSize: 10 | ||
| timeouts: | ||
| maxAcquireTimeMillis: 10000 | ||
| maxCreateConnectionTimeMillis: 30000 | ||
| maxValidationTimeMillis: -1 | ||
| maxIdleTimeMillis: 60000 | ||
| maxLifeTimeMillis: 1800000 |
| server: | ||
| name: "my-server" | ||
| label: "My Server" |
There was a problem hiding this comment.
surf core nutzen, siehe oben
|
|
||
| surfPaperPluginApi { | ||
| mainClass("dev.slne.surf.stats.paper.SurfStatsPlugin") | ||
| authors.add("SLNE Dev Team") |
There was a problem hiding this comment.
| authors.add("SLNE Dev Team") | |
| authors.add("Jan") |
| } | ||
|
|
||
| surfPaperPluginApi { | ||
| mainClass("dev.slne.surf.stats.paper.SurfStatsPlugin") |
There was a problem hiding this comment.
| mainClass("dev.slne.surf.stats.paper.SurfStatsPlugin") | |
| mainClass("dev.slne.surf.stats.paper.PaperMain") |
siehe oben, direkt auch echten File und Klassennamen updaten
| tasks.processResources { | ||
| expand("version" to project.version) | ||
| } |
Co-author: red
No description provided.