From 0d8e670cd8280e32d6c9508f3e652c01f53b3888 Mon Sep 17 00:00:00 2001 From: Octoturge Date: Sun, 9 Aug 2026 22:13:45 +0200 Subject: [PATCH] Phase 9: implement forge-1_7_10 leaf (WS, deltas, chat, link, reconciliation, player tracking) Ports the full feature set proven by forge-1_12_2 (Phases 1-2-3-7-7b) to MC 1.7.10's older, pre-block-state Forge/FML generation: Forge1710ChunkAdapter reads raw Block+meta via Chunk/ExtendedBlockStorage instead of IBlockState, hooks BlockEvent/ChunkEvent/TickEvent under cpw.mods.fml, and MCMapperMod/LinkCommand/Forge1710ChatBridge adapt to 1.7.10's CommandBase/ChatComponentText/ServerConfigurationManager API shapes. No MultiPlaceEvent hook at this Forge version (falls back to the reconciliation sweep for multi-block placements). Verified against real Forge 10.13.4.1614-1.7.10 via ./gradlew :forge-1_7_10:build. --- README.md | 17 +- .../forge1710/Forge1710ChatBridge.java | 30 +++ .../forge1710/Forge1710ChunkAdapter.java | 177 +++++++++++++++ .../mcmapper/forge1710/LinkCommand.java | 55 +++++ .../mcmapper/forge1710/MCMapperMod.java | 211 +++++++++++++++++- 5 files changed, 482 insertions(+), 8 deletions(-) create mode 100644 forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/Forge1710ChatBridge.java create mode 100644 forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/Forge1710ChunkAdapter.java create mode 100644 forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/LinkCommand.java diff --git a/README.md b/README.md index 581b4ad..94dc2b2 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,10 @@ in Claude Code's plan mode. - `forge-1_12_2/` — **primary leaf**, MC 1.12.2 Forge. First implemented; this is the actual driving use case (an Enigmatica 2 modpack server). Builds with legacy ForgeGradle 2.3 via anatawa12's Gradle-7-compatible fork (see `THIRD_PARTY_NOTICES.md`). -- `forge-1_7_10/` — MC 1.7.10 Forge. Second priority. Builds with legacy ForgeGradle 1.2, same - fork family, one Forge-tooling generation further back. +- `forge-1_7_10/` — MC 1.7.10 Forge (Phase 9). Second priority, fully wired against the same + `common` interfaces as `forge-1_12_2`. Builds with legacy ForgeGradle 1.2, same fork family, one + Forge-tooling generation further back — pre-block-state (raw `Block` + metadata int, no + `IBlockState`) and pre-FML-repackage (`cpw.mods.fml.*`, not `net.minecraftforge.fml.*`). - `neoforge-26_1/` — MC 26.1.2 (NeoForge, assumed). Lowest MVP priority. Structurally scaffolded, not yet wired into the default build (see `settings.gradle`). @@ -43,7 +45,7 @@ needs Gradle 8+ and JDK 17+ (ModDevGradle), incompatible with the legacy leaves' within one Gradle invocation; see `settings.gradle`'s comment for the workaround until Phase 10 gives it a proper isolated build. -## Configuration (Phase 1: forge-1_12_2) +## Configuration (forge-1_12_2 and forge-1_7_10) On first server start the leaf writes `config/mcmapper.cfg` with defaults. Set: @@ -58,9 +60,14 @@ overworld chunks, and streams event-driven column deltas (block place/break) in walks a bounded number of currently-loaded chunks per tick (`reconciliationChunksPerSweep`, default 4) and re-sends any that drifted from what the backend last acknowledged, to catch mutations event hooks miss (world-gen, other mods, `/fill`). Only the overworld is tracked — -other dimensions and `forge-1_7_10`/`neoforge-26_1` land in later phases. The WS client and -JSON encoding are hand-rolled (no third-party dependency) — see +other dimensions and `neoforge-26_1` land in a later phase. The WS client and JSON encoding are +hand-rolled (no third-party dependency) — see `common/src/main/java/.../ws/SimpleWebSocketClient.java` and `.../json/MiniJson.java` for why. +`forge-1_7_10` implements the identical config/flush/reconciliation/player-tracking behavior +against 1.7.10's own API generation (see `Forge1710ChunkAdapter`'s javadoc for what differs) — it +has no `MultiPlaceEvent` hook (that class doesn't reliably exist at this Forge version), so +multi-block placements like doors/beds are only caught by the reconciliation sweep rather than +immediately, unlike `forge-1_12_2`. ### Player position tracking (Phase 7b) diff --git a/forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/Forge1710ChatBridge.java b/forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/Forge1710ChatBridge.java new file mode 100644 index 0000000..fc35108 --- /dev/null +++ b/forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/Forge1710ChatBridge.java @@ -0,0 +1,30 @@ +package com.octoturge.mcmapper.forge1710; + +import com.octoturge.mcmapper.common.ChatBridge; +import com.octoturge.mcmapper.common.protocol.WaypointChatFormatter; +import com.octoturge.mcmapper.common.protocol.WaypointShare; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.ChatComponentText; +import org.apache.logging.log4j.Logger; + +public class Forge1710ChatBridge implements ChatBridge { + private final MinecraftServer server; + private final Logger logger; + + public Forge1710ChatBridge(MinecraftServer server, Logger logger) { + this.server = server; + this.logger = logger; + } + + @Override + public void injectWebChatMessage(String displayName, String message) { + server.getConfigurationManager().sendChatMsg(new ChatComponentText("[Web] " + displayName + ": " + message)); + } + + @Override + public void injectWaypointShare(WaypointShare waypoint) { + // Same rationale as Forge1122ChatBridge: neither JourneyMap's nor Xaero's chat-waypoint + // syntax needs a click-event component, so a plain broadcast message is enough. + server.getConfigurationManager().sendChatMsg(new ChatComponentText(WaypointChatFormatter.format(waypoint))); + } +} diff --git a/forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/Forge1710ChunkAdapter.java b/forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/Forge1710ChunkAdapter.java new file mode 100644 index 0000000..3a74b94 --- /dev/null +++ b/forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/Forge1710ChunkAdapter.java @@ -0,0 +1,177 @@ +package com.octoturge.mcmapper.forge1710; + +import com.octoturge.mcmapper.common.ChunkAdapter; +import com.octoturge.mcmapper.common.protocol.DeltaEvent; +import com.octoturge.mcmapper.common.protocol.SectionData; +import net.minecraft.block.Block; +import net.minecraft.world.World; +import net.minecraft.world.WorldServer; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.storage.ExtendedBlockStorage; +import net.minecraft.world.gen.ChunkProviderServer; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.world.BlockEvent; +import net.minecraftforge.event.world.ChunkEvent; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.gameevent.TickEvent; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 1.7.10's {@link ChunkAdapter} — one Forge/MC generation older than {@code forge-1_12_2}'s (pre + * block-state: raw {@code Block} + metadata int, not {@code IBlockState}), and pre-FML-repackage + * (event/tick classes live under {@code cpw.mods.fml}, not {@code net.minecraftforge.fml}). + * Column top-of-height lookups and section iteration otherwise mirror {@code + * Forge1122ChunkAdapter} exactly — see its javadoc for the reconciliation-vs-event framing. + */ +public class Forge1710ChunkAdapter implements ChunkAdapter { + private final WorldServer world; + private final String dimensionId; + + public Forge1710ChunkAdapter(WorldServer world) { + this.world = world; + this.dimensionId = String.valueOf(world.provider.dimensionId); + } + + public String getDimensionId() { + return dimensionId; + } + + @Override + public List readChunk(String dimension, int chunkX, int chunkZ) { + Chunk chunk = world.getChunkFromChunkCoords(chunkX, chunkZ); + List events = new ArrayList(256); + long now = System.currentTimeMillis(); + for (int lx = 0; lx < 16; lx++) { + for (int lz = 0; lz < 16; lz++) { + events.add(readColumn(chunk, chunkX * 16 + lx, chunkZ * 16 + lz, lx, lz, now, + DeltaEvent.Source.RECONCILIATION)); + } + } + return events; + } + + private DeltaEvent readColumn(Chunk chunk, int worldX, int worldZ, int localX, int localZ, + long now, DeltaEvent.Source source) { + int height = chunk.getHeightValue(localX, localZ); + int topY = Math.max(0, height - 1); + Block block = chunk.getBlock(localX, topY, localZ); + int meta = chunk.getBlockMetadata(localX, topY, localZ); + int id = Block.getIdFromBlock(block); + int blockStateId = ((id & 0xFFF) << 4) | (meta & 0xF); + return new DeltaEvent(dimensionId, worldX, topY, worldZ, blockStateId, now, source); + } + + @Override + public List readSections(int chunkX, int chunkZ) { + Chunk chunk = world.getChunkFromChunkCoords(chunkX, chunkZ); + ExtendedBlockStorage[] storage = chunk.getBlockStorageArray(); + List sections = new ArrayList(); + for (int sectionY = 0; sectionY < storage.length; sectionY++) { + ExtendedBlockStorage ebs = storage[sectionY]; + // Same empty-section shortcut as forge-1_12_2 — vanilla leaves storage null for an + // all-air section, and ExtendedBlockStorage tracks its own non-air count. + if (ebs == null || ebs.isEmpty()) continue; + + char[] blocks = new char[4096]; + for (int ly = 0; ly < 16; ly++) { + for (int lz = 0; lz < 16; lz++) { + for (int lx = 0; lx < 16; lx++) { + Block block = ebs.getBlockByExtId(lx, ly, lz); + int meta = ebs.getExtBlockMetadata(lx, ly, lz); + int id = Block.getIdFromBlock(block); + int blockStateId = ((id & 0xFFF) << 4) | (meta & 0xF); + blocks[(ly * 16 + lz) * 16 + lx] = (char) blockStateId; + } + } + } + sections.add(new SectionData(sectionY, blocks)); + } + return sections; + } + + @Override + public void registerEventHooks(DeltaSink sink) { + MinecraftForge.EVENT_BUS.register(new EventHooks(sink)); + } + + @Override + public List loadedChunkKeys() { + List keys = new ArrayList(); + ChunkProviderServer provider = (ChunkProviderServer) world.getChunkProvider(); + for (Object obj : provider.loadedChunks) { + Chunk chunk = (Chunk) obj; + keys.add((((long) chunk.xPosition) << 32) | (chunk.zPosition & 0xFFFFFFFFL)); + } + return keys; + } + + /** + * Same "mark dirty, drain once per tick" strategy as {@code Forge1122ChunkAdapter} (see its + * javadoc) — {@code BlockEvent.BreakEvent} still fires before the actual removal here. This + * era's {@code BlockEvent} carries {@code world}/{@code x}/{@code y}/{@code z} as plain public + * fields rather than a {@code World}/{@code BlockPos} getter pair, and there's no + * {@code MultiPlaceEvent} hook (multi-block placements like doors/beds are caught by the + * periodic reconciliation sweep instead — an acceptable gap given it's already a documented + * fallback for anything event hooks miss). + */ + private class EventHooks { + private final DeltaSink sink; + private final Set dirtyColumns = ConcurrentHashMap.newKeySet(); + + EventHooks(DeltaSink sink) { + this.sink = sink; + } + + @SubscribeEvent + public void onBlockBreak(BlockEvent.BreakEvent event) { + markDirty(event.world, event.x, event.z); + } + + @SubscribeEvent + public void onBlockPlace(BlockEvent.PlaceEvent event) { + markDirty(event.world, event.x, event.z); + } + + @SubscribeEvent + public void onChunkLoad(ChunkEvent.Load event) { + if (event.world != world) return; + Chunk chunk = event.getChunk(); + for (DeltaEvent e : readChunk(dimensionId, chunk.xPosition, chunk.zPosition)) { + sink.onDelta(e); + } + sink.onChunkDirty(chunk.xPosition, chunk.zPosition); + } + + @SubscribeEvent + public void onServerTick(TickEvent.ServerTickEvent event) { + if (event.phase != TickEvent.Phase.END || dirtyColumns.isEmpty()) return; + long now = System.currentTimeMillis(); + Set dirtyChunks = new LinkedHashSet(); + Iterator it = dirtyColumns.iterator(); + while (it.hasNext()) { + long key = it.next(); + it.remove(); + int wx = (int) (key >> 32); + int wz = (int) key; + Chunk chunk = world.getChunkFromChunkCoords(wx >> 4, wz >> 4); + sink.onDelta(readColumn(chunk, wx, wz, wx & 15, wz & 15, now, DeltaEvent.Source.EVENT)); + dirtyChunks.add((((long) (wx >> 4)) << 32) | ((wz >> 4) & 0xFFFFFFFFL)); + } + for (long chunkKey : dirtyChunks) { + sink.onChunkDirty((int) (chunkKey >> 32), (int) chunkKey); + } + } + + private void markDirty(World eventWorld, int x, int z) { + if (eventWorld != world) return; + long key = ((long) x << 32) | (z & 0xFFFFFFFFL); + dirtyColumns.add(key); + } + } +} diff --git a/forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/LinkCommand.java b/forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/LinkCommand.java new file mode 100644 index 0000000..94f4cc9 --- /dev/null +++ b/forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/LinkCommand.java @@ -0,0 +1,55 @@ +package com.octoturge.mcmapper.forge1710; + +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.ICommandSender; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.ChatComponentText; + +/** {@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 getCommandName() { + return "mcmapper"; + } + + @Override + public String getCommandUsage(ICommandSender sender) { + return "/mcmapper link"; + } + + @Override + public int getRequiredPermissionLevel() { + return 0; // any player may link their own account + } + + @Override + public void processCommand(ICommandSender sender, String[] args) { + if (args.length == 0 || !"link".equals(args[0])) { + sender.addChatMessage(new ChatComponentText("Usage: /mcmapper link")); + return; + } + if (!(sender instanceof EntityPlayerMP)) { + sender.addChatMessage(new ChatComponentText("Only players can link an account.")); + return; + } + + EntityPlayerMP player = (EntityPlayerMP) sender; + String code = LinkCodeGenerator.generate(); + LinkRequest.AuthMode authMode = MinecraftServer.getServer().isServerInOnlineMode() + ? LinkRequest.AuthMode.ONLINE : LinkRequest.AuthMode.OFFLINE; + connection.sendLinkRequest(new LinkRequest(player.getUniqueID().toString(), + player.getGameProfile().getName(), code, authMode)); + player.addChatMessage(new ChatComponentText( + "Your MCMapper link code: " + code + " — enter it on the map site within 10 minutes.")); + } +} diff --git a/forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/MCMapperMod.java b/forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/MCMapperMod.java index 6fb416c..ce37aae 100644 --- a/forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/MCMapperMod.java +++ b/forge-1_7_10/src/main/java/com/octoturge/mcmapper/forge1710/MCMapperMod.java @@ -1,11 +1,216 @@ package com.octoturge.mcmapper.forge1710; +import com.octoturge.mcmapper.common.ChunkAdapter; +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 cpw.mods.fml.common.Mod; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.common.event.FMLServerStartingEvent; +import cpw.mods.fml.common.event.FMLServerStoppingEvent; +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.common.gameevent.TickEvent; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.MathHelper; +import net.minecraft.world.WorldServer; +import net.minecraftforge.common.DimensionManager; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.common.config.Configuration; +import net.minecraftforge.event.ServerChatEvent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + /** - * Stub for the 1.7.10 leaf (Phase 9 — higher priority than 26.1.2, ships after 1.12.2 proves - * the architecture). Not wired to the Forge {@code @Mod} annotation yet since this leaf's - * ForgeGradle 2.1 toolchain isn't active in the root build — see build.gradle. + * Entry point for the 1.7.10 leaf (Phase 9) — one Forge/MC generation older than {@code + * forge-1_12_2} (raw block id + metadata, {@code cpw.mods.fml} packages, no {@code IBlockState}). + * Implements the same feature set the 1.12.2 leaf proved (Phases 1-2-3-7-7b: WS connect, column + + * section backfill/flush, chat bridge, {@code /mcmapper link}, reconciliation sweep, throttled + * player positions) against the {@code common} interfaces — see {@link MCMapperMod}'s 1.12.2 + * counterpart for the shared design rationale, and {@link Forge1710ChunkAdapter} for what + * actually differs API-wise. */ +@Mod(modid = MCMapperMod.MOD_ID, name = "MCMapper", version = MCMapperMod.VERSION) public class MCMapperMod { public static final String MOD_ID = "mcmapper"; public static final String VERSION = "0.1.0-SNAPSHOT"; + + private static final Logger LOGGER = LogManager.getLogger(MOD_ID); + + private Configuration forgeConfig; + private final MapperConfig config = new MapperConfig(); + private DefaultBackendConnection connection; + private Forge1710ChunkAdapter adapter; + 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) { + forgeConfig = new Configuration(event.getSuggestedConfigurationFile()); + loadConfig(); + } + + private void loadConfig() { + forgeConfig.load(); + config.backendUrl = forgeConfig.getString("backendUrl", "network", config.backendUrl, + "WS URL of the MCMapper backend api service"); + config.serverToken = forgeConfig.getString("serverToken", "network", config.serverToken, + "Per-server token issued when registering with the backend (see MCMapper-Backend's `bun run seed`)"); + config.deltaFlushIntervalTicks = forgeConfig.getInt("deltaFlushIntervalTicks", "network", + config.deltaFlushIntervalTicks, 1, 20 * 60, + "How often (in ticks) to batch and flush block-change deltas to the backend"); + config.reconciliationIntervalTicks = forgeConfig.getInt("reconciliationIntervalTicks", "network", + config.reconciliationIntervalTicks, 20, 20 * 60 * 60, + "How often (in ticks) to run a reconciliation sweep, re-reading and resending a " + + "rotating slice of loaded chunks to catch changes event hooks miss " + + "(world-gen, other mods, /fill, etc.)"); + 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(); + } + + @Mod.EventHandler + public void serverStarting(FMLServerStartingEvent event) { + if (config.serverToken == null || config.serverToken.isEmpty()) { + LOGGER.warn("MCMapper serverToken is not configured (see config/mcmapper.cfg) — not connecting to backend"); + return; + } + + mcServer = event.getServer(); + connection = new DefaultBackendConnection(LOGGER::info, LOGGER::warn); + connection.connect(config.backendUrl, config.serverToken); + LOGGER.info("MCMapper (1.7.10 leaf) connecting to " + config.backendUrl); + + WorldServer overworld = DimensionManager.getWorld(0); + adapter = new Forge1710ChunkAdapter(overworld); + adapter.registerEventHooks(new ChunkAdapter.DeltaSink() { + @Override + public void onDelta(DeltaEvent delta) { + pendingDeltas.add(delta); + } + + @Override + public void onChunkDirty(int chunkX, int chunkZ) { + pendingSectionChunks.add((((long) chunkX) << 32) | (chunkZ & 0xFFFFFFFFL)); + } + }); + + Forge1710ChatBridge chatBridge = new Forge1710ChatBridge(event.getServer(), LOGGER); + connection.setChatListener(chatBridge::injectWebChatMessage); + connection.setWaypointShareListener(chatBridge::injectWaypointShare); + event.registerServerCommand(new LinkCommand(connection)); + + MinecraftForge.EVENT_BUS.register(this); + } + + @SubscribeEvent + public void onServerChat(ServerChatEvent event) { + EntityPlayerMP player = (EntityPlayerMP) event.player; + connection.sendChatMessage(player.getUniqueID().toString(), player.getGameProfile().getName(), event.message); + } + + @SubscribeEvent + public void onServerTick(TickEvent.ServerTickEvent event) { + if (event.phase != TickEvent.Phase.END) return; + + if (++ticksSinceFlush >= config.deltaFlushIntervalTicks) { + ticksSinceFlush = 0; + flush(); + } + + if (++ticksSinceReconciliation >= config.reconciliationIntervalTicks) { + ticksSinceReconciliation = 0; + reconcile(); + } + + if (config.playerTrackingEnabled && ++ticksSincePlayerPositions >= config.playerPositionIntervalTicks) { + ticksSincePlayerPositions = 0; + sendPlayerPositions(); + } + } + + private void sendPlayerPositions() { + List players = new ArrayList(); + for (Object obj : mcServer.getConfigurationManager().playerEntityList) { + EntityPlayerMP player = (EntityPlayerMP) obj; + if (player.dimension != 0) continue; + players.add(new PlayerPosition(player.getUniqueID().toString(), player.getGameProfile().getName(), + MathHelper.floor_double(player.posX), MathHelper.floor_double(player.posY), + MathHelper.floor_double(player.posZ))); + } + connection.sendPlayerPositions(0, players); + } + + private void reconcile() { + List chunkKeys = reconciliationScheduler.next(adapter.loadedChunkKeys(), + config.reconciliationChunksPerSweep); + if (chunkKeys.isEmpty()) return; + + String dimensionId = adapter.getDimensionId(); + for (long key : chunkKeys) { + int chunkX = (int) (key >> 32); + int chunkZ = (int) key; + List columns = adapter.readChunk(dimensionId, chunkX, chunkZ); + if (!columns.isEmpty()) connection.sendDeltas(columns); + List sections = adapter.readSections(chunkX, chunkZ); + if (!sections.isEmpty()) connection.sendSections(dimensionId, chunkX, chunkZ, sections); + } + } + + private void flush() { + List batch; + synchronized (pendingDeltas) { + if (pendingDeltas.isEmpty()) { + batch = null; + } else { + batch = new ArrayList(pendingDeltas); + pendingDeltas.clear(); + } + } + if (batch != null) connection.sendDeltas(batch); + + List dirtyChunks; + synchronized (pendingSectionChunks) { + if (pendingSectionChunks.isEmpty()) return; + dirtyChunks = new ArrayList(pendingSectionChunks); + pendingSectionChunks.clear(); + } + String dimensionId = adapter.getDimensionId(); + for (long key : dirtyChunks) { + int chunkX = (int) (key >> 32); + int chunkZ = (int) key; + List sections = adapter.readSections(chunkX, chunkZ); + if (!sections.isEmpty()) { + connection.sendSections(dimensionId, chunkX, chunkZ, sections); + } + } + } + + @Mod.EventHandler + public void serverStopping(FMLServerStoppingEvent event) { + if (connection != null) connection.disconnect(); + } }