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:
@@ -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 -> 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":..}]}
|
||||
* </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;
|
||||
}
|
||||
}
|
||||
@@ -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<DeltaEvent> pendingDeltas = Collections.synchronizedList(new ArrayList<>());
|
||||
private final Set<Long> 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<PlayerPosition> 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() {
|
||||
|
||||
Reference in New Issue
Block a user