Phase 3: /mcmapper link command and two-way chat bridge

Test-first from here on (per request after Phase 2's backend commit):
LinkCodeGenerator has a standalone test (common/src/test) written and
confirmed failing before the implementation existed.

common: LinkCodeGenerator produces a 6-character code from an
unambiguous charset (excludes 0/O/1/I/L — it gets read off a chat line
and typed back). BackendConnection grows sendLinkRequest (now actually
implemented, was a Phase 1 stub), sendChatMessage, and setChatListener/
ChatListener for inbound web->game chat, all wired into
DefaultBackendConnection's existing JSON wire protocol.

forge-1_12_2: LinkCommand (/mcmapper link) generates a code, resolves
online/offline from the server's actual auth mode
(MinecraftServer#isServerInOnlineMode), and shows it to the player.
Forge1122ChatBridge implements the inbound half (injects web chat into
real in-game chat via the player list) — injectWaypointShare is a
documented Phase 4 stub, same pattern as sendLinkRequest was in Phase 1.
MCMapperMod hooks ServerChatEvent to forward in-game chat out and wires
the chat listener to the bridge.

Verified end-to-end against a live MCMapper-Backend instance through the
real Java client (not a stand-in): a mod-generated link code correctly
redeems via the backend's HTTP endpoint to an account with the mod-
supplied username, and a browser chat message correctly round-trips all
the way to the mod's live ChatListener callback.
This commit is contained in:
2026-08-08 17:09:09 +02:00
parent ab94cca1b2
commit 73e10d34e5
7 changed files with 249 additions and 6 deletions
@@ -0,0 +1,29 @@
package com.octoturge.mcmapper.forge1122;
import com.octoturge.mcmapper.common.ChatBridge;
import com.octoturge.mcmapper.common.protocol.WaypointShare;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.text.TextComponentString;
import org.apache.logging.log4j.Logger;
public class Forge1122ChatBridge implements ChatBridge {
private final MinecraftServer server;
private final Logger logger;
public Forge1122ChatBridge(MinecraftServer server, Logger logger) {
this.server = server;
this.logger = logger;
}
@Override
public void injectWebChatMessage(String displayName, String message) {
server.getPlayerList().sendMessage(new TextComponentString("[Web] " + displayName + ": " + message));
}
@Override
public void injectWaypointShare(WaypointShare waypoint) {
// Marker sharing lands in Phase 4 (needs the JourneyMap/Xaero wire-format research spike
// the plan calls for) — nothing calls this yet.
logger.warn("injectWaypointShare called before Phase 4's waypoint sharing is implemented — ignoring");
}
}
@@ -0,0 +1,55 @@
package com.octoturge.mcmapper.forge1122;
import com.octoturge.mcmapper.common.BackendConnection;
import com.octoturge.mcmapper.common.LinkCodeGenerator;
import com.octoturge.mcmapper.common.protocol.LinkRequest;
import net.minecraft.command.CommandBase;
import net.minecraft.command.CommandException;
import net.minecraft.command.ICommandSender;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.text.TextComponentString;
/** {@code /mcmapper link} — see LinkRequest.java's javadoc and MCMapper-Backend's link.ts. */
public class LinkCommand extends CommandBase {
private final BackendConnection connection;
public LinkCommand(BackendConnection connection) {
this.connection = connection;
}
@Override
public String getName() {
return "mcmapper";
}
@Override
public String getUsage(ICommandSender sender) {
return "/mcmapper link";
}
@Override
public int getRequiredPermissionLevel() {
return 0; // any player may link their own account
}
@Override
public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException {
if (args.length == 0 || !"link".equals(args[0])) {
sender.sendMessage(new TextComponentString("Usage: /mcmapper link"));
return;
}
if (!(sender instanceof EntityPlayerMP)) {
sender.sendMessage(new TextComponentString("Only players can link an account."));
return;
}
EntityPlayerMP player = (EntityPlayerMP) sender;
String code = LinkCodeGenerator.generate();
LinkRequest.AuthMode authMode =
server.isServerInOnlineMode() ? LinkRequest.AuthMode.ONLINE : LinkRequest.AuthMode.OFFLINE;
connection.sendLinkRequest(new LinkRequest(player.getUniqueID().toString(), player.getName(), code, authMode));
player.sendMessage(new TextComponentString(
"Your MCMapper link code: " + code + " — enter it on the map site within 10 minutes."));
}
}
@@ -5,9 +5,11 @@ import com.octoturge.mcmapper.common.DefaultBackendConnection;
import com.octoturge.mcmapper.common.config.MapperConfig;
import com.octoturge.mcmapper.common.protocol.DeltaEvent;
import com.octoturge.mcmapper.common.protocol.SectionData;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.world.WorldServer;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.event.ServerChatEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.event.FMLServerStartingEvent;
@@ -27,8 +29,8 @@ import java.util.Set;
* Entry point for the 1.12.2 leaf — the primary/first-implemented target (Enigmatica 2).
* Phase 1: connects to the backend over WS, backfills the overworld's loaded chunks, and
* flushes event-driven column deltas on a timer (no periodic reconciliation sweep yet — see
* plan's Phase 7). Phase 2 adds full-section backfill/flush for 3D mesh rendering. Chat bridge
* and `/mcmapper link` land in Phase 3.
* plan's Phase 7). Phase 2 adds full-section backfill/flush for 3D mesh rendering. Phase 3 adds
* `/mcmapper link` and the two-way chat bridge (see LinkCommand, Forge1122ChatBridge).
*/
@Mod(modid = MCMapperMod.MOD_ID, name = "MCMapper", version = MCMapperMod.VERSION)
public class MCMapperMod {
@@ -88,9 +90,19 @@ public class MCMapperMod {
}
});
Forge1122ChatBridge chatBridge = new Forge1122ChatBridge(event.getServer(), LOGGER);
connection.setChatListener(chatBridge::injectWebChatMessage);
event.registerServerCommand(new LinkCommand(connection));
MinecraftForge.EVENT_BUS.register(this);
}
@SubscribeEvent
public void onServerChat(ServerChatEvent event) {
EntityPlayerMP player = event.getPlayer();
connection.sendChatMessage(player.getUniqueID().toString(), player.getName(), event.getMessage());
}
@SubscribeEvent
public void onServerTick(TickEvent.ServerTickEvent event) {
if (event.phase != TickEvent.Phase.END) return;