Skip to content

Commit 522ed8d

Browse files
committed
feat(worlds): overhaul per-world storage architecture for 26.1
Previously, SpongeWorldManager created a separate LevelStorageAccess per dimension via createLevelStorageAccess(), with bridge$setDedicated(true) overriding getDimensionPath() to bypass vanilla's DimensionType.getStorageFolder(). This produced the old DIM-1/DIM1 directory layout with per-dimension level.dat files containing a SpongeData compound (UUID, key, WeatherState, CustomBossEvents, WorldOptions). Each dimension had its own session.lock and the SpongeData was injected into level.dat during saveDataTag() and extracted during getLevelDataAndDimensions() via bridge$readSpongeLevelData/writeSpongeLevelData. Now, all dimensions share the server's single LevelStorageAccess. Vanilla's DimensionType.getStorageFolder() handles path resolution, placing all dimensions under dimensions/{namespace}/{path}/. Each ServerLevel's ServerChunkCache creates an independent SavedDataStorage at getDimensionPath(dim)/data/, enabling per-world data through vanilla's own SavedData mechanism. Per-world features now use vanilla SavedDataType instances directly: - WeatherData.TYPE (minecraft:weather) via ServerLevel.getWeatherData() override - GameRuleMap.TYPE (minecraft:game_rules) via ServerLevel.getGameRules() override - CustomBossEvents.TYPE via bridge$getBossBarManager() using per-dim storage - WorldBorder already per-dimension in vanilla 26.1 (no override needed) Sponge-specific identity (UUID, ResourceKey) is stored as SpongeRegistryData, a custom SavedData at data/sponge/registry.dat per dimension. Global map/player UUID indices are stored as SpongeMapUUIDData at data/sponge/map_uuids.dat in the server-level SavedDataStorage. level.dat no longer contains any SpongeData — it is pure vanilla format. The SpongeData injection in LevelStorageSource_LevelStorageAccessMixin and the read hooks in LevelStorageSourceMixin_Vanilla and PrimaryLevelDataMixin are removed. Migration from old Sponge format is handled by FileFixerUpperMixin, which hooks into FileFixerUpper.fix() at HEAD to extract UUIDs from SpongeData in level.dat and DIM-1/DIM1 level.dat files before vanilla's DimensionStorageFileFix deletes those directories. Legacy SavedData files (random_sequences.dat, etc.) left behind by vanilla's fixer are moved to the correct namespaced paths and the old directories are cleaned up. Extracted identities are cached in SpongeLevelMigration (static volatile, cross-thread from main to Server thread) and written to per-dimension registry.dat during MinecraftServer.loadLevel() HEAD inject, before any ServerLevel is created. Additional fixes: - SavedDataStorageMixin.readSpongeMapData() was returning null unconditionally, breaking ALL SavedData persistence (return null -> return result) - Weather hasCeiling() logic in advanceWeatherCycle was inverted, preventing weather visual effects in overworld-type dimensions - DedicatedServerMixin constructor signature updated for new Optional<GameRules> parameter in 26.1-snapshot-6 Signed-off-by: Gabriel Harris-Rouquette <[email protected]>
1 parent 0d1f742 commit 522ed8d

22 files changed

Lines changed: 927 additions & 526 deletions

File tree

src/accessors/java/org/spongepowered/common/accessor/server/bossevents/CustomBossEventsAccessor.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
/*
2+
* This file is part of Sponge, licensed under the MIT License (MIT).
3+
*
4+
* Copyright (c) SpongePowered <https://www.spongepowered.org>
5+
* Copyright (c) contributors
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in
15+
* all copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
* THE SOFTWARE.
24+
*/
125
package org.spongepowered.common.accessor.server.bossevents;
226

327
import com.mojang.serialization.Codec;

src/accessors/java/org/spongepowered/common/accessor/world/entity/npc/wanderingtrader/WanderingTraderSpawnerAccessor.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import net.minecraft.world.level.saveddata.WanderingTraderData;
2929
import org.spongepowered.asm.mixin.Mixin;
3030
import org.spongepowered.asm.mixin.gen.Invoker;
31-
import org.spongepowered.common.UntransformedInvokerError;
3231

3332
@Mixin(WanderingTraderSpawner.class)
3433
public interface WanderingTraderSpawnerAccessor {

src/main/java/org/spongepowered/common/bridge/world/level/storage/LevelStorageAccessBridge.java

Lines changed: 0 additions & 30 deletions
This file was deleted.

src/main/java/org/spongepowered/common/bridge/world/level/storage/PrimaryLevelDataBridge.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@
2525
package org.spongepowered.common.bridge.world.level.storage;
2626

2727
import com.google.common.collect.BiMap;
28-
import com.mojang.serialization.Dynamic;
2928
import net.kyori.adventure.text.Component;
30-
import net.minecraft.nbt.CompoundTag;
31-
import net.minecraft.nbt.Tag;
3229
import net.minecraft.world.Difficulty;
3330
import net.minecraft.world.level.dimension.DimensionType;
3431
import net.minecraft.world.level.dimension.LevelStem;
@@ -67,8 +64,4 @@ public interface PrimaryLevelDataBridge extends ServerLevelDataBridge {
6764
void bridge$hardcore(boolean hardcore);
6865

6966
void bridge$allowCommands(boolean commands);
70-
71-
void bridge$readSpongeLevelData(Dynamic<Tag> impl$spongeLevelData);
72-
73-
CompoundTag bridge$writeSpongeLevelData();
7467
}

src/main/java/org/spongepowered/common/map/SpongeMapStorage.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@
3535
import org.spongepowered.api.world.DefaultWorldKeys;
3636
import org.spongepowered.common.SpongeCommon;
3737
import org.spongepowered.common.accessor.world.level.saveddata.maps.MapIndexAccessor;
38-
import org.spongepowered.common.bridge.world.level.storage.PrimaryLevelDataBridge;
3938
import org.spongepowered.common.event.SpongeCommonEventFactory;
4039
import org.spongepowered.common.event.tracking.PhaseTracker;
40+
import org.spongepowered.common.world.server.SpongeMapUUIDData;
4141

4242
import java.util.Collection;
4343
import java.util.HashMap;
@@ -120,7 +120,8 @@ public void addMapInfo(final MapInfo mapInfo) {
120120

121121
private void ensureHasMapUUIDIndex() {
122122
if (this.mapIdUUIDIndex == null) {
123-
this.mapIdUUIDIndex = ((PrimaryLevelDataBridge) Sponge.server().worldManager().world(DefaultWorldKeys.DEFAULT).get().properties()).bridge$getMapUUIDIndex();
123+
this.mapIdUUIDIndex = SpongeCommon.server().getDataStorage()
124+
.computeIfAbsent(SpongeMapUUIDData.TYPE).mapUUIDIndex();
124125
}
125126
}
126127
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* This file is part of Sponge, licensed under the MIT License (MIT).
3+
*
4+
* Copyright (c) SpongePowered <https://www.spongepowered.org>
5+
* Copyright (c) contributors
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in
15+
* all copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
* THE SOFTWARE.
24+
*/
25+
package org.spongepowered.common.world.server;
26+
27+
import net.minecraft.nbt.CompoundTag;
28+
import net.minecraft.nbt.NbtIo;
29+
import net.minecraft.nbt.NbtOps;
30+
import net.minecraft.nbt.NbtUtils;
31+
import net.minecraft.resources.ResourceKey;
32+
import net.minecraft.world.level.Level;
33+
import net.minecraft.world.level.dimension.DimensionType;
34+
import org.checkerframework.checker.nullness.qual.Nullable;
35+
import org.spongepowered.common.SpongeCommon;
36+
37+
import java.io.IOException;
38+
import java.nio.file.Files;
39+
import java.nio.file.Path;
40+
import java.util.Map;
41+
import java.util.Optional;
42+
import java.util.UUID;
43+
44+
/**
45+
* Handles migration of old Sponge world data (UUID, key) from the pre-26.1 format
46+
* to the new per-dimension {@link SpongeRegistryData} SavedData format.
47+
*
48+
* <p>The migration flow is:</p>
49+
* <ol>
50+
* <li>{@code FileFixerUpperMixin} extracts UUIDs from old {@code SpongeData} in level.dat
51+
* and per-dimension level.dat files BEFORE vanilla's file fixer rewrites them.</li>
52+
* <li>Cached identities are stored via {@link #cacheIdentities(Map)}.</li>
53+
* <li>{@code MinecraftServerMixin.loadLevel()} calls {@link #writeCachedRegistryData(Path)}
54+
* AFTER the file fix completes but BEFORE any ServerLevel is created.</li>
55+
* </ol>
56+
*/
57+
public final class SpongeLevelMigration {
58+
59+
// Static fields (not ThreadLocal) because fix() runs on "main" thread
60+
// but loadLevel() runs on "Server thread".
61+
private static volatile @Nullable Map<ResourceKey<Level>, UUID> cachedIdentities;
62+
private static volatile @Nullable CompoundTag cachedGlobalSpongeData;
63+
64+
private SpongeLevelMigration() {
65+
}
66+
67+
public static void cacheIdentities(final Map<ResourceKey<Level>, UUID> identities) {
68+
cachedIdentities = identities;
69+
}
70+
71+
public static void cacheGlobalSpongeData(final CompoundTag spongeData) {
72+
cachedGlobalSpongeData = spongeData;
73+
}
74+
75+
/**
76+
* Writes cached migrated identities to per-dimension {@code data/sponge/registry.dat} files.
77+
* Must be called AFTER FileFixerUpper.fix() completes (directory structure is final)
78+
* and BEFORE any ServerLevel is created (so SavedDataStorage finds files on disk).
79+
*/
80+
public static void writeCachedRegistryData(final Path worldDir) {
81+
final @Nullable Map<ResourceKey<Level>, UUID> identities = cachedIdentities;
82+
if (identities == null || identities.isEmpty()) {
83+
cachedIdentities = null;
84+
return;
85+
}
86+
cachedIdentities = null;
87+
88+
for (final var entry : identities.entrySet()) {
89+
final ResourceKey<Level> dimKey = entry.getKey();
90+
final UUID uuid = entry.getValue();
91+
try {
92+
final Path dimDir = DimensionType.getStorageFolder(dimKey, worldDir);
93+
final Path dataDir = dimDir.resolve("data").resolve("sponge");
94+
final Path registryFile = dataDir.resolve("registry.dat");
95+
if (Files.exists(registryFile)) {
96+
continue;
97+
}
98+
Files.createDirectories(dataDir);
99+
final SpongeRegistryData registryData = new SpongeRegistryData(uuid, Optional.of(dimKey));
100+
final var encoded = SpongeRegistryData.CODEC.encodeStart(NbtOps.INSTANCE, registryData);
101+
if (encoded.isSuccess()) {
102+
final CompoundTag savedDataTag = new CompoundTag();
103+
savedDataTag.put("data", encoded.getOrThrow());
104+
NbtUtils.addCurrentDataVersion(savedDataTag);
105+
NbtIo.writeCompressed(savedDataTag, registryFile);
106+
SpongeCommon.logger().info("[Sponge] Wrote migrated {} UUID {} -> {}", dimKey.identifier(), uuid, registryFile);
107+
}
108+
} catch (final IOException e) {
109+
SpongeCommon.logger().warn("[Sponge] Failed to write registry.dat for {}", dimKey.identifier(), e);
110+
}
111+
}
112+
113+
// Write global Sponge data (MapUUIDs, player-uuid-table) to server-level SavedData
114+
writeGlobalSpongeData(worldDir);
115+
}
116+
117+
private static void writeGlobalSpongeData(final Path worldDir) {
118+
final @Nullable CompoundTag spongeData = cachedGlobalSpongeData;
119+
cachedGlobalSpongeData = null;
120+
if (spongeData == null) {
121+
return;
122+
}
123+
124+
final Path dataDir = worldDir.resolve("data").resolve("sponge");
125+
final Path mapUUIDFile = dataDir.resolve("map_uuids.dat");
126+
if (Files.exists(mapUUIDFile)) {
127+
return; // Already migrated
128+
}
129+
130+
// Extract MapUUIDs and player-uuid-table from old SpongeData
131+
final var mapUUIDs = spongeData.getCompound("MapUUIDs").orElse(null);
132+
final var playerTable = spongeData.getList("player-uuid-table").orElse(null);
133+
134+
// Only write if there's actual data to migrate
135+
final boolean hasMapData = mapUUIDs != null && !mapUUIDs.isEmpty();
136+
final boolean hasPlayerData = playerTable != null && !playerTable.isEmpty();
137+
if (!hasMapData && !hasPlayerData) {
138+
return;
139+
}
140+
141+
try {
142+
Files.createDirectories(dataDir);
143+
// Write as a raw compound since the old format doesn't match the new codec exactly.
144+
// The SpongeMapUUIDData.CODEC will read the proper format; here we just preserve the raw data
145+
// for potential manual migration. The new SavedData will be created fresh on first access.
146+
final CompoundTag savedDataTag = new CompoundTag();
147+
final CompoundTag data = new CompoundTag();
148+
if (hasMapData) {
149+
data.put("legacy_map_uuids", mapUUIDs);
150+
}
151+
if (hasPlayerData) {
152+
data.put("legacy_player_uuids", playerTable);
153+
}
154+
savedDataTag.put("data", data);
155+
NbtUtils.addCurrentDataVersion(savedDataTag);
156+
NbtIo.writeCompressed(savedDataTag, mapUUIDFile);
157+
SpongeCommon.logger().info("[Sponge] Preserved global MapUUIDs/player-uuid-table -> {}", mapUUIDFile);
158+
} catch (final IOException e) {
159+
SpongeCommon.logger().warn("[Sponge] Failed to write global Sponge data", e);
160+
}
161+
}
162+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* This file is part of Sponge, licensed under the MIT License (MIT).
3+
*
4+
* Copyright (c) SpongePowered <https://www.spongepowered.org>
5+
* Copyright (c) contributors
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in
15+
* all copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
* THE SOFTWARE.
24+
*/
25+
package org.spongepowered.common.world.server;
26+
27+
import com.google.common.collect.BiMap;
28+
import com.google.common.collect.HashBiMap;
29+
import com.mojang.serialization.Codec;
30+
import com.mojang.serialization.codecs.RecordCodecBuilder;
31+
import net.minecraft.core.UUIDUtil;
32+
import net.minecraft.resources.Identifier;
33+
import net.minecraft.util.datafix.DataFixTypes;
34+
import net.minecraft.world.level.saveddata.SavedData;
35+
import net.minecraft.world.level.saveddata.SavedDataType;
36+
37+
import java.util.HashMap;
38+
import java.util.Map;
39+
import java.util.UUID;
40+
41+
/**
42+
* Stores Sponge's map ID → UUID index and player UUID table as server-global SavedData.
43+
* Previously stored in {@code SpongeData} within {@code level.dat}.
44+
*
45+
* <p>Persisted at {@code data/sponge/map_uuids.dat} in the server-level data directory.</p>
46+
*/
47+
public final class SpongeMapUUIDData extends SavedData {
48+
49+
private static final Codec<Map<Integer, UUID>> MAP_INDEX_CODEC =
50+
Codec.unboundedMap(Codec.INT, UUIDUtil.CODEC);
51+
52+
public static final Codec<SpongeMapUUIDData> CODEC = RecordCodecBuilder.create(
53+
i -> i.group(
54+
MAP_INDEX_CODEC.optionalFieldOf("map_uuids", Map.of()).forGetter(SpongeMapUUIDData::mapIndex),
55+
MAP_INDEX_CODEC.optionalFieldOf("player_uuids", Map.of()).forGetter(SpongeMapUUIDData::playerIndex)
56+
)
57+
.apply(i, SpongeMapUUIDData::new)
58+
);
59+
60+
public static final SavedDataType<SpongeMapUUIDData> TYPE = new SavedDataType<>(
61+
Identifier.fromNamespaceAndPath("sponge", "map_uuids"), SpongeMapUUIDData::new, CODEC, DataFixTypes.LEVEL
62+
);
63+
64+
private final BiMap<Integer, UUID> mapUUIDIndex;
65+
private final BiMap<Integer, UUID> playerUUIDIndex;
66+
67+
public SpongeMapUUIDData() {
68+
this.mapUUIDIndex = HashBiMap.create();
69+
this.playerUUIDIndex = HashBiMap.create();
70+
}
71+
72+
public SpongeMapUUIDData(final Map<Integer, UUID> mapIndex, final Map<Integer, UUID> playerIndex) {
73+
this.mapUUIDIndex = HashBiMap.create(mapIndex);
74+
this.playerUUIDIndex = HashBiMap.create(playerIndex);
75+
}
76+
77+
private Map<Integer, UUID> mapIndex() {
78+
return new HashMap<>(this.mapUUIDIndex);
79+
}
80+
81+
private Map<Integer, UUID> playerIndex() {
82+
return new HashMap<>(this.playerUUIDIndex);
83+
}
84+
85+
public BiMap<Integer, UUID> mapUUIDIndex() {
86+
return this.mapUUIDIndex;
87+
}
88+
89+
public BiMap<Integer, UUID> playerUUIDIndex() {
90+
return this.playerUUIDIndex;
91+
}
92+
}

0 commit comments

Comments
 (0)