11package dev .ryanhcode .sable .api .sublevel ;
22
3-
43import dev .ryanhcode .sable .Sable ;
4+ import dev .ryanhcode .sable .SableConfig ;
5+ import dev .ryanhcode .sable .api .SubLevelHelper ;
6+ import dev .ryanhcode .sable .api .sublevel .ticket .SubLevelLoadingTicket ;
7+ import dev .ryanhcode .sable .api .sublevel .ticket .SubLevelLoadingTicketType ;
8+ import dev .ryanhcode .sable .api .sublevel .ticket .SubLevelTicketInfo ;
59import dev .ryanhcode .sable .companion .math .Pose3d ;
610import dev .ryanhcode .sable .sublevel .ServerSubLevel ;
711import dev .ryanhcode .sable .sublevel .SubLevel ;
812import dev .ryanhcode .sable .sublevel .storage .SubLevelOccupancySavedData ;
913import dev .ryanhcode .sable .sublevel .storage .SubLevelRemovalReason ;
14+ import dev .ryanhcode .sable .sublevel .storage .SubLevelTicketsSavedData ;
15+ import dev .ryanhcode .sable .sublevel .storage .holding .GlobalSavedSubLevelPointer ;
1016import dev .ryanhcode .sable .sublevel .storage .holding .SubLevelHoldingChunkMap ;
1117import dev .ryanhcode .sable .sublevel .system .SubLevelPhysicsSystem ;
1218import dev .ryanhcode .sable .sublevel .system .SubLevelTrackingSystem ;
19+ import it .unimi .dsi .fastutil .objects .*;
1320import net .minecraft .core .BlockPos ;
1421import net .minecraft .core .Holder ;
1522import net .minecraft .resources .ResourceKey ;
2128import org .jetbrains .annotations .Nullable ;
2229import org .joml .Vector3d ;
2330
24- import java .util .List ;
25- import java .util .Optional ;
26- import java .util .UUID ;
31+ import java .util .*;
2732
2833/**
2934 * Holds all sub-levels and plots in a {@link ServerLevel}
@@ -45,6 +50,16 @@ public class ServerSubLevelContainer extends SubLevelContainer {
4550 */
4651 private SubLevelHoldingChunkMap holdingChunkMap ;
4752
53+ /**
54+ * All active sub-level loading tickets
55+ */
56+ protected final Object2ObjectMap <ServerSubLevel , ObjectSet <SubLevelLoadingTicket <?>>> activeTickets = new Object2ObjectOpenHashMap <>();
57+
58+ /**
59+ * All sub-level loading tickets
60+ */
61+ protected final Object2ObjectMap <UUID , SubLevelTicketInfo > allTickets = new Object2ObjectOpenHashMap <>();
62+
4863 /**
4964 * Creates a new sub-level container with the given side length and plot size.
5065 *
@@ -63,6 +78,8 @@ public ServerSubLevelContainer(final Level level, final int logSideLength, final
6378 */
6479 public void initialize () {
6580 this .holdingChunkMap = new SubLevelHoldingChunkMap (this .getLevel (), this );
81+
82+ this .loadForceLoadedSubLevels ();
6683 }
6784
6885 /**
@@ -169,10 +186,148 @@ public ServerLevel getLevel() {
169186 return (ServerLevel ) super .getLevel ();
170187 }
171188
189+ /**
190+ * Adds a sub-level force-loading ticket
191+ *
192+ * @param subLevel the loaded sub-level to add the ticket to
193+ * @param ticketType the type of ticket to add to the sub-level
194+ * @param key the key of the ticket. This will be used to identify the ticket to remove it.
195+ * Two tickets with the same key on the same sub-level cannot exist
196+ *
197+ * @return true if the ticket was added (and did not previously exist)
198+ */
199+ public <T > boolean addForceLoadTicket (final ServerSubLevel subLevel , final SubLevelLoadingTicketType <T > ticketType , final T key ) {
200+ final UUID uuid = subLevel .getUniqueId ();
201+ final SubLevelLoadingTicket <T > ticket = new SubLevelLoadingTicket <>(ticketType , uuid , key );
202+
203+ final ObjectSet <SubLevelLoadingTicket <?>> loadedSet = this .activeTickets .computeIfAbsent (subLevel , (ignored ) -> new ObjectArraySet <>());
204+ final SubLevelTicketInfo allSet = this .allTickets .computeIfAbsent (uuid , (ignored ) -> new SubLevelTicketInfo ());
205+ loadedSet .add (ticket );
206+
207+ if (allSet .tickets ().add (ticket )) {
208+ SubLevelTicketsSavedData .getOrLoad (this .getLevel ()).setDirty ();
209+ return true ;
210+ }
211+
212+ return false ;
213+ }
214+
215+ /**
216+ * Removes a sub-level force-loading ticket
217+ *
218+ * @param subLevel the loaded sub-level to remove the ticket from
219+ * @param ticketType the type of ticket to add to the sub-level
220+ * @param key the key of the ticket. This will be used to identify the ticket to remove it.
221+ * Two tickets with the same key on the same sub-level cannot exist
222+ *
223+ * @return true if the ticket existed and was removed
224+ */
225+ public <T > boolean removeForceLoadTicket (final ServerSubLevel subLevel , final SubLevelLoadingTicketType <T > ticketType , final T key ) {
226+ final UUID uuid = subLevel .getUniqueId ();
227+ final SubLevelLoadingTicket <T > ticket = new SubLevelLoadingTicket <>(ticketType , uuid , key );
228+
229+ final ObjectSet <SubLevelLoadingTicket <?>> loadedSet = this .activeTickets .get (subLevel );
230+ final SubLevelTicketInfo allSet = this .allTickets .get (subLevel .getUniqueId ());
231+
232+ if (loadedSet != null ) {
233+ loadedSet .remove (ticket );
234+
235+ if (loadedSet .isEmpty ()) {
236+ this .activeTickets .remove (subLevel );
237+ }
238+ }
239+
240+ if (allSet != null ) {
241+ final boolean existed = allSet .tickets ().remove (ticket );
242+
243+ if (allSet .tickets ().isEmpty ()) {
244+ this .allTickets .remove (subLevel .getUniqueId ());
245+ }
246+
247+ if (existed ) {
248+ SubLevelTicketsSavedData .getOrLoad (this .getLevel ()).setDirty ();
249+ return true ;
250+ }
251+ }
252+
253+ return false ;
254+ }
255+
256+ /**
257+ * Collect all force-loaded sub-levels (and sub-levels force-loaded through dependencies)
258+ */
259+ public Collection <ServerSubLevel > collectForceLoadedSubLevels () {
260+ if (this .activeTickets .isEmpty ()) {
261+ return List .of ();
262+ }
263+ final ObjectOpenHashSet <ServerSubLevel > subLevels = new ObjectOpenHashSet <>();
264+
265+ for (final ServerSubLevel subLevel : this .activeTickets .keySet ()) {
266+ if (subLevels .contains (subLevel )) {
267+ continue ;
268+ }
269+
270+ subLevels .addAll (SubLevelHelper .getLoadingDependencyChain (subLevel ));
271+ }
272+
273+ return subLevels ;
274+ }
275+
276+ /**
277+ * Loads sub-level tickets
278+ */
279+ @ ApiStatus .Internal
280+ public void loadTickets (final Object2ObjectMap <UUID , SubLevelTicketInfo > tickets ) {
281+ this .allTickets .putAll (tickets );
282+ }
283+
284+ /**
285+ * @return an immutable view of all sub-level loading tickets
286+ */
287+ @ ApiStatus .Internal
288+ public Map <UUID , SubLevelTicketInfo > getAllTickets () {
289+ return Collections .unmodifiableMap (this .allTickets );
290+ }
291+
292+ /**
293+ * Loads all force-loaded sub-levels
294+ */
295+ private void loadForceLoadedSubLevels () {
296+ for (final Map .Entry <UUID , SubLevelTicketInfo > entry : this .allTickets .entrySet ()) {
297+ final UUID uuid = entry .getKey ();
298+ final GlobalSavedSubLevelPointer pointer = entry .getValue ().getPointer ();
299+
300+ if (pointer != null ) {
301+ this .holdingChunkMap .snatchAndLoad (pointer , uuid );
302+ } else {
303+ Sable .LOGGER .error ("Cannot load force-loaded sub-level with ID {} because the ticket info was not saved with a pointer" , uuid );
304+ }
305+ }
306+ }
307+
172308 /**
173309 * Frees all native resources
174310 */
311+ @ ApiStatus .Internal
175312 public void close () {
313+ final List <ServerSubLevel > subLevels = new ObjectArrayList <>(this .getAllSubLevels ());
314+
315+ if (!subLevels .isEmpty ()) {
316+ final Map <UUID , SubLevelTicketInfo > tickets = this .getAllTickets ();
317+
318+ for (final ServerSubLevel subLevel : subLevels ) {
319+ if (SableConfig .VERBOSE_SERIALIZATION_LOGGING .get () && !tickets .containsKey (subLevel .getUniqueId ())) {
320+ Sable .LOGGER .error ("Sub-level {} was present after world closing, but is not force-loaded." , subLevel );
321+ }
322+
323+ this .removeSubLevel (subLevel , SubLevelRemovalReason .UNLOADED );
324+ }
325+ }
326+
327+ if (this .physics != null ) {
328+ this .physics .getPipeline ().dispose ();
329+ }
330+
176331 try {
177332 this .holdingChunkMap .close ();
178333 } catch (final Exception e ) {
0 commit comments