From ef47d4481a1f3351e7f7f6fbb8457cfb890c3983 Mon Sep 17 00:00:00 2001 From: Octoturge Date: Sun, 9 Aug 2026 19:38:16 +0200 Subject: [PATCH] Add throttled player-position tracking, mod side (Phase 7b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds PlayerPosition (common/protocol) and BackendConnection.sendPlayerPositions(), wired into MCMapperMod.java: a new playerPositionIntervalTicks-throttled tick timer (gated by playerTrackingEnabled) sends the full current overworld online-player roster to the backend each interval, mirroring the "current state, not a diff" philosophy of columns/sections — a player logging out just stops appearing next send, no separate leave message needed. Wire message: {"type":"player_positions","dimension":0,"players":[...]}. Backend-side receive/relay + admin visibility toggle land in a follow-up commit. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015tKdPZt78zbPUZMXWzKEKt --- .../mcmapper/common/BackendConnection.java | 13 +++++++ .../common/DefaultBackendConnection.java | 35 +++++++++++++++++-- .../mcmapper/common/config/MapperConfig.java | 1 + .../common/protocol/PlayerPosition.java | 23 ++++++++++++ .../mcmapper/forge1122/MCMapperMod.java | 33 ++++++++++++++++- 5 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 common/src/main/java/com/octoturge/mcmapper/common/protocol/PlayerPosition.java diff --git a/common/src/main/java/com/octoturge/mcmapper/common/BackendConnection.java b/common/src/main/java/com/octoturge/mcmapper/common/BackendConnection.java index 41690c9..5eb794f 100644 --- a/common/src/main/java/com/octoturge/mcmapper/common/BackendConnection.java +++ b/common/src/main/java/com/octoturge/mcmapper/common/BackendConnection.java @@ -2,6 +2,7 @@ package com.octoturge.mcmapper.common; import com.octoturge.mcmapper.common.protocol.DeltaEvent; import com.octoturge.mcmapper.common.protocol.LinkRequest; +import com.octoturge.mcmapper.common.protocol.PlayerPosition; import com.octoturge.mcmapper.common.protocol.SectionData; import com.octoturge.mcmapper.common.protocol.WaypointShare; @@ -28,6 +29,14 @@ public interface BackendConnection { /** Forwards one in-game chat message to the backend — see ChatBridge.OutboundSink's javadoc. */ void sendChatMessage(String uuid, String username, String message); + /** + * Sends the full current online-player roster for one dimension (Phase 7b) — see + * {@link PlayerPosition}'s javadoc for why this is always the whole roster, not a diff. An + * empty list is a meaningful, intentional send (everyone logged out), so callers should call + * this every throttle tick rather than skipping it when there are no players. + */ + void sendPlayerPositions(int dimension, List players); + /** Registers the callback for web-originated chat messages the backend relays back to us. */ void setChatListener(ChatListener listener); @@ -65,6 +74,10 @@ public interface BackendConnection { public void sendChatMessage(String uuid, String username, String message) { } + @Override + public void sendPlayerPositions(int dimension, List players) { + } + @Override public void setChatListener(ChatListener listener) { } diff --git a/common/src/main/java/com/octoturge/mcmapper/common/DefaultBackendConnection.java b/common/src/main/java/com/octoturge/mcmapper/common/DefaultBackendConnection.java index 899f913..53e1c5b 100644 --- a/common/src/main/java/com/octoturge/mcmapper/common/DefaultBackendConnection.java +++ b/common/src/main/java/com/octoturge/mcmapper/common/DefaultBackendConnection.java @@ -3,6 +3,7 @@ package com.octoturge.mcmapper.common; import com.octoturge.mcmapper.common.json.MiniJson; import com.octoturge.mcmapper.common.protocol.DeltaEvent; import com.octoturge.mcmapper.common.protocol.LinkRequest; +import com.octoturge.mcmapper.common.protocol.PlayerPosition; import com.octoturge.mcmapper.common.protocol.SectionData; import com.octoturge.mcmapper.common.protocol.WaypointShare; import com.octoturge.mcmapper.common.ws.SimpleWebSocketClient; @@ -32,6 +33,8 @@ import java.util.function.Consumer; * mod -> api {"type":"chat","uuid":"...","username":"...","message":"..."} * api -> mod {"type":"chat","username":"...","message":"..."} * api -> mod {"type":"waypoint_share","name":"...","x":..,"y":..,"z":..,"dimension":..,"color":"#RRGGBB","format":"journeymap"|"xaero"} + * + * mod -> api {"type":"player_positions","dimension":0,"players":[{"uuid":"...","username":"...","x":..,"y":..,"z":..}]} * * * Phase 3: {@code link_request} is sent by {@code /mcmapper link}; {@code chat} both directions @@ -58,8 +61,16 @@ import java.util.function.Consumer; * last flush). * * No offline queue: deltas sent while disconnected are dropped rather than buffered — the - * periodic reconciliation sweep (not yet built, see plan's Phase 7) is what's meant to catch - * whatever a disconnect window missed, so buffering here would be solving the same problem twice. + * periodic reconciliation sweep (see {@link com.octoturge.mcmapper.common.ReconciliationScheduler}) + * is what's meant to catch whatever a disconnect window missed, so buffering here would be + * solving the same problem twice. + * + * Phase 7b: {@code player_positions} carries the full current online-player roster for one + * dimension (not a diff), throttled on a separate timer from the delta flush — see + * {@link PlayerPosition}'s javadoc. The backend's per-server {@code playerPositionsVisible} + * admin toggle decides whether this gets relayed on to web viewers; it's independent of the + * mod-local {@code playerTrackingEnabled} config, which decides whether the mod computes/sends + * this at all. * * This class avoids a third-party JSON/WS library entirely (see {@link SimpleWebSocketClient} * and {@link MiniJson}'s javadoc) to keep the mod's classpath free of anything that would need @@ -270,6 +281,26 @@ public class DefaultBackendConnection implements BackendConnection { sendRaw("chat", msg); } + @Override + public void sendPlayerPositions(int dimension, List players) { + if (!serverReady) return; + List playerList = new ArrayList<>(); + for (PlayerPosition p : players) { + Map obj = new LinkedHashMap<>(); + obj.put("uuid", p.uuid); + obj.put("username", p.username); + obj.put("x", (double) p.x); + obj.put("y", (double) p.y); + obj.put("z", (double) p.z); + playerList.add(obj); + } + Map msg = new LinkedHashMap<>(); + msg.put("type", "player_positions"); + msg.put("dimension", (double) dimension); + msg.put("players", playerList); + sendRaw("player_positions", msg); + } + @Override public void setChatListener(ChatListener listener) { this.chatListener = listener; diff --git a/common/src/main/java/com/octoturge/mcmapper/common/config/MapperConfig.java b/common/src/main/java/com/octoturge/mcmapper/common/config/MapperConfig.java index 5ca18ac..0aa4769 100644 --- a/common/src/main/java/com/octoturge/mcmapper/common/config/MapperConfig.java +++ b/common/src/main/java/com/octoturge/mcmapper/common/config/MapperConfig.java @@ -13,4 +13,5 @@ public class MapperConfig { public int deltaFlushIntervalTicks = 20; public int reconciliationIntervalTicks = 20 * 60 * 5; public int reconciliationChunksPerSweep = 50; + public int playerPositionIntervalTicks = 20 * 2; } diff --git a/common/src/main/java/com/octoturge/mcmapper/common/protocol/PlayerPosition.java b/common/src/main/java/com/octoturge/mcmapper/common/protocol/PlayerPosition.java new file mode 100644 index 0000000..fe57f66 --- /dev/null +++ b/common/src/main/java/com/octoturge/mcmapper/common/protocol/PlayerPosition.java @@ -0,0 +1,23 @@ +package com.octoturge.mcmapper.common.protocol; + +/** + * One online player's throttled position (Phase 7b), block-granularity. Sent as the full current + * roster on every flush (not a diff) — same "current state, not a diff" philosophy as + * {@link DeltaEvent}'s columns — so a player logging out simply stops appearing in the next + * roster rather than needing a separate leave message. + */ +public class PlayerPosition { + public final String uuid; + public final String username; + public final int x; + public final int y; + public final int z; + + public PlayerPosition(String uuid, String username, int x, int y, int z) { + this.uuid = uuid; + this.username = username; + this.x = x; + this.y = y; + this.z = z; + } +} diff --git a/forge-1_12_2/src/main/java/com/octoturge/mcmapper/forge1122/MCMapperMod.java b/forge-1_12_2/src/main/java/com/octoturge/mcmapper/forge1122/MCMapperMod.java index 691df9a..42b7826 100644 --- a/forge-1_12_2/src/main/java/com/octoturge/mcmapper/forge1122/MCMapperMod.java +++ b/forge-1_12_2/src/main/java/com/octoturge/mcmapper/forge1122/MCMapperMod.java @@ -5,8 +5,11 @@ import com.octoturge.mcmapper.common.DefaultBackendConnection; import com.octoturge.mcmapper.common.ReconciliationScheduler; import com.octoturge.mcmapper.common.config.MapperConfig; import com.octoturge.mcmapper.common.protocol.DeltaEvent; +import com.octoturge.mcmapper.common.protocol.PlayerPosition; import com.octoturge.mcmapper.common.protocol.SectionData; import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.math.MathHelper; import net.minecraft.world.WorldServer; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.config.Configuration; @@ -33,7 +36,9 @@ import java.util.Set; * 3D mesh rendering. Phase 3 adds `/mcmapper link` and the two-way chat bridge (see LinkCommand, * Forge1122ChatBridge). Phase 7 adds a periodic reconciliation sweep (see {@link * ReconciliationScheduler}) that catches non-event mutations (world-gen, other mods, `/fill`) - * the event-driven hooks in {@link Forge1122ChunkAdapter} never see. + * the event-driven hooks in {@link Forge1122ChunkAdapter} never see. Phase 7b adds throttled + * online-player position tracking (config-gated via {@code playerTrackingEnabled}), overworld + * only — see {@link #sendPlayerPositions()}. */ @Mod(modid = MCMapperMod.MOD_ID, name = "MCMapper", version = MCMapperMod.VERSION) public class MCMapperMod { @@ -49,8 +54,10 @@ public class MCMapperMod { private final List pendingDeltas = Collections.synchronizedList(new ArrayList<>()); private final Set pendingSectionChunks = Collections.synchronizedSet(new LinkedHashSet<>()); private final ReconciliationScheduler reconciliationScheduler = new ReconciliationScheduler(); + private MinecraftServer mcServer; private int ticksSinceFlush = 0; private int ticksSinceReconciliation = 0; + private int ticksSincePlayerPositions = 0; @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { @@ -75,6 +82,14 @@ public class MCMapperMod { config.reconciliationChunksPerSweep = forgeConfig.getInt("reconciliationChunksPerSweep", "network", config.reconciliationChunksPerSweep, 1, 5000, "Max chunks to re-read and resend per reconciliation sweep"); + config.playerTrackingEnabled = forgeConfig.getBoolean("playerTrackingEnabled", "network", + config.playerTrackingEnabled, + "Whether to send throttled online-player positions to the backend at all. The " + + "backend also has its own per-server admin toggle deciding whether it " + + "relays this on to web viewers — this setting only controls the mod side."); + config.playerPositionIntervalTicks = forgeConfig.getInt("playerPositionIntervalTicks", "network", + config.playerPositionIntervalTicks, 5, 20 * 60, + "How often (in ticks) to send the online-player roster to the backend, when playerTrackingEnabled"); if (forgeConfig.hasChanged()) forgeConfig.save(); } @@ -85,6 +100,7 @@ public class MCMapperMod { return; } + mcServer = event.getServer(); connection = new DefaultBackendConnection(LOGGER::info, LOGGER::warn); connection.connect(config.backendUrl, config.serverToken); LOGGER.info("MCMapper (1.12.2 leaf) connecting to " + config.backendUrl); @@ -130,6 +146,21 @@ public class MCMapperMod { ticksSinceReconciliation = 0; reconcile(); } + + if (config.playerTrackingEnabled && ++ticksSincePlayerPositions >= config.playerPositionIntervalTicks) { + ticksSincePlayerPositions = 0; + sendPlayerPositions(); + } + } + + private void sendPlayerPositions() { + List players = new ArrayList<>(); + for (EntityPlayerMP player : mcServer.getPlayerList().getPlayers()) { + if (player.dimension != 0) continue; + players.add(new PlayerPosition(player.getUniqueID().toString(), player.getName(), + MathHelper.floor(player.posX), MathHelper.floor(player.posY), MathHelper.floor(player.posZ))); + } + connection.sendPlayerPositions(0, players); } private void reconcile() {