11package dev .ryanhcode .sable .api .sublevel ;
22
3-
43import 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 ;
58import dev .ryanhcode .sable .companion .math .Pose3d ;
69import dev .ryanhcode .sable .sublevel .ServerSubLevel ;
710import dev .ryanhcode .sable .sublevel .SubLevel ;
811import dev .ryanhcode .sable .sublevel .storage .SubLevelOccupancySavedData ;
912import dev .ryanhcode .sable .sublevel .storage .SubLevelRemovalReason ;
13+ import dev .ryanhcode .sable .sublevel .storage .SubLevelTicketsSavedData ;
14+ import dev .ryanhcode .sable .sublevel .storage .holding .GlobalSavedSubLevelPointer ;
1015import dev .ryanhcode .sable .sublevel .storage .holding .SubLevelHoldingChunkMap ;
1116import dev .ryanhcode .sable .sublevel .system .SubLevelPhysicsSystem ;
1217import dev .ryanhcode .sable .sublevel .system .SubLevelTrackingSystem ;
18+ import it .unimi .dsi .fastutil .objects .*;
1319import net .minecraft .core .BlockPos ;
1420import net .minecraft .core .Holder ;
1521import net .minecraft .resources .ResourceKey ;
2127import org .jetbrains .annotations .Nullable ;
2228import 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 ) {
0 commit comments