Add throttled player-position tracking, mod side (Phase 7b)

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tKdPZt78zbPUZMXWzKEKt
This commit is contained in:
2026-08-09 19:38:16 +02:00
parent 52583f2056
commit ef47d4481a
5 changed files with 102 additions and 3 deletions
@@ -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<PlayerPosition> 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<PlayerPosition> players) {
}
@Override
public void setChatListener(ChatListener listener) {
}
@@ -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 -&gt; api {"type":"chat","uuid":"...","username":"...","message":"..."}
* api -&gt; mod {"type":"chat","username":"...","message":"..."}
* api -&gt; mod {"type":"waypoint_share","name":"...","x":..,"y":..,"z":..,"dimension":..,"color":"#RRGGBB","format":"journeymap"|"xaero"}
*
* mod -&gt; api {"type":"player_positions","dimension":0,"players":[{"uuid":"...","username":"...","x":..,"y":..,"z":..}]}
* </pre>
*
* 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<PlayerPosition> players) {
if (!serverReady) return;
List<Object> playerList = new ArrayList<>();
for (PlayerPosition p : players) {
Map<String, Object> 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<String, Object> 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;
@@ -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;
}
@@ -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;
}
}