Skip to content

Commit 07643b4

Browse files
committed
feat(fake-chunks): add configurable chunk generation and invalidation events
1 parent 81c0348 commit 07643b4

File tree

5 files changed

+161
-63
lines changed

5 files changed

+161
-63
lines changed

src/main/java/me/mapacheee/extendedhorizons/chunk/FakeChunkDispatchService.java

Lines changed: 72 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.Set;
88
import java.util.UUID;
99
import java.util.concurrent.CompletableFuture;
10+
import java.util.concurrent.ConcurrentHashMap;
1011
import java.util.concurrent.atomic.AtomicBoolean;
1112
import java.util.concurrent.atomic.AtomicInteger;
1213
import me.mapacheee.extendedhorizons.chunk.cache.ChunkPacketCacheService;
@@ -60,6 +61,10 @@ private record ChunkSendRequest(
6061
private final ChunkPacketCacheService chunkPacketCacheService;
6162
private final FakeChunkRefreshCoordinator refreshCoordinator;
6263
private final State state;
64+
private final Map<ChunkPacketCacheService.ChunkKey, CompletableFuture<ClientboundLevelChunkWithLightPacket>>
65+
packetBuildInFlight = new ConcurrentHashMap<>();
66+
private final Map<ChunkPacketCacheService.ChunkKey, Long> unavailableUntilMs =
67+
new ConcurrentHashMap<>();
6368

6469
public FakeChunkDispatchService(
6570
Container<Config> configContainer,
@@ -137,10 +142,20 @@ private CompletableFuture<Boolean> sendFakeChunk(ChunkSendRequest request, Hooks
137142
if (!hooks.isSessionValid(player, expectedWorldId, expectedEpoch))
138143
return CompletableFuture.completedFuture(false);
139144
long chunkKey = ChunkPos.asLong(x, z);
145+
ChunkPacketCacheService.ChunkKey cacheKey =
146+
new ChunkPacketCacheService.ChunkKey(expectedWorldId, chunkKey);
147+
Long blockedUntil = unavailableUntilMs.get(cacheKey);
148+
long now = System.currentTimeMillis();
149+
if (blockedUntil != null && blockedUntil > now) {
150+
hooks.inc(player.getUniqueId(), "chunk_unavailable_skip");
151+
hooks.recordChunkLatency(startedNs);
152+
return CompletableFuture.completedFuture(false);
153+
}
140154
if (!chunkPacketCacheService.shouldBypass(expectedWorldId, chunkKey)) {
141155
ClientboundLevelChunkWithLightPacket cached =
142156
chunkPacketCacheService.get(expectedWorldId, chunkKey);
143157
if (cached != null) {
158+
unavailableUntilMs.remove(cacheKey);
144159
hooks.sendPacketForSession(player, cached, expectedWorldId, expectedEpoch);
145160
if (hooks.isSessionValid(player, expectedWorldId, expectedEpoch)) {
146161
tracker.markChunkSent(x, z);
@@ -151,34 +166,69 @@ private CompletableFuture<Boolean> sendFakeChunk(ChunkSendRequest request, Hooks
151166
}
152167
}
153168
}
154-
return world
155-
.getChunkAtAsync(x, z, true)
156-
.thenCompose(
157-
chunk -> {
169+
return getOrBuildPacket(world, expectedWorldId, x, z, chunkKey, hooks)
170+
.thenApply(
171+
packet -> {
172+
if (packet == null) {
173+
unavailableUntilMs.put(cacheKey, System.currentTimeMillis() + 2000L);
174+
return false;
175+
}
176+
unavailableUntilMs.remove(cacheKey);
177+
if (!hooks.isSessionValid(player, expectedWorldId, expectedEpoch)) return false;
178+
hooks.sendPacketForSession(player, packet, expectedWorldId, expectedEpoch);
158179
if (!hooks.isSessionValid(player, expectedWorldId, expectedEpoch)) {
159-
return CompletableFuture.completedFuture(false);
180+
return false;
160181
}
182+
tracker.markChunkSent(x, z);
183+
refreshCoordinator.addSubscription(player.getUniqueId(), expectedWorldId, chunkKey);
184+
hooks.inc(player.getUniqueId(), "fake_sent");
185+
return true;
186+
})
187+
.exceptionally(
188+
e -> {
189+
state
190+
.logger()
191+
.error("Failed to send fake chunk {},{} to {}", x, z, player.getName(), e);
192+
return false;
193+
})
194+
.whenComplete((ok, err) -> hooks.recordChunkLatency(startedNs));
195+
}
196+
197+
private CompletableFuture<ClientboundLevelChunkWithLightPacket> getOrBuildPacket(
198+
World world, UUID expectedWorldId, int x, int z, long chunkKey, Hooks hooks) {
199+
if (world == null || expectedWorldId == null || hooks == null) {
200+
return CompletableFuture.completedFuture(null);
201+
}
202+
ChunkPacketCacheService.ChunkKey key = new ChunkPacketCacheService.ChunkKey(expectedWorldId, chunkKey);
203+
CompletableFuture<ClientboundLevelChunkWithLightPacket> existing = packetBuildInFlight.get(key);
204+
if (existing != null) return existing;
205+
206+
CompletableFuture<ClientboundLevelChunkWithLightPacket> promise = new CompletableFuture<>();
207+
CompletableFuture<ClientboundLevelChunkWithLightPacket> raced = packetBuildInFlight.putIfAbsent(key, promise);
208+
if (raced != null) return raced;
209+
210+
world
211+
.getChunkAtAsync(x, z, config().fakeChunksGenerateMissingChunks())
212+
.thenAccept(
213+
chunk -> {
161214
if (chunk == null) {
162-
hooks.inc(player.getUniqueId(), "chunk_async_null");
163-
return CompletableFuture.completedFuture(false);
215+
promise.complete(null);
216+
return;
164217
}
165-
CompletableFuture<Boolean> future = new CompletableFuture<>();
166218
boolean scheduled =
167219
hooks.runAtChunk(
168220
world,
169221
x,
170222
z,
171223
() -> {
172-
if (!state.enabled().get()
173-
|| !hooks.isSessionValid(player, expectedWorldId, expectedEpoch)) {
174-
future.complete(false);
224+
if (!state.enabled().get()) {
225+
promise.complete(null);
175226
return;
176227
}
177228
try {
178229
ChunkAccess access = ((CraftChunk) chunk).getHandle(ChunkStatus.FULL);
179230
if (!(access instanceof LevelChunk nmsChunk)) {
180-
hooks.inc(player.getUniqueId(), "chunk_not_full");
181-
future.complete(false);
231+
promise.complete(null);
182232
return;
183233
}
184234
LevelLightEngine lightEngine = nmsChunk.getLevel().getLightEngine();
@@ -187,42 +237,23 @@ private CompletableFuture<Boolean> sendFakeChunk(ChunkSendRequest request, Hooks
187237
new ClientboundLevelChunkWithLightPacket(
188238
nmsChunk, lightEngine, lightMasks[0], lightMasks[1], true);
189239
chunkPacketCacheService.put(expectedWorldId, chunkKey, packet);
190-
hooks.sendPacketForSession(
191-
player, packet, expectedWorldId, expectedEpoch);
192-
if (!hooks.isSessionValid(player, expectedWorldId, expectedEpoch)) {
193-
future.complete(false);
194-
return;
195-
}
196-
tracker.markChunkSent(x, z);
197-
refreshCoordinator.addSubscription(
198-
player.getUniqueId(), expectedWorldId, chunkKey);
199-
hooks.inc(player.getUniqueId(), "fake_sent");
200-
future.complete(true);
240+
promise.complete(packet);
201241
} catch (Throwable t) {
202-
state
203-
.logger()
204-
.error(
205-
"Failed live-chunk packet {},{} for {}",
206-
x,
207-
z,
208-
player.getName(),
209-
t);
210-
future.complete(false);
242+
promise.complete(null);
211243
}
212244
});
213245
if (!scheduled) {
214-
future.complete(false);
246+
promise.complete(null);
215247
}
216-
return future;
217248
})
218249
.exceptionally(
219250
e -> {
220-
state
221-
.logger()
222-
.error("Failed to send fake chunk {},{} to {}", x, z, player.getName(), e);
223-
return false;
224-
})
225-
.whenComplete((ok, err) -> hooks.recordChunkLatency(startedNs));
251+
promise.complete(null);
252+
return null;
253+
});
254+
255+
promise.whenComplete((packet, err) -> packetBuildInFlight.remove(key, promise));
256+
return promise;
226257
}
227258

228259
private BitSet[] getLightMasks(LevelChunk chunk) {

src/main/java/me/mapacheee/extendedhorizons/chunk/FakeChunkService.java

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,20 @@ public void onEnable() {
130130
World world = player.getWorld();
131131
lastKnownWorldId.putIfAbsent(playerId, world.getUID());
132132

133-
sendPacket(
134-
player,
135-
new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ));
136-
137133
PlayerChunkTracker tracker = trackers.get(playerId);
138134
if (tracker != null) {
139135
long now = System.currentTimeMillis();
140136
Long lastPlan = lastForcedPlanMs.get(playerId);
141-
if (lastPlan == null
142-
|| now - lastPlan >= config().forcePlanIntervalMs()) {
137+
Deque<Long> queue = pendingQueues.get(playerId);
138+
AtomicInteger inflight =
139+
inflightCounts.computeIfAbsent(playerId, k -> new AtomicInteger(0));
140+
boolean needsRecoveryPlan =
141+
tracker.getSentChunks().isEmpty()
142+
|| (queue != null && !queue.isEmpty())
143+
|| inflight.get() > 0;
144+
if (needsRecoveryPlan
145+
&& (lastPlan == null
146+
|| now - lastPlan >= config().forcePlanIntervalMs())) {
143147
updatePlayerChunks(
144148
player, playerId, world, chunkX, chunkZ, true);
145149
lastForcedPlanMs.put(playerId, now);
@@ -335,28 +339,47 @@ public void applyDistancePreference(Player player, int distance) {
335339

336340
public void handleMove(Player player) {
337341
if (!enabled.get()) return;
342+
if (player == null || !player.isOnline()) return;
343+
UUID playerId = player.getUniqueId();
344+
World world = player.getWorld();
345+
if (world == null) return;
346+
int chunkX = player.getLocation().getBlockX() >> 4;
347+
int chunkZ = player.getLocation().getBlockZ() >> 4;
348+
UUID worldId = world.getUID();
349+
UUID lastWorldId = lastKnownWorldId.get(playerId);
350+
PlayerChunkTracker fastTracker = trackers.get(playerId);
351+
if (fastTracker != null
352+
&& lastWorldId != null
353+
&& lastWorldId.equals(worldId)
354+
&& !fastTracker.hasMovedChunk(chunkX, chunkZ)) {
355+
return;
356+
}
338357
runForPlayer(
339358
player,
340359
() -> {
341360
if (!enabled.get()) return;
342361
if (!player.isOnline()) return;
343362

344-
int chunkX = player.getLocation().getBlockX() >> 4;
345-
int chunkZ = player.getLocation().getBlockZ() >> 4;
346-
World world = player.getWorld();
347-
UUID playerId = player.getUniqueId();
348-
UUID worldId = world.getUID();
349-
UUID lastWorldId = lastKnownWorldId.get(playerId);
350-
if (lastWorldId == null || !lastWorldId.equals(worldId)) {
363+
int currentChunkX = player.getLocation().getBlockX() >> 4;
364+
int currentChunkZ = player.getLocation().getBlockZ() >> 4;
365+
World currentWorld = player.getWorld();
366+
UUID currentWorldId = currentWorld.getUID();
367+
UUID knownWorldId = lastKnownWorldId.get(playerId);
368+
boolean worldChanged = knownWorldId == null || !knownWorldId.equals(currentWorldId);
369+
if (worldChanged) {
351370
resetPlayerState(playerId, true);
352-
lastKnownWorldId.put(playerId, worldId);
353-
debug(playerId, "world_change", "[EH] world change detected to " + world.getName());
371+
lastKnownWorldId.put(playerId, currentWorldId);
372+
debug(playerId, "world_change", "[EH] world change detected to " + currentWorld.getName());
354373
}
355374

356-
sendPacket(player, new ClientboundSetChunkCacheCenterPacket(chunkX, chunkZ));
375+
PlayerChunkTracker tracker = trackers.get(playerId);
376+
boolean movedChunk = tracker == null || tracker.hasMovedChunk(currentChunkX, currentChunkZ);
377+
if (movedChunk) {
378+
sendPacket(player, new ClientboundSetChunkCacheCenterPacket(currentChunkX, currentChunkZ));
379+
}
357380

358381
try {
359-
updatePlayerChunks(player, playerId, world, chunkX, chunkZ, false);
382+
updatePlayerChunks(player, playerId, currentWorld, currentChunkX, currentChunkZ, false);
360383
} catch (Throwable t) {
361384
LOGGER.error("updatePlayerChunks failed for {}", player.getName(), t);
362385
}

src/main/java/me/mapacheee/extendedhorizons/chunk/listener/ChunkCacheInvalidationListener.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package me.mapacheee.extendedhorizons.chunk.listener;
22

33
import com.google.inject.Inject;
4+
import com.thewinterframework.configurate.Container;
45
import com.thewinterframework.paper.listener.ListenerComponent;
56
import me.mapacheee.extendedhorizons.chunk.FakeChunkService;
67
import me.mapacheee.extendedhorizons.chunk.cache.ChunkPacketCacheService;
8+
import me.mapacheee.extendedhorizons.config.Config;
79
import org.bukkit.block.Block;
810
import org.bukkit.entity.Player;
911
import org.bukkit.event.EventHandler;
@@ -33,12 +35,16 @@ public class ChunkCacheInvalidationListener implements Listener {
3335

3436
private final ChunkPacketCacheService chunkPacketCacheService;
3537
private final FakeChunkService fakeChunkService;
38+
private final Container<Config> configContainer;
3639

3740
@Inject
3841
public ChunkCacheInvalidationListener(
39-
ChunkPacketCacheService chunkPacketCacheService, FakeChunkService fakeChunkService) {
42+
ChunkPacketCacheService chunkPacketCacheService,
43+
FakeChunkService fakeChunkService,
44+
Container<Config> configContainer) {
4045
this.chunkPacketCacheService = chunkPacketCacheService;
4146
this.fakeChunkService = fakeChunkService;
47+
this.configContainer = configContainer;
4248
}
4349

4450
@EventHandler
@@ -110,17 +116,20 @@ public void onIgnite(BlockIgniteEvent event) {
110116

111117
@EventHandler
112118
public void onPhysics(BlockPhysicsEvent event) {
119+
if (!config().autoRefreshInvalidateOnPhysics()) return;
113120
invalidate(event.getBlock());
114121
}
115122

116123
@EventHandler
117124
public void onFlow(BlockFromToEvent event) {
125+
if (!config().autoRefreshInvalidateOnFlow()) return;
118126
invalidate(event.getBlock());
119127
invalidate(event.getToBlock());
120128
}
121129

122130
@EventHandler
123131
public void onPlayerInteract(PlayerInteractEvent event) {
132+
if (!config().autoRefreshInvalidateOnPlayerInteract()) return;
124133
invalidate(event.getClickedBlock());
125134
}
126135

@@ -159,4 +168,9 @@ private void invalidate(Block block) {
159168
int chunkZ = block.getZ() >> 4;
160169
fakeChunkService.handleRealChunkInteraction(block.getWorld(), chunkX, chunkZ);
161170
}
171+
172+
private Config config() {
173+
Config cfg = configContainer.get();
174+
return cfg == null ? Config.empty() : cfg;
175+
}
162176
}

src/main/java/me/mapacheee/extendedhorizons/config/Config.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ public int maxInflightPerPlayer() {
5555
return fakeChunks == null ? 16 : fakeChunks.maxInflightPerPlayer();
5656
}
5757

58+
public boolean fakeChunksGenerateMissingChunks() {
59+
if (fakeChunks == null) return false;
60+
return fakeChunks.generateMissingChunks();
61+
}
62+
5863
public long forcePlanIntervalMs() {
5964
return fakeChunks == null ? 3000L : fakeChunks.forcePlanIntervalMs();
6065
}
@@ -128,6 +133,21 @@ public long autoRefreshMinInvalidateIntervalMs() {
128133
return Math.max(0L, fakeChunks.liveRefresh().minInvalidateIntervalMs());
129134
}
130135

136+
public boolean autoRefreshInvalidateOnPhysics() {
137+
if (fakeChunks == null || fakeChunks.liveRefresh() == null) return false;
138+
return fakeChunks.liveRefresh().invalidateOnPhysics();
139+
}
140+
141+
public boolean autoRefreshInvalidateOnFlow() {
142+
if (fakeChunks == null || fakeChunks.liveRefresh() == null) return false;
143+
return fakeChunks.liveRefresh().invalidateOnFlow();
144+
}
145+
146+
public boolean autoRefreshInvalidateOnPlayerInteract() {
147+
if (fakeChunks == null || fakeChunks.liveRefresh() == null) return false;
148+
return fakeChunks.liveRefresh().invalidateOnPlayerInteract();
149+
}
150+
131151
public int interceptorMaxTargetDistance() {
132152
return packetInterceptor == null ? 32 : packetInterceptor.maxTargetDistance();
133153
}
@@ -214,6 +234,7 @@ public record FakeChunksConfig(
214234
@Setting("target-view-distance") int targetViewDistance,
215235
@Setting("max-send-per-cycle") int maxSendPerCycle,
216236
@Setting("max-inflight-per-player") int maxInflightPerPlayer,
237+
@Setting("generate-missing-chunks") boolean generateMissingChunks,
217238
@Setting("force-plan-interval-ms") long forcePlanIntervalMs,
218239
KeepAliveConfig keepalive,
219240
WarmupConfig warmup,
@@ -247,7 +268,10 @@ public record LiveRefreshConfig(
247268
@Setting("chunks-per-cycle") int chunksPerCycle,
248269
@Setting("invalidate-fallback-enabled") boolean invalidateFallbackEnabled,
249270
@Setting("invalidate-fallback-max-per-cycle") int invalidateFallbackMaxPerCycle,
250-
@Setting("min-invalidate-interval-ms") long minInvalidateIntervalMs
271+
@Setting("min-invalidate-interval-ms") long minInvalidateIntervalMs,
272+
@Setting("invalidate-on-physics") boolean invalidateOnPhysics,
273+
@Setting("invalidate-on-flow") boolean invalidateOnFlow,
274+
@Setting("invalidate-on-player-interact") boolean invalidateOnPlayerInteract
251275
) {}
252276
}
253277

0 commit comments

Comments
 (0)