Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ public class TerasologyEngine implements GameEngine {
* You don't want to add to this! If you need a module, make a module!
*/
private static final Set<String> LEGACY_ENGINE_MODULE_POLLUTERS = Set.of(
"org.terasology.subsystem.discordrpc.DiscordRPCSubSystem"
"org.terasology.subsystem.discordrpc.DiscordRPCSubSystem",
"org.terasology.subsystem.nakama.NakamaSubSystem"
);

private final List<Class<?>> classesOnClasspathsToAddToEngine = new ArrayList<>();
Expand Down
8 changes: 8 additions & 0 deletions facades/PC/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ plugins {
id("facade")
}

repositories {
maven {
name = "JitPack"
url = uri("https://jitpack.io")
}
}

// Grab all the common stuff like plugins to use, artifact repositories, code analysis config
apply(from = "$rootDir/config/gradle/publish.gradle")

Expand Down Expand Up @@ -64,6 +71,7 @@ dependencies {

implementation(project(":engine"))
implementation(project(":subsystems:DiscordRPC"))
implementation(project(":subsystems:Nakama"))
implementation("io.projectreactor:reactor-core:3.4.7")

// TODO: Consider whether we can move the CR dependency back here from the engine, where it is referenced from the main menu
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ private void populateSubsystems(TerasologyEngineBuilder builder) {
.add(new LwjglInput())
.add(new BindsSubsystem());
builder.add(new DiscordRPCSubSystem());
builder.add(new org.terasology.subsystem.nakama.NakamaSubSystem());
}
builder.add(new HibernationSubsystem());
}
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ dependencyResolutionManagement {
library("mockito-inline", "org.mockito:mockito-inline:3.12.4")
library("mockito-junit", "org.mockito", "mockito-junit-jupiter").versionRef(mockito)
// protobuf does not work as the others, see https://github.com/google/protobuf-gradle-plugin/issues/563
val protobuf = version("protobuf", "3.22.5")
val protobuf = version("protobuf", "4.28.2")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for protobuf usage in the codebase
echo "=== Finding .proto files ==="
fd -e proto

echo ""
echo "=== Checking protobuf API usage patterns that may have changed ==="
rg -n 'parseFrom|toByteArray|newBuilder|getDescriptor' --type java -C 2 | head -100

echo ""
echo "=== Checking for protobuf imports ==="
rg -n 'import com.google.protobuf' --type java | head -50

Repository: MovingBlocks/Terasology

Length of output: 15943


🌐 Web query:

What are the breaking changes between protobuf-java 3.x and 4.x?

💡 Result:

The primary breaking changes in protobuf-java 4.x (starting with v4.26.0 / protoc v26.0, released March 2024) compared to 3.x revolve around internal cleanups and support for new features like Protobuf Editions, breaking binary compatibility with older generated code. Key breaking changes: - Removal of GeneratedMessageV3 class and related APIs (e.g., PARSER field replaced by parser method; makeExtensionsImmutable and other deprecated methods removed). Generated messages now extend GeneratedMessage. - Breaking compatibility with pre-v3.22 generated code from 3.x; requires regeneration with protoc >=3.22 (ideally 3.25.x) for runtime compatibility via shims added in later 4.x releases (e.g., 4.27.4+, 4.33+ supports all 3.x gencode). - Removal of deprecated descriptor syntax APIs; use feature accessors like FieldDescriptor.hasPresence, EnumDescriptor.isClosed. - Runtime methods for v2 gencode compatibility removed. - Introduction of "poison pills" to enforce gencode/runtime version matching. - Refactoring of Java generator into separate packages. - Support for Editions (edition = "2023" etc.) changes generated code; old proto2/proto3 syntax still supported but may warn. Migration steps: - Regenerate .proto files with matching/newer protoc version (protoc 25.x+ for 3.x/4.x compatibility). - Update non-generated code referencing removed classes/methods (use Message interface instead of GeneratedMessageV3). - For libraries, use protoc 25.x for 3.x+4.x support; latest for 4.x+5.x. - protobuf-java 3.x supported until March 2027; 4.x until 2028. Binary wire format remains unchanged. Later 4.x versions (4.27+) added shims for gradual migration.

Citations:


Protobuf 4.x requires .proto files to be regenerated with matching protoc version.

Protobuf 4.x has breaking changes from 3.x, particularly removal of GeneratedMessageV3 and changes to descriptor APIs. The .proto files in engine/src/main/protobuf/ must be regenerated with protoc 4.x (or ≥3.22 for 4.x compatibility).

The codebase's heavy use of builder patterns (.newBuilder(), .addField(), .build()) is compatible with 4.x. However, verify that code using com.google.protobuf.Descriptors (in EntityDataJSONFormat.java, ProtobufPersistedDataSerializer.java) works correctly with the new API, then run full test suite to confirm compatibility before merging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@settings.gradle.kts` at line 29, The project bumped protobuf to 4.28.2 (val
protobuf = version("protobuf", "4.28.2")), so regenerate all .proto artifacts
under engine/src/main/protobuf/ with a matching protoc 4.x (or at least protoc
≥3.22 for 4.x compatibility), recompile the generated Java sources, and replace
any out-of-date generated files; then audit and adjust usages of
com.google.protobuf.Descriptors in EntityDataJSONFormat.java and
ProtobufPersistedDataSerializer.java for API changes introduced in protobuf 4.x
(fix method calls or imports as needed), rebuild and run the full test suite to
confirm compatibility before merging.

val slf4j = version("slf4j", "2.0.11")
library("slf4j-api", "org.slf4j", "slf4j-api").versionRef(slf4j)
library("slf4j-jul", "org.slf4j", "jul-to-slf4j").versionRef(slf4j)
Expand Down
45 changes: 45 additions & 0 deletions subsystems/Nakama/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Nakama subsystem - optional Bifrost integration
// Bridges Gestalt chat events to/from a Nakama chat channel

plugins {
java
`java-library`
id("terasology-common")
}

apply(from = "$rootDir/config/gradle/common.gradle")

configure<SourceSetContainer> {
main { java.destinationDirectory.set(layout.buildDirectory.dir("classes")) }
test { java.destinationDirectory.set(layout.buildDirectory.dir("testClasses")) }
}

repositories {
maven {
name = "JitPack"
url = uri("https://jitpack.io")
}
}

dependencies {
implementation(project(":engine"))

annotationProcessor(libs.gestalt.injectjava)

api("com.github.heroiclabs.nakama-java:nakama-java:2.5.3")
}

tasks.named<Test>("test") {
// Exclude integration tests by default — they need a running Nakama server
useJUnitPlatform {
if (!project.hasProperty("includeTags")) {
excludeTags("integration")
} else {
includeTags(project.property("includeTags") as String)
}
}
// Pass nakama.test.* system properties through to the test JVM
System.getProperties().entries
.filter { (it.key as String).startsWith("nakama.test.") }
.forEach { systemProperty(it.key as String, it.value) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2026 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

package org.terasology.subsystem.nakama;

import org.terasology.engine.config.flexible.AutoConfig;
import org.terasology.engine.config.flexible.Setting;

import static org.terasology.engine.config.flexible.SettingArgument.defaultValue;
import static org.terasology.engine.config.flexible.SettingArgument.name;
import static org.terasology.engine.config.flexible.SettingArgument.type;

/**
* Persistent configuration for the Nakama subsystem.
* Auto-discovered by the engine and saved to ~/.terasology/configs/nakama/NakamaAutoConfig.cfg
*/
public class NakamaAutoConfig extends AutoConfig {

public final Setting<Boolean> enabled = setting(
type(Boolean.class),
defaultValue(false),
name("Enable Nakama")
);

public final Setting<String> host = setting(
type(String.class),
defaultValue("localhost"),
name("Nakama Server Host")
);

public final Setting<Integer> grpcPort = setting(
type(Integer.class),
defaultValue(7349),
name("gRPC Port")
);

public final Setting<Integer> wsPort = setting(
type(Integer.class),
defaultValue(7350),
name("WebSocket Port")
);

public final Setting<String> channel = setting(
type(String.class),
defaultValue("bifrost.lobby"),
name("Chat Channel")
);

public final Setting<String> playerName = setting(
type(String.class),
defaultValue(""),
name("Player Name")
);

@Override
public String getName() {
return "Nakama Settings";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2026 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

package org.terasology.subsystem.nakama;

/**
* Configuration for the Nakama subsystem.
* Read from system properties for the POC.
*/
public class NakamaConfig {
private boolean enabled = false;
private String host = "localhost";
private int grpcPort = 7349;
private int wsPort = 7350;
private String channel = "bifrost.lobby";
private String playerName = "";

public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }

public String getHost() { return host; }
public void setHost(String host) { this.host = host; }

/** gRPC port for API calls (auth, account). Default: 7349. */
public int getGrpcPort() { return grpcPort; }
public void setGrpcPort(int grpcPort) { this.grpcPort = grpcPort; }

/** @deprecated Use {@link #getGrpcPort()}. Kept for backwards compatibility. */
@Deprecated
public int getPort() { return grpcPort; }
/** @deprecated Use {@link #setGrpcPort(int)}. */
@Deprecated
public void setPort(int port) { this.grpcPort = port; }

/** WebSocket port for realtime (chat, presence). Default: 7350. */
public int getWsPort() { return wsPort; }
public void setWsPort(int wsPort) { this.wsPort = wsPort; }

public String getChannel() { return channel; }
public void setChannel(String channel) { this.channel = channel; }

public String getPlayerName() { return playerName; }
public void setPlayerName(String playerName) { this.playerName = playerName; }

/**
* Load config from system properties (nakama.enabled, nakama.host, etc.)
* Falls back to defaults if not set.
*/
public static NakamaConfig fromSystemProperties() {
NakamaConfig config = new NakamaConfig();
config.setEnabled(Boolean.parseBoolean(System.getProperty("nakama.enabled", "false")));
config.setHost(System.getProperty("nakama.host", "localhost"));
config.setGrpcPort(Integer.parseInt(System.getProperty("nakama.grpcPort", "7349")));
config.setWsPort(Integer.parseInt(System.getProperty("nakama.wsPort", "7350")));
Comment on lines +53 to +54
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Integer.parseInt() can throw NumberFormatException for malformed system properties.

If nakama.grpcPort or nakama.wsPort contain non-numeric values, this will crash. Consider wrapping in try-catch or using a helper that returns the default on parse failure.

🛡️ Proposed defensive parsing
-        config.setGrpcPort(Integer.parseInt(System.getProperty("nakama.grpcPort", "7349")));
-        config.setWsPort(Integer.parseInt(System.getProperty("nakama.wsPort", "7350")));
+        config.setGrpcPort(parseIntOrDefault(System.getProperty("nakama.grpcPort"), 7349));
+        config.setWsPort(parseIntOrDefault(System.getProperty("nakama.wsPort"), 7350));

Add a helper method:

private static int parseIntOrDefault(String value, int defaultValue) {
    if (value == null || value.isEmpty()) {
        return defaultValue;
    }
    try {
        return Integer.parseInt(value);
    } catch (NumberFormatException e) {
        return defaultValue;
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@subsystems/Nakama/src/main/java/org/terasology/subsystem/nakama/NakamaConfig.java`
around lines 53 - 54, The current direct calls to Integer.parseInt in
NakamaConfig
(config.setGrpcPort(Integer.parseInt(System.getProperty("nakama.grpcPort",
"7349"))) and
config.setWsPort(Integer.parseInt(System.getProperty("nakama.wsPort", "7350"))))
can throw NumberFormatException; add a defensive helper (e.g., private static
int parseIntOrDefault(String value, int defaultValue)) in NakamaConfig that
returns the default when the value is null, empty, or unparsable, and replace
those Integer.parseInt calls to use
parseIntOrDefault(System.getProperty("nakama.grpcPort"), 7349) and
parseIntOrDefault(System.getProperty("nakama.wsPort"), 7350).

config.setChannel(System.getProperty("nakama.channel", "bifrost.lobby"));
config.setPlayerName(System.getProperty("nakama.playerName", ""));
return config;
}
Comment on lines +49 to +58
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether NakamaConfig is actually used anywhere in the codebase
echo "=== Searching for NakamaConfig usage ==="
rg -n 'NakamaConfig' --type java -g '!**/NakamaConfig.java' -g '!**/NakamaConfigTest.java'

Repository: MovingBlocks/Terasology

Length of output: 107


🏁 Script executed:

# Find and examine NakamaSubSystem.java to verify it only uses NakamaAutoConfig
find . -name "NakamaSubSystem.java" -type f

Repository: MovingBlocks/Terasology

Length of output: 153


🏁 Script executed:

# Check NakamaSubSystem's usage of config classes
rg -n "initialise\|NakamaAutoConfig\|NakamaConfig" subsystems/Nakama/src/main/java/org/terasology/subsystem/nakama/ --type java

Repository: MovingBlocks/Terasology

Length of output: 49


🏁 Script executed:

# Look at the actual NakamaConfig file to check for exception handling and comments
cat -n subsystems/Nakama/src/main/java/org/terasology/subsystem/nakama/NakamaConfig.java

Repository: MovingBlocks/Terasology

Length of output: 2875


🏁 Script executed:

cat -n subsystems/Nakama/src/main/java/org/terasology/subsystem/nakama/NakamaSubSystem.java

Repository: MovingBlocks/Terasology

Length of output: 10875


🏁 Script executed:

# Also check what files exist in the Nakama subsystem directory
ls -la subsystems/Nakama/src/main/java/org/terasology/subsystem/nakama/

Repository: MovingBlocks/Terasology

Length of output: 453


NakamaConfig is dead code — never called anywhere in the codebase.

The search returned zero matches for NakamaConfig usage outside its own file. NakamaSubSystem.initialise() exclusively uses NakamaAutoConfig (lines 71, 86, 90, 99–100) and never references NakamaConfig.fromSystemProperties(). This entire class should be removed.

Additionally, fromSystemProperties() lacks exception handling: Integer.parseInt() at lines 53–54 will throw NumberFormatException if nakama.grpcPort or nakama.wsPort contain non-numeric values. Wrap with try-catch or use a helper method to log and fall back to defaults.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@subsystems/Nakama/src/main/java/org/terasology/subsystem/nakama/NakamaConfig.java`
around lines 49 - 58, NakamaConfig is unused and its fromSystemProperties() also
risks NumberFormatException; remove the dead NakamaConfig class entirely (it’s
not referenced by NakamaSubSystem.initialise()) and delete
fromSystemProperties() to avoid dead code, or if you prefer to keep
functionality, update the fromSystemProperties() method to validate/parses ports
safely by using Integer.parseInt inside a try-catch (or a helper
parseWithDefault method) for nakama.grpcPort and nakama.wsPort, log parse
errors, and fall back to the default port values; reference: class NakamaConfig,
method fromSystemProperties(), and caller NakamaSubSystem.initialise().

}
Loading
Loading