Skip to content

Commit df5f343

Browse files
committed
Allow for force-loading sub-levels
Upgrade the holding chunk-map, add more configs regarding serialization, and add support for force-loading sub-levels.
1 parent 46eb6ed commit df5f343

24 files changed

Lines changed: 919 additions & 108 deletions

common/src/main/java/dev/ryanhcode/sable/Sable.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import dev.ryanhcode.sable.api.physics.force.ForceGroups;
66
import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer;
77
import dev.ryanhcode.sable.api.sublevel.SubLevelContainer;
8+
import dev.ryanhcode.sable.api.sublevel.SubLevelTicketLoadingSystem;
89
import dev.ryanhcode.sable.companion.SableCompanion;
910
import dev.ryanhcode.sable.index.SableTags;
1011
import dev.ryanhcode.sable.network.tcp.SableTCPPackets;
@@ -83,6 +84,7 @@ public static void defaultSubLevelContainerInitializer(final Level level, final
8384
serverContainer.addObserver(physicsSystem);
8485
serverContainer.addObserver(trackingSystem);
8586
serverContainer.addObserver(new SubLevelTrackingPointObserver(serverLevel));
87+
serverContainer.addObserver(new SubLevelTicketLoadingSystem(serverContainer));
8688

8789
PhysicsBlockPropertiesDefinitionLoader.INSTANCE.applyAll();
8890
}

common/src/main/java/dev/ryanhcode/sable/SableConfig.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public final class SableConfig {
1717
public static final ModConfigSpec.IntValue SUB_LEVEL_PUNCH_COOLDOWN_TICKS;
1818
public static final ModConfigSpec.BooleanValue DISABLE_UDP_PIPELINE;
1919
public static final ModConfigSpec.BooleanValue ATTEMPT_UDP_NETWORKING;
20+
public static final ModConfigSpec.BooleanValue VERBOSE_SERIALIZATION_LOGGING;
21+
public static final ModConfigSpec.BooleanValue SUB_LEVEL_SAVING_LOG_MESSAGE;
2022

2123
static {
2224
final ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
@@ -56,6 +58,12 @@ public final class SableConfig {
5658
ATTEMPT_UDP_NETWORKING = builder
5759
.comment("If Sable should attempt to authenticate with clients and send them sub-level data over UDP")
5860
.define("attempt_udp_networking", true);
61+
SUB_LEVEL_SAVING_LOG_MESSAGE = builder
62+
.comment("If Sable should log when saving sub-levels for a dimension.")
63+
.define("sub_level_saving_log_message", true);
64+
VERBOSE_SERIALIZATION_LOGGING = builder
65+
.comment("If Sable should use verbose logging for its serialization system and the holding chunk-map. Not recommended- for debugging purposes only.")
66+
.define("verbose_serialization_logging", false);
5967

6068
SPEC = builder.build();
6169
}

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

Lines changed: 158 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
package dev.ryanhcode.sable.api.sublevel;
22

3-
43
import dev.ryanhcode.sable.Sable;
4+
import dev.ryanhcode.sable.api.SubLevelHelper;
5+
import dev.ryanhcode.sable.api.sublevel.ticket.SubLevelLoadingTicket;
6+
import dev.ryanhcode.sable.api.sublevel.ticket.SubLevelLoadingTicketType;
7+
import dev.ryanhcode.sable.api.sublevel.ticket.SubLevelTicketInfo;
58
import dev.ryanhcode.sable.companion.math.Pose3d;
69
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
710
import dev.ryanhcode.sable.sublevel.SubLevel;
811
import dev.ryanhcode.sable.sublevel.storage.SubLevelOccupancySavedData;
912
import dev.ryanhcode.sable.sublevel.storage.SubLevelRemovalReason;
13+
import dev.ryanhcode.sable.sublevel.storage.SubLevelTicketsSavedData;
14+
import dev.ryanhcode.sable.sublevel.storage.holding.GlobalSavedSubLevelPointer;
1015
import dev.ryanhcode.sable.sublevel.storage.holding.SubLevelHoldingChunkMap;
1116
import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem;
1217
import dev.ryanhcode.sable.sublevel.system.SubLevelTrackingSystem;
18+
import it.unimi.dsi.fastutil.objects.*;
1319
import net.minecraft.core.BlockPos;
1420
import net.minecraft.core.Holder;
1521
import net.minecraft.resources.ResourceKey;
@@ -21,9 +27,7 @@
2127
import org.jetbrains.annotations.Nullable;
2228
import org.joml.Vector3d;
2329

24-
import java.util.List;
25-
import java.util.Optional;
26-
import java.util.UUID;
30+
import java.util.*;
2731

2832
/**
2933
* Holds all sub-levels and plots in a {@link ServerLevel}
@@ -45,6 +49,16 @@ public class ServerSubLevelContainer extends SubLevelContainer {
4549
*/
4650
private SubLevelHoldingChunkMap holdingChunkMap;
4751

52+
/**
53+
* All active sub-level loading tickets
54+
*/
55+
protected final Object2ObjectMap<ServerSubLevel, ObjectSet<SubLevelLoadingTicket<?>>> activeTickets = new Object2ObjectOpenHashMap<>();
56+
57+
/**
58+
* All sub-level loading tickets
59+
*/
60+
protected final Object2ObjectMap<UUID, SubLevelTicketInfo> allTickets = new Object2ObjectOpenHashMap<>();
61+
4862
/**
4963
* Creates a new sub-level container with the given side length and plot size.
5064
*
@@ -63,6 +77,8 @@ public ServerSubLevelContainer(final Level level, final int logSideLength, final
6377
*/
6478
public void initialize() {
6579
this.holdingChunkMap = new SubLevelHoldingChunkMap(this.getLevel(), this);
80+
81+
this.loadForceLoadedSubLevels();
6682
}
6783

6884
/**
@@ -169,10 +185,148 @@ public ServerLevel getLevel() {
169185
return (ServerLevel) super.getLevel();
170186
}
171187

188+
/**
189+
* Adds a sub-level force-loading ticket
190+
*
191+
* @param subLevel the loaded sub-level to add the ticket to
192+
* @param ticketType the type of ticket to add to the sub-level
193+
* @param key the key of the ticket. This will be used to identify the ticket to remove it.
194+
* Two tickets with the same key on the same sub-level cannot exist
195+
*
196+
* @return true if the ticket was added (and did not previously exist)
197+
*/
198+
public <T> boolean addForceLoadTicket(final ServerSubLevel subLevel, final SubLevelLoadingTicketType<T> ticketType, final T key) {
199+
final UUID uuid = subLevel.getUniqueId();
200+
final SubLevelLoadingTicket<T> ticket = new SubLevelLoadingTicket<>(ticketType, uuid, key);
201+
202+
final ObjectSet<SubLevelLoadingTicket<?>> loadedSet = this.activeTickets.computeIfAbsent(subLevel, (ignored) -> new ObjectArraySet<>());
203+
final SubLevelTicketInfo allSet = this.allTickets.computeIfAbsent(uuid, (ignored) -> new SubLevelTicketInfo());
204+
loadedSet.add(ticket);
205+
206+
if (allSet.tickets().add(ticket)) {
207+
SubLevelTicketsSavedData.getOrLoad(this.getLevel()).setDirty();
208+
return true;
209+
}
210+
211+
return false;
212+
}
213+
214+
/**
215+
* Removes a sub-level force-loading ticket
216+
*
217+
* @param subLevel the loaded sub-level to remove the ticket from
218+
* @param ticketType the type of ticket to add to the sub-level
219+
* @param key the key of the ticket. This will be used to identify the ticket to remove it.
220+
* Two tickets with the same key on the same sub-level cannot exist
221+
*
222+
* @return true if the ticket existed and was removed
223+
*/
224+
public <T> boolean removeForceLoadTicket(final ServerSubLevel subLevel, final SubLevelLoadingTicketType<T> ticketType, final T key) {
225+
final UUID uuid = subLevel.getUniqueId();
226+
final SubLevelLoadingTicket<T> ticket = new SubLevelLoadingTicket<>(ticketType, uuid, key);
227+
228+
final ObjectSet<SubLevelLoadingTicket<?>> loadedSet = this.activeTickets.get(subLevel);
229+
final SubLevelTicketInfo allSet = this.allTickets.get(subLevel.getUniqueId());
230+
231+
if (loadedSet != null) {
232+
loadedSet.remove(ticket);
233+
234+
if (loadedSet.isEmpty()) {
235+
this.activeTickets.remove(subLevel);
236+
}
237+
}
238+
239+
if (allSet != null) {
240+
final boolean existed = allSet.tickets().remove(ticket);
241+
242+
if (allSet.tickets().isEmpty()) {
243+
this.allTickets.remove(subLevel.getUniqueId());
244+
}
245+
246+
if (existed) {
247+
SubLevelTicketsSavedData.getOrLoad(this.getLevel()).setDirty();
248+
return true;
249+
}
250+
}
251+
252+
return false;
253+
}
254+
255+
/**
256+
* Collect all force-loaded sub-levels (and sub-levels force-loaded through dependencies)
257+
*/
258+
public Collection<ServerSubLevel> collectForceLoadedSubLevels() {
259+
if (this.activeTickets.isEmpty()) {
260+
return List.of();
261+
}
262+
final ObjectOpenHashSet<ServerSubLevel> subLevels = new ObjectOpenHashSet<>();
263+
264+
for (final ServerSubLevel subLevel : this.activeTickets.keySet()) {
265+
if (subLevels.contains(subLevel)) {
266+
continue;
267+
}
268+
269+
subLevels.addAll(SubLevelHelper.getLoadingDependencyChain(subLevel));
270+
}
271+
272+
return subLevels;
273+
}
274+
275+
/**
276+
* Loads sub-level tickets
277+
*/
278+
@ApiStatus.Internal
279+
public void loadTickets(final Object2ObjectMap<UUID, SubLevelTicketInfo> tickets) {
280+
this.allTickets.putAll(tickets);
281+
}
282+
283+
/**
284+
* @return an immutable view of all sub-level loading tickets
285+
*/
286+
@ApiStatus.Internal
287+
public Map<UUID, SubLevelTicketInfo> getAllTickets() {
288+
return Collections.unmodifiableMap(this.allTickets);
289+
}
290+
291+
/**
292+
* Loads all force-loaded sub-levels
293+
*/
294+
private void loadForceLoadedSubLevels() {
295+
for (final Map.Entry<UUID, SubLevelTicketInfo> entry : this.allTickets.entrySet()) {
296+
final UUID uuid = entry.getKey();
297+
final GlobalSavedSubLevelPointer pointer = entry.getValue().getPointer();
298+
299+
if (pointer != null) {
300+
this.holdingChunkMap.snatchAndLoad(pointer, uuid);
301+
} else {
302+
Sable.LOGGER.error("Cannot load force-loaded sub-level with ID {} because the ticket info was not saved with a pointer", uuid);
303+
}
304+
}
305+
}
306+
172307
/**
173308
* Frees all native resources
174309
*/
310+
@ApiStatus.Internal
175311
public void close() {
312+
final List<ServerSubLevel> subLevels = new ObjectArrayList<>(this.getAllSubLevels());
313+
314+
if (!subLevels.isEmpty()) {
315+
final Map<UUID, SubLevelTicketInfo> tickets = this.getAllTickets();
316+
317+
for (final ServerSubLevel subLevel : subLevels) {
318+
if (!tickets.containsKey(subLevel.getUniqueId())) {
319+
Sable.LOGGER.error("Sub-level {} was present after world closing, but is not force-loaded.", subLevel);
320+
}
321+
322+
this.removeSubLevel(subLevel, SubLevelRemovalReason.UNLOADED);
323+
}
324+
}
325+
326+
if (this.physics != null) {
327+
this.physics.getPipeline().dispose();
328+
}
329+
176330
try {
177331
this.holdingChunkMap.close();
178332
} catch (final Exception e) {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package dev.ryanhcode.sable.api.sublevel;
2+
3+
import dev.ryanhcode.sable.api.sublevel.ticket.SubLevelTicketInfo;
4+
import dev.ryanhcode.sable.sublevel.ServerSubLevel;
5+
import dev.ryanhcode.sable.sublevel.SubLevel;
6+
import dev.ryanhcode.sable.sublevel.storage.SubLevelRemovalReason;
7+
import it.unimi.dsi.fastutil.objects.ObjectArraySet;
8+
import org.jetbrains.annotations.ApiStatus;
9+
10+
/**
11+
* Manages the {@code activeTickets} map in {@link ServerSubLevelContainer}
12+
*/
13+
@ApiStatus.Internal
14+
public class SubLevelTicketLoadingSystem implements SubLevelObserver {
15+
private final ServerSubLevelContainer container;
16+
17+
public SubLevelTicketLoadingSystem(final ServerSubLevelContainer container) {
18+
this.container = container;
19+
}
20+
21+
@Override
22+
public void onSubLevelAdded(final SubLevel subLevel) {
23+
final SubLevelTicketInfo info = this.container.allTickets.get(subLevel.getUniqueId());
24+
25+
if (info != null && !info.tickets().isEmpty()) {
26+
this.container.activeTickets.put((ServerSubLevel) subLevel, new ObjectArraySet<>(info.tickets()));
27+
}
28+
}
29+
30+
@Override
31+
public void onSubLevelRemoved(final SubLevel subLevel, final SubLevelRemovalReason reason) {
32+
final ServerSubLevel serverSubLevel = ((ServerSubLevel) subLevel);
33+
34+
if (reason == SubLevelRemovalReason.UNLOADED) {
35+
this.container.activeTickets.remove(serverSubLevel);
36+
37+
final SubLevelTicketInfo info = this.container.allTickets.get(serverSubLevel);
38+
39+
if (info != null) {
40+
info.setPointer(serverSubLevel.getLastSerializationPointer());
41+
}
42+
} else if (reason == SubLevelRemovalReason.REMOVED) {
43+
this.container.allTickets.remove(subLevel.getUniqueId());
44+
this.container.activeTickets.remove(serverSubLevel);
45+
}
46+
}
47+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package dev.ryanhcode.sable.api.sublevel.ticket;
2+
3+
import java.util.Objects;
4+
import java.util.UUID;
5+
6+
public final class SubLevelLoadingTicket<T> {
7+
private final SubLevelLoadingTicketType<T> type;
8+
private final UUID subLevelId;
9+
private final T key;
10+
11+
public SubLevelLoadingTicket(final SubLevelLoadingTicketType<T> type, final UUID subLevelId, final T key) {
12+
this.subLevelId = subLevelId;
13+
this.type = type;
14+
this.key = key;
15+
}
16+
17+
@Override
18+
public boolean equals(final Object o) {
19+
if (o == null || this.getClass() != o.getClass()) return false;
20+
21+
final SubLevelLoadingTicket<?> that = (SubLevelLoadingTicket<?>) o;
22+
return Objects.equals(this.type, that.type) && Objects.equals(this.subLevelId, that.subLevelId) && Objects.equals(this.key, that.key);
23+
}
24+
25+
public String toString() {
26+
final String type = String.valueOf(this.type);
27+
return "SubLevelLoadingTicket[" + type + " " + this.subLevelId + " (" + this.key + ")]";
28+
}
29+
30+
public SubLevelLoadingTicketType<T> getType() {
31+
return this.type;
32+
}
33+
34+
public T getKey() {
35+
return this.key;
36+
}
37+
38+
public UUID getSubLevelId() {
39+
return this.subLevelId;
40+
}
41+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package dev.ryanhcode.sable.api.sublevel.ticket;
2+
3+
import com.mojang.serialization.Codec;
4+
import dev.ryanhcode.sable.Sable;
5+
import net.minecraft.resources.ResourceLocation;
6+
import net.minecraft.util.Unit;
7+
import org.jetbrains.annotations.NotNull;
8+
import org.jetbrains.annotations.Nullable;
9+
10+
import java.util.HashMap;
11+
import java.util.Map;
12+
13+
public record SubLevelLoadingTicketType<T>(ResourceLocation name, Codec<T> codec) {
14+
private static final Map<ResourceLocation, SubLevelLoadingTicketType<?>> REGISTRY = new HashMap<>();
15+
16+
public static final SubLevelLoadingTicketType<Unit> COMMAND_FORCED = create(Sable.sablePath("command_forced"), Unit.CODEC);
17+
18+
private static <T> SubLevelLoadingTicketType<T> create(final ResourceLocation name, final Codec<T> codec) {
19+
final SubLevelLoadingTicketType<T> type = new SubLevelLoadingTicketType<>(name, codec);
20+
REGISTRY.put(name, type);
21+
return type;
22+
}
23+
24+
public static @Nullable SubLevelLoadingTicketType<?> byName(final ResourceLocation name) {
25+
return REGISTRY.get(name);
26+
}
27+
28+
@Override
29+
public @NotNull String toString() {
30+
return "SubLevelTicketType{" + "name=" + this.name + "}";
31+
}
32+
}

0 commit comments

Comments
 (0)