Adding client-side and server-side interactions. Also adds purpur/paper support

This commit is contained in:
Dmitri Shimanski
2026-07-20 05:41:57 +03:00
parent fbe4d786ca
commit ce93e99962
71 changed files with 5068 additions and 279 deletions
+117
View File
@@ -0,0 +1,117 @@
plugins {
id 'dev.architectury.loom'
id 'maven-publish'
}
version = rootProject.mod_version
group = rootProject.maven_group
base {
archivesName = "${rootProject.archives_base_name}-1.21.1"
}
repositories {
mavenCentral()
maven { url 'https://maven.shedaniel.me/' }
maven { url 'https://maven.terraformersmc.com/releases/' }
maven { url 'https://api.modrinth.com/maven/' }
}
loom {
splitEnvironmentSourceSets()
mods {
go_minecraft_bridge {
sourceSet sourceSets.main
sourceSet sourceSets.client
}
}
runs {
configureEach {
vmArg '--enable-native-access=ALL-UNNAMED'
}
}
}
def sharedRoot = file('../..')
def prepareMainSources = tasks.register('prepareMainSources', Sync) {
from(sharedRoot.toPath().resolve('src/main/java')) {
exclude 'dev/yawaflua/gominecraftbridge/compat/MinecraftVersionAdapter.java'
exclude 'dev/yawaflua/gominecraftbridge/network/AdminRequestPayload.java'
exclude 'dev/yawaflua/gominecraftbridge/network/AdminResponsePayload.java'
exclude 'dev/yawaflua/gominecraftbridge/network/BridgeAdminNetworking.java'
}
from(sharedRoot.toPath().resolve('src/main/generated'))
from(file('src/main/java'))
into(layout.buildDirectory.dir('version-sources/main'))
}
sourceSets {
main {
java.setSrcDirs([layout.buildDirectory.dir('version-sources/main')])
resources.setSrcDirs([file('src/main/resources')])
}
client {
java.setSrcDirs([sharedRoot.toPath().resolve('src/client/java')])
}
test {
java.setSrcDirs([sharedRoot.toPath().resolve('src/test/java'), file('src/test/java')])
}
}
dependencies {
minecraft 'com.mojang:minecraft:1.21.1'
mappings loom.officialMojangMappings()
modImplementation 'net.fabricmc:fabric-loader:0.16.14'
modImplementation 'net.fabricmc.fabric-api:fabric-api:0.116.14+1.21.1'
implementation 'com.google.code.gson:gson:2.13.2'
implementation include('com.google.flatbuffers:flatbuffers-java:25.2.10')
implementation 'net.java.dev.jna:jna:5.18.1'
modClientImplementation 'me.shedaniel.cloth:cloth-config-fabric:15.0.140'
modClientImplementation 'maven.modrinth:modmenu:11.0.3'
testImplementation platform('org.junit:junit-bom:5.13.4')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
processResources {
inputs.property 'version', project.version
filesMatching('fabric.mod.json') {
expand 'version': project.version
}
}
tasks.withType(JavaCompile).configureEach {
options.release = 21
}
tasks.named('compileJava') {
dependsOn prepareMainSources
}
tasks.withType(Test).configureEach {
useJUnitPlatform()
jvmArgs '--enable-native-access=ALL-UNNAMED'
}
java {
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
tasks.named('sourcesJar') {
dependsOn prepareMainSources
}
publishing {
publications {
create('mavenJava', MavenPublication) {
from components.java
}
}
}
@@ -0,0 +1,29 @@
package dev.yawaflua.gominecraftbridge.compat;
import net.minecraft.SharedConstants;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.Level;
/** Minecraft 1.21.1 implementation of the shared version adapter. */
public final class MinecraftVersionAdapter {
private MinecraftVersionAdapter() {
}
public static String gameVersion() {
return SharedConstants.getCurrentVersion().getName();
}
public static String dimension(Level level) {
return level.dimension().location().toString();
}
public static long dayTime(ServerLevel level) {
return level.getDayTime();
}
public static boolean isOperator(MinecraftServer server, ServerPlayer player) {
return server.getPlayerList().isOp(player.getGameProfile());
}
}
@@ -0,0 +1,29 @@
package dev.yawaflua.gominecraftbridge.network;
import dev.yawaflua.gominecraftbridge.GoMinecraftBridgeMod;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record AdminRequestPayload(String action, String pluginId) implements CustomPacketPayload {
public static final Type<AdminRequestPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(GoMinecraftBridgeMod.MOD_ID, "admin_request")
);
public static final StreamCodec<RegistryFriendlyByteBuf, AdminRequestPayload> CODEC =
CustomPacketPayload.codec(AdminRequestPayload::write, AdminRequestPayload::new);
public AdminRequestPayload(RegistryFriendlyByteBuf buffer) {
this(buffer.readUtf(32), buffer.readUtf(64));
}
private void write(RegistryFriendlyByteBuf buffer) {
buffer.writeUtf(this.action, 32);
buffer.writeUtf(this.pluginId == null ? "" : this.pluginId, 64);
}
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
@@ -0,0 +1,31 @@
package dev.yawaflua.gominecraftbridge.network;
import dev.yawaflua.gominecraftbridge.GoMinecraftBridgeMod;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record AdminResponsePayload(String json) implements CustomPacketPayload {
// 1.21.1 rejects a custom payload at 1 MiB. Leave room for the channel,
// packet framing, VarInts and UTF-8 expansion.
public static final int MAX_JSON_CHARS = 900 * 1024;
public static final Type<AdminResponsePayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(GoMinecraftBridgeMod.MOD_ID, "admin_response")
);
public static final StreamCodec<RegistryFriendlyByteBuf, AdminResponsePayload> CODEC =
CustomPacketPayload.codec(AdminResponsePayload::write, AdminResponsePayload::new);
public AdminResponsePayload(RegistryFriendlyByteBuf buffer) {
this(buffer.readUtf(MAX_JSON_CHARS));
}
private void write(RegistryFriendlyByteBuf buffer) {
buffer.writeUtf(this.json, MAX_JSON_CHARS);
}
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
@@ -0,0 +1,98 @@
package dev.yawaflua.gominecraftbridge.network;
import dev.yawaflua.gominecraftbridge.compat.MinecraftVersionAdapter;
import dev.yawaflua.gominecraftbridge.host.GoPluginManager;
import dev.yawaflua.gominecraftbridge.management.BridgeManagementSnapshot;
import dev.yawaflua.gominecraftbridge.management.ManagedPluginSnapshot;
import dev.yawaflua.gominecraftbridge.management.ReloadResult;
import dev.yawaflua.gominecraftbridge.protocol.ProtocolJson;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/** Minecraft 1.21.1 networking registration overlay. */
public final class BridgeAdminNetworking {
private BridgeAdminNetworking() {
}
public static void register(GoPluginManager plugins) {
PayloadTypeRegistry.playC2S().register(AdminRequestPayload.TYPE, AdminRequestPayload.CODEC);
PayloadTypeRegistry.playS2C().register(AdminResponsePayload.TYPE, AdminResponsePayload.CODEC);
ServerPlayNetworking.registerGlobalReceiver(AdminRequestPayload.TYPE, (payload, context) -> {
boolean allowed = MinecraftVersionAdapter.isOperator(context.server(), context.player());
if (!allowed) {
send(context, withoutDetails(plugins.managementSnapshot(
false, "Administrator permission is required"
)));
return;
}
String message = null;
if ("reload".equals(payload.action())) {
ReloadResult result = plugins.reload(payload.pluginId(), context.server());
message = result.message();
} else if ("rescan".equals(payload.action())) {
ReloadResult result = plugins.rescan(context.server());
message = result.message();
} else if (!"refresh".equals(payload.action())) {
message = "Unknown admin action: " + payload.action();
}
send(context, plugins.managementSnapshot(true, message));
});
}
static BridgeManagementSnapshot withoutDetails(BridgeManagementSnapshot snapshot) {
return new BridgeManagementSnapshot(
snapshot.generatedAtUnixMilli(), snapshot.serverRunning(), false,
snapshot.message(), List.of(), List.of()
);
}
private static void send(ServerPlayNetworking.Context context, BridgeManagementSnapshot snapshot) {
context.responseSender().sendPacket(new AdminResponsePayload(boundedJson(snapshot)));
}
/** Keeps the encoded 1.21.1 payload below its fixed 1 MiB protocol limit. */
static String boundedJson(BridgeManagementSnapshot snapshot) {
String json = ProtocolJson.GSON.toJson(snapshot);
if (fits(json)) {
return json;
}
for (int retainedLogs : new int[]{64, 16, 4, 0}) {
List<ManagedPluginSnapshot> plugins = new ArrayList<>(snapshot.plugins().size());
for (ManagedPluginSnapshot plugin : snapshot.plugins()) {
int from = Math.max(0, plugin.logs().size() - retainedLogs);
plugins.add(new ManagedPluginSnapshot(
plugin.metadata(), plugin.state(), plugin.backend(), plugin.origin(),
plugin.logs().subList(from, plugin.logs().size())
));
}
BridgeManagementSnapshot trimmed = new BridgeManagementSnapshot(
snapshot.generatedAtUnixMilli(), snapshot.serverRunning(), snapshot.canReload(),
"Response was shortened to fit the Minecraft 1.21.1 payload limit",
snapshot.packages(), plugins
);
json = ProtocolJson.GSON.toJson(trimmed);
if (fits(json)) {
return json;
}
}
return ProtocolJson.GSON.toJson(new BridgeManagementSnapshot(
snapshot.generatedAtUnixMilli(), snapshot.serverRunning(), snapshot.canReload(),
"Management data exceeded the Minecraft 1.21.1 payload limit; reload or inspect fewer packages",
List.of(), List.of()
));
}
private static boolean fits(String json) {
return json.length() <= AdminResponsePayload.MAX_JSON_CHARS
&& json.getBytes(StandardCharsets.UTF_8).length <= AdminResponsePayload.MAX_JSON_CHARS;
}
}
@@ -0,0 +1,25 @@
{
"schemaVersion": 1,
"id": "go_minecraft_bridge",
"version": "${version}",
"name": "Go Minecraft Bridge",
"description": "A native Go plugin host with server and client runtimes.",
"authors": ["yawaflua"],
"license": "All-Rights-Reserved",
"environment": "*",
"entrypoints": {
"main": ["dev.yawaflua.gominecraftbridge.GoMinecraftBridgeMod"],
"client": ["dev.yawaflua.gominecraftbridge.client.GoMinecraftBridgeClient"],
"modmenu": ["dev.yawaflua.gominecraftbridge.client.GoBridgeModMenuIntegration"]
},
"depends": {
"fabricloader": ">=0.16.14",
"minecraft": "~1.21.1",
"java": ">=21",
"fabric-api": "*"
},
"suggests": {
"cloth-config": ">=15.0.140",
"modmenu": ">=11.0.3"
}
}
@@ -0,0 +1,35 @@
package dev.yawaflua.gominecraftbridge.network;
import dev.yawaflua.gominecraftbridge.management.BridgeManagementSnapshot;
import dev.yawaflua.gominecraftbridge.management.ManagedPluginSnapshot;
import dev.yawaflua.gominecraftbridge.protocol.PluginEnvironment;
import dev.yawaflua.gominecraftbridge.protocol.PluginLog;
import dev.yawaflua.gominecraftbridge.protocol.PluginMetadata;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
class BridgeAdminNetworking1211Test {
@Test
void managementJsonIsTrimmedBelowTheLegacyPayloadLimit() {
String hugeMessage = "Ж".repeat(8192);
List<PluginLog> logs = java.util.stream.IntStream.range(0, 500)
.mapToObj(index -> new PluginLog("sdk", "info", hugeMessage, index))
.toList();
PluginMetadata metadata = new PluginMetadata(
"large", "Large", "1", "", List.of(), null, 2, null, PluginEnvironment.BOTH
);
BridgeManagementSnapshot snapshot = new BridgeManagementSnapshot(
1, true, true, null, List.of(),
List.of(new ManagedPluginSnapshot(metadata, "running", "native", "/plugin.so", logs))
);
String json = BridgeAdminNetworking.boundedJson(snapshot);
assertTrue(json.length() <= AdminResponsePayload.MAX_JSON_CHARS);
assertTrue(json.getBytes(StandardCharsets.UTF_8).length <= AdminResponsePayload.MAX_JSON_CHARS);
}
}
+95
View File
@@ -0,0 +1,95 @@
plugins {
id 'net.fabricmc.fabric-loom'
id 'maven-publish'
}
version = rootProject.mod_version
group = rootProject.maven_group
base {
archivesName = "${rootProject.archives_base_name}-26.1.2"
}
repositories {
mavenCentral()
maven { url 'https://maven.shedaniel.me/' }
maven { url 'https://maven.terraformersmc.com/releases/' }
maven { url 'https://api.modrinth.com/maven/' }
}
loom {
splitEnvironmentSourceSets()
mods {
go_minecraft_bridge {
sourceSet sourceSets.main
sourceSet sourceSets.client
}
}
runs {
configureEach {
vmArg '--enable-native-access=ALL-UNNAMED'
}
}
}
sourceSets {
main {
java.srcDirs rootProject.file('src/main/java'), rootProject.file('src/main/generated')
resources.srcDirs rootProject.file('src/main/resources')
}
client {
java.srcDirs rootProject.file('src/client/java')
}
test {
java.srcDirs rootProject.file('src/test/java')
}
}
dependencies {
minecraft 'com.mojang:minecraft:26.1.2'
implementation 'net.fabricmc:fabric-loader:0.19.3'
implementation 'net.fabricmc.fabric-api:fabric-api:0.149.1+26.1.2'
implementation 'com.google.code.gson:gson:2.13.2'
implementation include('com.google.flatbuffers:flatbuffers-java:25.2.10')
implementation 'net.java.dev.jna:jna:5.18.1'
clientImplementation 'me.shedaniel.cloth:cloth-config-fabric:26.1.154'
clientImplementation 'maven.modrinth:modmenu:18.0.0'
testImplementation platform('org.junit:junit-bom:5.13.4')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
processResources {
inputs.property 'version', project.version
filesMatching('fabric.mod.json') {
expand 'version': project.version
}
}
tasks.withType(JavaCompile).configureEach {
options.release = 25
}
tasks.withType(Test).configureEach {
useJUnitPlatform()
jvmArgs '--enable-native-access=ALL-UNNAMED'
}
java {
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25
}
publishing {
publications {
create('mavenJava', MavenPublication) {
from components.java
}
}
}
@@ -0,0 +1,29 @@
package dev.yawaflua.gominecraftbridge.compat;
import net.minecraft.SharedConstants;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.Level;
/** Minecraft-version-specific names kept behind one small source overlay. */
public final class MinecraftVersionAdapter {
private MinecraftVersionAdapter() {
}
public static String gameVersion() {
return SharedConstants.getCurrentVersion().name();
}
public static String dimension(Level level) {
return level.dimension().identifier().toString();
}
public static long dayTime(ServerLevel level) {
return level.getDefaultClockTime();
}
public static boolean isOperator(MinecraftServer server, ServerPlayer player) {
return server.getPlayerList().isOp(player.nameAndId());
}
}
@@ -0,0 +1,29 @@
package dev.yawaflua.gominecraftbridge.network;
import dev.yawaflua.gominecraftbridge.GoMinecraftBridgeMod;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.Identifier;
public record AdminRequestPayload(String action, String pluginId) implements CustomPacketPayload {
public static final Type<AdminRequestPayload> TYPE = new Type<>(
Identifier.fromNamespaceAndPath(GoMinecraftBridgeMod.MOD_ID, "admin_request")
);
public static final StreamCodec<RegistryFriendlyByteBuf, AdminRequestPayload> CODEC =
CustomPacketPayload.codec(AdminRequestPayload::write, AdminRequestPayload::new);
public AdminRequestPayload(RegistryFriendlyByteBuf buffer) {
this(buffer.readUtf(32), buffer.readUtf(64));
}
private void write(RegistryFriendlyByteBuf buffer) {
buffer.writeUtf(this.action, 32);
buffer.writeUtf(this.pluginId == null ? "" : this.pluginId, 64);
}
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
@@ -0,0 +1,29 @@
package dev.yawaflua.gominecraftbridge.network;
import dev.yawaflua.gominecraftbridge.GoMinecraftBridgeMod;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.Identifier;
public record AdminResponsePayload(String json) implements CustomPacketPayload {
public static final int MAX_JSON_CHARS = 2 * 1024 * 1024;
public static final Type<AdminResponsePayload> TYPE = new Type<>(
Identifier.fromNamespaceAndPath(GoMinecraftBridgeMod.MOD_ID, "admin_response")
);
public static final StreamCodec<RegistryFriendlyByteBuf, AdminResponsePayload> CODEC =
CustomPacketPayload.codec(AdminResponsePayload::write, AdminResponsePayload::new);
public AdminResponsePayload(RegistryFriendlyByteBuf buffer) {
this(buffer.readUtf(MAX_JSON_CHARS));
}
private void write(RegistryFriendlyByteBuf buffer) {
buffer.writeUtf(this.json, MAX_JSON_CHARS);
}
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
@@ -0,0 +1,68 @@
package dev.yawaflua.gominecraftbridge.network;
import dev.yawaflua.gominecraftbridge.compat.MinecraftVersionAdapter;
import dev.yawaflua.gominecraftbridge.host.GoPluginManager;
import dev.yawaflua.gominecraftbridge.management.BridgeManagementSnapshot;
import dev.yawaflua.gominecraftbridge.management.ReloadResult;
import dev.yawaflua.gominecraftbridge.protocol.ProtocolJson;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import java.util.List;
public final class BridgeAdminNetworking {
private BridgeAdminNetworking() {
}
public static void register(GoPluginManager plugins) {
PayloadTypeRegistry.serverboundPlay().register(AdminRequestPayload.TYPE, AdminRequestPayload.CODEC);
PayloadTypeRegistry.clientboundPlay().registerLarge(
AdminResponsePayload.TYPE,
AdminResponsePayload.CODEC,
AdminResponsePayload.MAX_JSON_CHARS * 4 + 256
);
ServerPlayNetworking.registerGlobalReceiver(AdminRequestPayload.TYPE, (payload, context) -> {
boolean allowed = MinecraftVersionAdapter.isOperator(context.server(), context.player());
if (!allowed) {
send(context, withoutDetails(plugins.managementSnapshot(
false,
"Administrator permission is required"
)));
return;
}
String message = null;
if ("reload".equals(payload.action())) {
ReloadResult result = plugins.reload(payload.pluginId(), context.server());
message = result.message();
} else if ("rescan".equals(payload.action())) {
ReloadResult result = plugins.rescan(context.server());
message = result.message();
} else if (!"refresh".equals(payload.action())) {
message = "Unknown admin action: " + payload.action();
}
send(context, plugins.managementSnapshot(true, message));
});
}
static BridgeManagementSnapshot withoutDetails(BridgeManagementSnapshot snapshot) {
return new BridgeManagementSnapshot(
snapshot.generatedAtUnixMilli(),
snapshot.serverRunning(),
false,
snapshot.message(),
List.of(),
List.of()
);
}
private static void send(
ServerPlayNetworking.Context context,
BridgeManagementSnapshot snapshot
) {
String json = ProtocolJson.GSON.toJson(snapshot);
context.responseSender().sendPacket(new AdminResponsePayload(json));
}
}