Skip to content

Commit 61beb65

Browse files
committed
fix: sublevel not rendering correctly after transit
1 parent eb4e808 commit 61beb65

6 files changed

Lines changed: 114 additions & 18 deletions

File tree

common/src/main/java/dev/ryanhcode/sable/api/sublevel/ServerSubLevelContainer.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,40 @@ public <T> boolean addForceLoadTicket(final ServerSubLevel subLevel, final SubLe
238238
return false;
239239
}
240240

241+
/**
242+
* Adds a force-loading ticket that only lasts for the current server session and is not written to saved data.
243+
*/
244+
public <T> boolean addTransientForceLoadTicket(
245+
final ServerSubLevel subLevel,
246+
final SubLevelLoadingTicketType<T> ticketType,
247+
final T key) {
248+
final SubLevelLoadingTicket<T> ticket = new SubLevelLoadingTicket<>(
249+
ticketType, subLevel.getUniqueId(), key);
250+
return this.activeTickets
251+
.computeIfAbsent(subLevel, ignored -> new ObjectArraySet<>())
252+
.add(ticket);
253+
}
254+
255+
/**
256+
* Removes a force-loading ticket previously added with {@link #addTransientForceLoadTicket}.
257+
*/
258+
public <T> boolean removeTransientForceLoadTicket(
259+
final ServerSubLevel subLevel,
260+
final SubLevelLoadingTicketType<T> ticketType,
261+
final T key) {
262+
final ObjectSet<SubLevelLoadingTicket<?>> tickets = this.activeTickets.get(subLevel);
263+
if (tickets == null) {
264+
return false;
265+
}
266+
267+
final boolean removed = tickets.remove(new SubLevelLoadingTicket<>(
268+
ticketType, subLevel.getUniqueId(), key));
269+
if (tickets.isEmpty()) {
270+
this.activeTickets.remove(subLevel);
271+
}
272+
return removed;
273+
}
274+
241275
/**
242276
* Removes a sub-level force-loading ticket
243277
*

common/src/main/java/dev/ryanhcode/sable/network/packets/tcp/ClientboundStartTrackingSubLevelPacket.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ public void handle(final PacketContext context) {
6666
return;
6767
}
6868

69+
Sable.LOGGER.debug(
70+
"Receiving full sync for sub-level {}: clientDimension={}, pose={}, bounds={}, localPlot={},{}",
71+
this.subLevelID,
72+
level.dimension().location(),
73+
this.pose,
74+
this.bounds,
75+
ChunkPos.getX(this.plotCoordinate),
76+
ChunkPos.getZ(this.plotCoordinate));
77+
6978
final ClientSubLevel subLevel = (ClientSubLevel) clientContainer.allocateSubLevel(this.subLevelID, ChunkPos.getX(this.plotCoordinate), ChunkPos.getZ(this.plotCoordinate), new Pose3d(this.lastPose));
7079

7180
final SubLevelSnapshotInterpolator interpolator = subLevel.getInterpolator();
@@ -90,4 +99,4 @@ public void handle(final PacketContext context) {
9099
subLevel.setName(this.name);
91100
}
92101
}
93-
}
102+
}

common/src/main/java/dev/ryanhcode/sable/sublevel/plot/ServerLevelPlot.java

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,29 @@ public CompoundTag save() {
420420
return tag;
421421
}
422422

423+
/**
424+
* Returns the block-space offset applied when serialized plot contents are loaded here.
425+
*/
426+
public BlockPos getRelocationOffset(final CompoundTag tag) {
427+
final ServerLevel level = this.getSubLevel().getLevel();
428+
final int sourceOriginX = tag.contains("plot_grid_origin_x")
429+
? tag.getInt("plot_grid_origin_x")
430+
: this.container.getOrigin().x;
431+
final int sourceOriginZ = tag.contains("plot_grid_origin_z")
432+
? tag.getInt("plot_grid_origin_z")
433+
: this.container.getOrigin().y;
434+
final int sourcePlotX = sourceOriginX + tag.getInt("plot_x");
435+
final int sourcePlotZ = sourceOriginZ + tag.getInt("plot_z");
436+
final int sourceCenterY = tag.contains("plot_center_y")
437+
? tag.getInt("plot_center_y")
438+
: this.getCenterBlock().getY();
439+
440+
return new BlockPos(
441+
(this.plotPos.x - sourcePlotX) << (this.logSize + 4),
442+
this.getCenterBlock().getY() - sourceCenterY,
443+
(this.plotPos.z - sourcePlotZ) << (this.logSize + 4));
444+
}
445+
423446
/**
424447
* Deserializes a plot from an NBT tag
425448
*/
@@ -436,23 +459,13 @@ public void load(final CompoundTag tag) {
436459

437460
final ServerSubLevel subLevel = this.getSubLevel();
438461
final ServerLevel level = subLevel.getLevel();
439-
final int sourceOriginX = tag.contains("plot_grid_origin_x")
440-
? tag.getInt("plot_grid_origin_x")
441-
: this.container.getOrigin().x;
442-
final int sourceOriginZ = tag.contains("plot_grid_origin_z")
443-
? tag.getInt("plot_grid_origin_z")
444-
: this.container.getOrigin().y;
445-
final int sourcePlotX = sourceOriginX + tag.getInt("plot_x");
446-
final int sourcePlotZ = sourceOriginZ + tag.getInt("plot_z");
447-
final int blockOffsetX = (this.plotPos.x - sourcePlotX) << (this.logSize + 4);
448-
final int blockOffsetZ = (this.plotPos.z - sourcePlotZ) << (this.logSize + 4);
462+
final BlockPos relocationOffset = this.getRelocationOffset(tag);
463+
final int blockOffsetX = relocationOffset.getX();
464+
final int blockOffsetZ = relocationOffset.getZ();
449465
final int sourceMinSection = tag.contains("min_section_y")
450466
? tag.getInt("min_section_y")
451467
: level.getMinSection();
452-
final int sourceCenterY = tag.contains("plot_center_y")
453-
? tag.getInt("plot_center_y")
454-
: this.getCenterBlock().getY();
455-
final int blockOffsetY = this.getCenterBlock().getY() - sourceCenterY;
468+
final int blockOffsetY = relocationOffset.getY();
456469

457470
if (tag.contains("biome")) {
458471
final ResourceLocation location = ResourceLocation.tryParse(tag.getString("biome"));

common/src/main/java/dev/ryanhcode/sable/sublevel/storage/serialization/SubLevelSerializer.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import net.minecraft.nbt.ListTag;
2121
import net.minecraft.nbt.NbtUtils;
2222
import net.minecraft.nbt.Tag;
23+
import net.minecraft.core.BlockPos;
2324
import net.minecraft.server.level.ServerLevel;
2425
import org.jetbrains.annotations.NotNull;
2526
import org.jetbrains.annotations.Nullable;
@@ -196,6 +197,13 @@ private static ServerSubLevel fullyLoad(
196197
}
197198

198199
final ServerLevelPlot plot = subLevel.getPlot();
200+
if (destinationPose != null) {
201+
final BlockPos relocationOffset = plot.getRelocationOffset(plotTag);
202+
pose.rotationPoint().add(
203+
relocationOffset.getX(),
204+
relocationOffset.getY(),
205+
relocationOffset.getZ());
206+
}
199207
try {
200208
plot.load(plotTag);
201209
} catch (final RuntimeException e) {

common/src/main/java/dev/ryanhcode/sable/sublevel/system/SubLevelTrackingSystem.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,15 @@ private void sendFullSync(final ServerPlayer player, final ServerSubLevel subLev
106106
final LevelPlot plot = subLevel.getPlot();
107107

108108
final Collection<PlotChunkHolder> chunks = plot.getLoadedChunks();
109+
dev.ryanhcode.sable.Sable.LOGGER.debug(
110+
"Sending full sync for sub-level {} to player {}: dimension={}, pose={}, bounds={}, plot={}, chunks={}",
111+
subLevel.getUniqueId(),
112+
player.getGameProfile().getName(),
113+
this.level.dimension().location(),
114+
subLevel.logicalPose(),
115+
subLevel.boundingBox(),
116+
plot.plotPos,
117+
chunks.size());
109118
final ObjectList<Packet<? super ClientGamePacketListener>> packets = new ObjectArrayList<>(3 + chunks.size());
110119

111120
packets.add(new ClientboundCustomPayloadPacket(new ClientboundStartTrackingSubLevelPacket(l, subLevel.getUniqueId(), subLevel.lastPose(), subLevel.logicalPose(), plot.getBoundingBox(), subLevel.getName(), this.interpolationTick)));
@@ -136,6 +145,7 @@ private void sendRemoval(final VeilPacketManager.PacketSink sink, final ServerSu
136145

137146
@Override
138147
public void tick(final SubLevelContainer container) {
148+
final Set<UUID> fullSyncPlayers = new ObjectOpenHashSet<>();
139149
for (final SubLevel subLevel : this.additionQueue) {
140150
// If the sub-level has been removed before we could even send it to clients, skip it
141151
if (subLevel.isRemoved()) {
@@ -170,6 +180,7 @@ public void tick(final SubLevelContainer container) {
170180
}
171181

172182
this.sendFullSync(player, serverSubLevel, extraPacket);
183+
fullSyncPlayers.add(uuid);
173184
}
174185

175186
serverSubLevel.clearSplitFrom();
@@ -216,13 +227,14 @@ public void tick(final SubLevelContainer container) {
216227
if (this.shouldLoad(player, entityPos) && !tracking.contains(uuid)) {
217228
tracking.add(uuid);
218229
this.sendFullSync(player, serverSubLevel, null);
230+
fullSyncPlayers.add(uuid);
219231
}
220232
}
221233
}
222234

223235
// send positional updates separately
224236
this.sendBoundsUpdates(container);
225-
this.sendMovementUpdates(container);
237+
this.sendMovementUpdates(container, fullSyncPlayers);
226238
}
227239

228240
/**
@@ -258,7 +270,7 @@ public int getInterpolationTick() {
258270
*
259271
* @param container the sublevels to send updates for
260272
*/
261-
private void sendMovementUpdates(final SubLevelContainer container) {
273+
private void sendMovementUpdates(final SubLevelContainer container, final Set<UUID> fullSyncPlayers) {
262274
// we want to batch updates we send to players, so we'll collect them here
263275
final Map<UUID, List<SubLevelUpdateTicket>> movementUpdates = new Object2ObjectOpenHashMap<>();
264276

@@ -363,7 +375,9 @@ private void sendMovementUpdates(final SubLevelContainer container) {
363375
final int maxBatchSize = 16;
364376

365377
final SableUDPServer udpServer = SableUDPServer.getServer(this.level.getServer());
366-
if (udpServer != null && udpServer.isConnectedTo(player)) {
378+
// Keep the first snapshot ordered after the TCP full-sync bundle. UDP, especially the local
379+
// singleplayer event loop, can otherwise deliver movement before the client creates the sub-level.
380+
if (udpServer != null && udpServer.isConnectedTo(player) && !fullSyncPlayers.contains(uuid)) {
367381
final Iterator<ClientboundSableSnapshotDualPacket.Entry> iter = entries.iterator();
368382

369383
udpServer.sendUDPPacket(player, new ClientboundSableSnapshotInfoDualPacket(msSinceLastSend, this.interpolationTick, false), true);

neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/SubLevelTransferTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public static void transfersAcrossDimensions(final GameTestHelper helper) {
5050
final Vector3d sourcePosition = new Vector3d(sourceCenter.x, sourceCenter.y, sourceCenter.z);
5151
final ServerSubLevel source = spawnSingleBlockSubLevel(
5252
sourceContainer, sourcePosition, Blocks.DIAMOND_BLOCK.defaultBlockState());
53+
final ServerSubLevel destinationOccupant = spawnSingleBlockSubLevel(
54+
destinationContainer, new Vector3d(0.5, 80.0, 0.5), Blocks.STONE.defaultBlockState());
5355
final UUID uuid = source.getUniqueId();
5456
final CompoundTag userData = new CompoundTag();
5557
userData.putString("transfer_test", "preserved");
@@ -74,6 +76,7 @@ public static void transfersAcrossDimensions(final GameTestHelper helper) {
7476

7577
final Pose3d destinationPose = new Pose3d(source.logicalPose());
7678
destinationPose.position().set(20.5, 90.0, -12.5);
79+
final BlockPos sourcePlotCenter = source.getPlot().getCenterBlock();
7780
final SubLevelTransferResult result = SubLevelTransferService.transfer(source, destinationLevel, destinationPose);
7881
final ServerSubLevel replacement = result.root();
7982

@@ -108,6 +111,20 @@ public static void transfersAcrossDimensions(final GameTestHelper helper) {
108111
helper.fail("Destination pose was not applied");
109112
return;
110113
}
114+
final BlockPos destinationPlotCenter = replacement.getPlot().getCenterBlock();
115+
final Vector3d expectedRotationPoint = new Vector3d(destinationPose.rotationPoint()).add(
116+
destinationPlotCenter.getX() - sourcePlotCenter.getX(),
117+
destinationPlotCenter.getY() - sourcePlotCenter.getY(),
118+
destinationPlotCenter.getZ() - sourcePlotCenter.getZ());
119+
if (sourcePlotCenter.equals(destinationPlotCenter)) {
120+
helper.fail("Transfer test did not allocate a relocated destination plot");
121+
return;
122+
}
123+
if (replacement.logicalPose().rotationPoint().distance(expectedRotationPoint) > 0.001) {
124+
helper.fail("Rotation point was not relocated with plot contents: expected="
125+
+ expectedRotationPoint + ", actual=" + replacement.logicalPose().rotationPoint());
126+
return;
127+
}
111128

112129
final RigidBodyHandle replacementHandle = RigidBodyHandle.of(replacement);
113130
if (replacementHandle == null
@@ -118,6 +135,7 @@ public static void transfersAcrossDimensions(final GameTestHelper helper) {
118135
}
119136

120137
removeSubLevel(destinationContainer, replacement);
138+
removeSubLevel(destinationContainer, destinationOccupant);
121139
helper.succeed();
122140
}
123141

0 commit comments

Comments
 (0)