Skip to content

Commit 0270bc3

Browse files
committed
Add primitive storage pruning on save
1 parent e29b449 commit 0270bc3

11 files changed

Lines changed: 207 additions & 55 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ public Collection<ServerSubLevel> collectForceLoadedSubLevels() {
273273
return subLevels;
274274
}
275275

276+
@ApiStatus.Internal
277+
public Map<ServerSubLevel, Set<SubLevelLoadingTicket<?>>> collectForceLoadTickets() {
278+
return Collections.unmodifiableMap(this.activeTickets);
279+
}
280+
276281
/**
277282
* Loads sub-level tickets
278283
*/
Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,15 @@
11
package dev.ryanhcode.sable.api.sublevel.ticket;
22

3-
import java.util.Objects;
43
import java.util.UUID;
54

6-
public final class SubLevelLoadingTicket<T> {
7-
private final SubLevelLoadingTicketType<T> type;
8-
private final UUID subLevelId;
9-
private final T key;
5+
public record SubLevelLoadingTicket<T>(SubLevelLoadingTicketType<T> type, UUID subLevelId, T key) {
106

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);
7+
public String toCompactString() {
8+
return "Ticket[" + this.type.name() + " (" + this.key + ")]";
239
}
2410

2511
public String toString() {
2612
final String type = String.valueOf(this.type);
2713
return "SubLevelLoadingTicket[" + type + " " + this.subLevelId + " (" + this.key + ")]";
2814
}
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-
}
4115
}

common/src/main/java/dev/ryanhcode/sable/command/SableCommand.java

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import dev.ryanhcode.sable.api.command.SubLevelArgumentType;
1010
import dev.ryanhcode.sable.api.physics.handle.RigidBodyHandle;
1111
import dev.ryanhcode.sable.api.sublevel.ServerSubLevelContainer;
12+
import dev.ryanhcode.sable.api.sublevel.ticket.SubLevelLoadingTicket;
1213
import dev.ryanhcode.sable.api.sublevel.ticket.SubLevelLoadingTicketType;
1314
import dev.ryanhcode.sable.companion.math.Pose3dc;
1415
import dev.ryanhcode.sable.network.packets.tcp.ClientboundEnterGizmoPacket;
@@ -30,9 +31,7 @@
3031
import org.joml.Vector3d;
3132
import org.joml.Vector3dc;
3233

33-
import java.util.Collection;
34-
import java.util.Formatter;
35-
import java.util.Locale;
34+
import java.util.*;
3635

3736
public class SableCommand {
3837

@@ -69,6 +68,8 @@ public static void register(final CommandDispatcher<CommandSourceStack> dispatch
6968
.then(Commands.literal("forceload")
7069
.then(Commands.literal("add").then(Commands.argument("sub_level", SubLevelArgumentType.subLevels())
7170
.executes(SableCommand::executeForceloadAddCommand)))
71+
.then(Commands.literal("query")
72+
.executes(SableCommand::executeForceloadQueryCommand))
7273
.then(Commands.literal("remove").then(Commands.argument("sub_level", SubLevelArgumentType.subLevels())
7374
.executes(SableCommand::executeForceloadRemoveCommand)))
7475
)
@@ -117,6 +118,51 @@ private static int executeSetPhysicsPausedCommand(final CommandContext<CommandSo
117118
return 1;
118119
}
119120

121+
private static int executeForceloadQueryCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
122+
final CommandSourceStack source = ctx.getSource();
123+
final ServerSubLevelContainer container = SableCommandHelper.requireSubLevelContainer(source);
124+
125+
final Map<ServerSubLevel, Set<SubLevelLoadingTicket<?>>> allTickets = container.collectForceLoadTickets();
126+
127+
int subLevelCount = 0;
128+
int ticketCount = 0;
129+
for (final Map.Entry<ServerSubLevel, Set<SubLevelLoadingTicket<?>>> entry : allTickets.entrySet()) {
130+
subLevelCount ++;
131+
ticketCount += entry.getValue().size();
132+
}
133+
134+
final Component dimension = Component.translationArg(ctx.getSource().getLevel().dimension().location());
135+
136+
if (ticketCount == 0) {
137+
source.sendFailure(Component.translatable("commands.sable.forceload.query.none", dimension));
138+
return ticketCount;
139+
}
140+
141+
final int finalTicketCount = ticketCount;
142+
final int finalSubLevelCount = subLevelCount;
143+
source.sendSuccess(() -> Component.translatable("commands.sable.forceload.query.count", finalTicketCount, finalSubLevelCount, dimension), true);
144+
145+
for (final Map.Entry<ServerSubLevel, Set<SubLevelLoadingTicket<?>>> entry : allTickets.entrySet()) {
146+
final ServerSubLevel subLevel = entry.getKey();
147+
148+
source.sendSuccess(() -> {
149+
final String uuid = subLevel.getUniqueId().toString();
150+
final MutableComponent component = Component.translatable("commands.sable.forceload.sub_level_name", Component.literal(subLevel.getName() != null ? subLevel.getName() : uuid));
151+
component.setStyle(Style.EMPTY.withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, uuid))
152+
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal(uuid)))
153+
.withColor(ChatFormatting.GRAY));
154+
return component;
155+
}, true);
156+
157+
final Set<SubLevelLoadingTicket<?>> tickets = entry.getValue();
158+
for (final SubLevelLoadingTicket<?> ticket : tickets) {
159+
source.sendSuccess(() -> Component.translatable("commands.sable.forceload.ticket", ticket.toCompactString()), false);
160+
}
161+
}
162+
163+
return ticketCount;
164+
}
165+
120166
private static int executeForceloadAddCommand(final CommandContext<CommandSourceStack> ctx) throws CommandSyntaxException {
121167
final CommandSourceStack source = ctx.getSource();
122168
final ServerSubLevelContainer container = SableCommandHelper.requireSubLevelContainer(source);

common/src/main/java/dev/ryanhcode/sable/command/SableStorageCommands.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,49 @@ public class SableStorageCommands {
3030

3131
public static void register(final LiteralArgumentBuilder<CommandSourceStack> sableBuilder, final CommandBuildContext buildContext) {
3232
sableBuilder.then(Commands.literal("storage")
33+
.then(Commands.literal("prune_regions")
34+
.executes(ctx -> {
35+
final ServerLevel level = ctx.getSource().getLevel();
36+
final ServerSubLevelContainer container = ServerSubLevelContainer.getContainer(level);
37+
final SubLevelHoldingChunkMap holdingChunkMap = container.getHoldingChunkMap();
38+
final SubLevelStorage storage = holdingChunkMap.getStorage();
39+
40+
final File[] regionFiles = storage.getFolder().toFile().listFiles((dir, name) -> name.endsWith(SubLevelRegionFile.FILE_EXTENSION));
41+
42+
if (regionFiles != null) {
43+
for (final File regionFile : regionFiles) {
44+
final String fileName = regionFile.getName();
45+
final String withoutExtension = fileName.substring(0, fileName.length() - SubLevelRegionFile.FILE_EXTENSION.length());
46+
final String[] parts = withoutExtension.split("\\.");
47+
if (parts.length != 3) continue;
48+
49+
final int regionX, regionZ;
50+
try {
51+
regionX = Integer.parseInt(parts[1]);
52+
regionZ = Integer.parseInt(parts[2]);
53+
} catch (final NumberFormatException e) {
54+
continue;
55+
}
56+
57+
for (int localX = 0; localX < SubLevelRegionFile.SIDE_LENGTH; localX++) {
58+
for (int localZ = 0; localZ < SubLevelRegionFile.SIDE_LENGTH; localZ++) {
59+
final ChunkPos chunkPos = new ChunkPos(
60+
regionX * SubLevelRegionFile.SIDE_LENGTH + localX,
61+
regionZ * SubLevelRegionFile.SIDE_LENGTH + localZ
62+
);
63+
64+
final SubLevelHoldingChunk holdingChunk = storage.attemptLoadHoldingChunk(chunkPos);
65+
if (holdingChunk == null) continue;
66+
67+
if (holdingChunk.isEmpty()) {
68+
storage.attemptRemoveHoldingChunk(chunkPos);
69+
}
70+
}
71+
}
72+
}
73+
}
74+
return 1;
75+
}))
3376
.then(Commands.literal("find_all_sub_levels")
3477
.executes(ctx -> {
3578
final ServerLevel level = ctx.getSource().getLevel();

common/src/main/java/dev/ryanhcode/sable/sublevel/storage/SubLevelTicketsSavedData.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,10 @@ private static <T> SubLevelLoadingTicket<T> deserializeTicket(final UUID subLeve
109109
}
110110

111111
private static <T> CompoundTag serializeTicket(final SubLevelLoadingTicket<T> ticket) {
112-
final SubLevelLoadingTicketType<T> type = ticket.getType();
112+
final SubLevelLoadingTicketType<T> type = ticket.type();
113113
final Codec<T> codec = type.codec();
114114

115-
return codec.encodeStart(NbtOps.INSTANCE, ticket.getKey())
115+
return codec.encodeStart(NbtOps.INSTANCE, ticket.key())
116116
.resultOrPartial(error -> Sable.LOGGER.warn("Failed to serialize ticket key for type {}: {}", type.name(), error))
117117
.map(keyTag -> {
118118
final CompoundTag tag = new CompoundTag();

common/src/main/java/dev/ryanhcode/sable/sublevel/storage/holding/SubLevelHoldingChunk.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ public boolean shouldKeepLoaded() {
188188
return this.keepLoaded;
189189
}
190190

191+
public boolean isEmpty() {
192+
return this.pointers.isEmpty() && this.loadedHoldingSubLevels.isEmpty();
193+
}
194+
191195
@Override
192196
public String toString() {
193197
return "SubLevelHoldingChunk{" +

common/src/main/java/dev/ryanhcode/sable/sublevel/storage/holding/SubLevelHoldingChunkMap.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,10 @@ public void saveAll() {
270270
this.queuedUnloads.add(holdingChunkPos);
271271
}
272272
}
273+
274+
if (holdingChunk.isEmpty()) {
275+
this.queuedUnloads.add(holdingChunkPos);
276+
}
273277
}
274278

275279
for (final ChunkPos unload : this.queuedUnloads) {
@@ -283,6 +287,7 @@ public void saveAll() {
283287
for (final HoldingSubLevel holdingSubLevel : holdingChunk.getLoadedHoldingSubLevels()) {
284288
this.allHoldingSubLevels.remove(holdingSubLevel.data().uuid());
285289
}
290+
286291
this.setDirty(unload);
287292
}
288293
}
@@ -297,7 +302,11 @@ public void saveAll() {
297302
}
298303

299304
if (holdingChunk != null) {
300-
this.storage.attemptSaveHoldingChunk(chunkPos, holdingChunk);
305+
if (holdingChunk.isEmpty()) {
306+
this.storage.attemptRemoveHoldingChunk(chunkPos);
307+
} else {
308+
this.storage.attemptSaveHoldingChunk(chunkPos, holdingChunk);
309+
}
301310
}
302311
}
303312

@@ -312,6 +321,7 @@ public void saveAll() {
312321
}
313322

314323
this.storage.flush();
324+
this.storage.pruneCache();
315325
} catch (final IOException e) {
316326
Sable.LOGGER.error("Failed to flush sub-level storage to disk", e);
317327
}

common/src/main/java/dev/ryanhcode/sable/sublevel/storage/region/SubLevelRegionFile.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ public void trySave(final int localX, final int localZ, final SubLevelHoldingChu
4242
}
4343
}
4444

45+
public void tryRemove(final int localX, final int localZ) {
46+
try {
47+
this.write(this.getIndex(localX, localZ), (CompoundTag) null);
48+
} catch (final IOException e) {
49+
Sable.LOGGER.error("Failed to remove sub-level holding chunk at ({}, {})", localX, localZ, e);
50+
}
51+
}
52+
4553
@Nullable
4654
public SubLevelHoldingChunk read(final ChunkPos chunkPos) {
4755
final int localX = chunkPos.getRegionLocalX();

common/src/main/java/dev/ryanhcode/sable/sublevel/storage/region/SubLevelStorageFile.java

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ private ByteBuffer createExternalStub() {
208208
return byteBuffer;
209209
}
210210

211+
public boolean isEmpty() {
212+
return this.usedSectors.length() <= (this.beginningSectorSize / this.sectorSize);
213+
}
214+
211215
/**
212216
* Writes a sub-levels data to disk
213217
*
@@ -377,6 +381,24 @@ public int packSpan(final int start, final int length) {
377381
return (start << 8) | length; // Pack the offset and length into a single integer
378382
}
379383

384+
private void padOrTruncateToFullSector() throws IOException {
385+
// how many sectors of data are we using?
386+
final int bytesNeededForFile = this.usedSectors.length() * this.sectorSize;
387+
final int currentFileSize = (int) this.file.size();
388+
389+
if (currentFileSize > bytesNeededForFile) {
390+
this.file.truncate(bytesNeededForFile);
391+
} else {
392+
final int desiredSize = bytesNeededForFile;
393+
394+
if (currentFileSize < desiredSize) {
395+
final ByteBuffer byteBuffer = PADDING_BUFFER.duplicate();
396+
byteBuffer.position(0);
397+
this.file.write(byteBuffer, desiredSize - 1);
398+
}
399+
}
400+
}
401+
380402
/**
381403
* Frees any native resources held by this object.
382404
*/
@@ -393,26 +415,14 @@ public void close() throws IOException {
393415
}
394416
}
395417

396-
public void flush() throws IOException {
397-
this.file.force(true);
418+
public void delete() throws IOException {
419+
this.file.close();
420+
Files.delete(this.path);
421+
Files.deleteIfExists(this.externalFileDir);
398422
}
399423

400-
private void padOrTruncateToFullSector() throws IOException {
401-
// how many sectors of data are we using?
402-
final int bytesNeededForFile = this.usedSectors.length() * this.sectorSize;
403-
final int currentFileSize = (int) this.file.size();
404-
405-
if (currentFileSize > bytesNeededForFile) {
406-
this.file.truncate(bytesNeededForFile);
407-
} else {
408-
final int desiredSize = bytesNeededForFile;
409-
410-
if (currentFileSize < desiredSize) {
411-
final ByteBuffer byteBuffer = PADDING_BUFFER.duplicate();
412-
byteBuffer.position(0);
413-
this.file.write(byteBuffer, desiredSize - 1);
414-
}
415-
}
424+
public void flush() throws IOException {
425+
this.file.force(true);
416426
}
417427

418428
class SectorSpanDataBuffer extends ByteArrayOutputStream {

0 commit comments

Comments
 (0)