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.
This commit is contained in:
2026-08-09 22:13:45 +02:00
parent 890ce46c4f
commit 0d8e670cd8
5 changed files with 482 additions and 8 deletions
+12 -5
View File
@@ -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 - `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 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`). 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 - `forge-1_7_10/` — MC 1.7.10 Forge (Phase 9). Second priority, fully wired against the same
fork family, one Forge-tooling generation further back. `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, - `neoforge-26_1/` — MC 26.1.2 (NeoForge, assumed). Lowest MVP priority. Structurally scaffolded,
not yet wired into the default build (see `settings.gradle`). 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 within one Gradle invocation; see `settings.gradle`'s comment for the workaround until Phase 10
gives it a proper isolated build. 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: 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`, 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 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 — 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 other dimensions and `neoforge-26_1` land in a later phase. The WS client and JSON encoding are
JSON encoding are hand-rolled (no third-party dependency) — see hand-rolled (no third-party dependency) — see
`common/src/main/java/.../ws/SimpleWebSocketClient.java` and `.../json/MiniJson.java` for why. `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) ### Player position tracking (Phase 7b)
@@ -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)));
}
}
@@ -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<DeltaEvent> readChunk(String dimension, int chunkX, int chunkZ) {
Chunk chunk = world.getChunkFromChunkCoords(chunkX, chunkZ);
List<DeltaEvent> events = new ArrayList<DeltaEvent>(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<SectionData> readSections(int chunkX, int chunkZ) {
Chunk chunk = world.getChunkFromChunkCoords(chunkX, chunkZ);
ExtendedBlockStorage[] storage = chunk.getBlockStorageArray();
List<SectionData> sections = new ArrayList<SectionData>();
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<Long> loadedChunkKeys() {
List<Long> keys = new ArrayList<Long>();
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<Long> 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<Long> dirtyChunks = new LinkedHashSet<Long>();
Iterator<Long> 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);
}
}
}
@@ -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."));
}
}
@@ -1,11 +1,216 @@
package com.octoturge.mcmapper.forge1710; 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 * Entry point for the 1.7.10 leaf (Phase 9)one Forge/MC generation older than {@code
* the architecture). Not wired to the Forge {@code @Mod} annotation yet since this leaf's * forge-1_12_2} (raw block id + metadata, {@code cpw.mods.fml} packages, no {@code IBlockState}).
* ForgeGradle 2.1 toolchain isn't active in the root build — see build.gradle. * 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 class MCMapperMod {
public static final String MOD_ID = "mcmapper"; public static final String MOD_ID = "mcmapper";
public static final String VERSION = "0.1.0-SNAPSHOT"; 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<DeltaEvent> pendingDeltas = Collections.synchronizedList(new ArrayList<DeltaEvent>());
private final Set<Long> pendingSectionChunks = Collections.synchronizedSet(new LinkedHashSet<Long>());
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<PlayerPosition> players = new ArrayList<PlayerPosition>();
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<Long> 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<DeltaEvent> columns = adapter.readChunk(dimensionId, chunkX, chunkZ);
if (!columns.isEmpty()) connection.sendDeltas(columns);
List<SectionData> sections = adapter.readSections(chunkX, chunkZ);
if (!sections.isEmpty()) connection.sendSections(dimensionId, chunkX, chunkZ, sections);
}
}
private void flush() {
List<DeltaEvent> batch;
synchronized (pendingDeltas) {
if (pendingDeltas.isEmpty()) {
batch = null;
} else {
batch = new ArrayList<DeltaEvent>(pendingDeltas);
pendingDeltas.clear();
}
}
if (batch != null) connection.sendDeltas(batch);
List<Long> dirtyChunks;
synchronized (pendingSectionChunks) {
if (pendingSectionChunks.isEmpty()) return;
dirtyChunks = new ArrayList<Long>(pendingSectionChunks);
pendingSectionChunks.clear();
}
String dimensionId = adapter.getDimensionId();
for (long key : dirtyChunks) {
int chunkX = (int) (key >> 32);
int chunkZ = (int) key;
List<SectionData> 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();
}
} }