Add periodic reconciliation sweep (Phase 7)
Adds a rotating, non-overlapping-batch ReconciliationScheduler (common/, loader-agnostic) and wires it into the 1.12.2 leaf: a new tick timer (reconciliationIntervalTicks, mirroring the existing delta-flush timer) periodically re-reads and resends a bounded slice of currently-loaded chunks, catching mutations that never fire a block event (world-gen, other mods writing blocks directly, /fill, etc.). ChunkAdapter gains loadedChunkKeys(); Forge1122ChunkAdapter implements it via ChunkProviderServer.getLoadedChunks(). MapperConfig gains reconciliationChunksPerSweep to bound sweep cost. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tKdPZt78zbPUZMXWzKEKt
This commit is contained in:
+10
@@ -10,6 +10,7 @@ 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.common.util.BlockSnapshot;
|
||||
import net.minecraftforge.event.world.BlockEvent;
|
||||
@@ -101,6 +102,15 @@ public class Forge1122ChunkAdapter implements ChunkAdapter {
|
||||
MinecraftForge.EVENT_BUS.register(new EventHooks(sink));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> loadedChunkKeys() {
|
||||
List<Long> keys = new ArrayList<>();
|
||||
for (Chunk chunk : ((ChunkProviderServer) world.getChunkProvider()).getLoadedChunks()) {
|
||||
keys.add((((long) chunk.x) << 32) | (chunk.z & 0xFFFFFFFFL));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code BlockEvent.BreakEvent} fires *before* the block is actually removed (it's
|
||||
* cancellable), so reading world state synchronously inside that handler would see the
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.octoturge.mcmapper.forge1122;
|
||||
|
||||
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.SectionData;
|
||||
@@ -28,9 +29,11 @@ import java.util.Set;
|
||||
/**
|
||||
* Entry point for the 1.12.2 leaf — the primary/first-implemented target (Enigmatica 2).
|
||||
* Phase 1: connects to the backend over WS, backfills the overworld's loaded chunks, and
|
||||
* flushes event-driven column deltas on a timer (no periodic reconciliation sweep yet — see
|
||||
* plan's Phase 7). Phase 2 adds full-section backfill/flush for 3D mesh rendering. Phase 3 adds
|
||||
* `/mcmapper link` and the two-way chat bridge (see LinkCommand, Forge1122ChatBridge).
|
||||
* flushes event-driven column deltas on a timer. Phase 2 adds full-section backfill/flush for
|
||||
* 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.
|
||||
*/
|
||||
@Mod(modid = MCMapperMod.MOD_ID, name = "MCMapper", version = MCMapperMod.VERSION)
|
||||
public class MCMapperMod {
|
||||
@@ -45,7 +48,9 @@ public class MCMapperMod {
|
||||
private Forge1122ChunkAdapter adapter;
|
||||
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 int ticksSinceFlush = 0;
|
||||
private int ticksSinceReconciliation = 0;
|
||||
|
||||
@Mod.EventHandler
|
||||
public void preInit(FMLPreInitializationEvent event) {
|
||||
@@ -62,6 +67,14 @@ public class MCMapperMod {
|
||||
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");
|
||||
if (forgeConfig.hasChanged()) forgeConfig.save();
|
||||
}
|
||||
|
||||
@@ -107,9 +120,32 @@ public class MCMapperMod {
|
||||
@SubscribeEvent
|
||||
public void onServerTick(TickEvent.ServerTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
if (++ticksSinceFlush < config.deltaFlushIntervalTicks) return;
|
||||
ticksSinceFlush = 0;
|
||||
flush();
|
||||
|
||||
if (++ticksSinceFlush >= config.deltaFlushIntervalTicks) {
|
||||
ticksSinceFlush = 0;
|
||||
flush();
|
||||
}
|
||||
|
||||
if (++ticksSinceReconciliation >= config.reconciliationIntervalTicks) {
|
||||
ticksSinceReconciliation = 0;
|
||||
reconcile();
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user