diff --git a/build.gradle.kts b/build.gradle.kts
index 6e38347..1aedac3 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -90,6 +90,10 @@ subprojects {
includeGroup("lol.bai")
}
}
+
+ maven("https://maven.ryanhcode.dev/releases")
+ maven("https://raw.githubusercontent.com/Fuzss/modresources/main/maven/")
+ maven("https://maven.blamejared.com")
}
dependencies {
diff --git a/common/build.gradle.kts b/common/build.gradle.kts
index 4ecd318..c6c29da 100644
--- a/common/build.gradle.kts
+++ b/common/build.gradle.kts
@@ -2,6 +2,7 @@ val modId = project.property("mod.id").toString()
val minecraft = project.property("minecraft.version").toString()
val yarn = project.property("fabric.yarn.build").toString()
val fabricLoader = project.property("fabric.loader.version").toString()
+val sable = project.property("sable.version").toString()
plugins {
id("fabric-loom")
@@ -28,4 +29,6 @@ dependencies {
// loom expects some loader classes to exist, provides mixin and mixin-extras too
modCompileOnly("net.fabricmc:fabric-loader:${fabricLoader}")
+
+ compileOnly("dev.ryanhcode.sable:sable-common-$minecraft:$sable")
}
diff --git a/common/src/main/java/dev/galacticraft/dynamicdimensions/api/DynamicDimensionProperties.java b/common/src/main/java/dev/galacticraft/dynamicdimensions/api/DynamicDimensionProperties.java
new file mode 100644
index 0000000..ca9512c
--- /dev/null
+++ b/common/src/main/java/dev/galacticraft/dynamicdimensions/api/DynamicDimensionProperties.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2021-2025 Team Galacticraft
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package dev.galacticraft.dynamicdimensions.api;
+
+import org.joml.Vector3f;
+
+/**
+ * Generic physics/environment properties for a dynamic dimension.
+ *
+ * DynamicDimensions does not decide what these values mean.
+ * Optional compat layers, such as Sable compat, may consume them.
+ */
+public abstract class DynamicDimensionProperties {
+ public abstract int priority();
+
+ public abstract Vector3f baseGravity();
+
+ public abstract double basePressure();
+
+ public abstract float universalDrag();
+
+ public abstract Vector3f magneticNorth();
+}
\ No newline at end of file
diff --git a/common/src/main/java/dev/galacticraft/dynamicdimensions/api/DynamicDimensionRegistry.java b/common/src/main/java/dev/galacticraft/dynamicdimensions/api/DynamicDimensionRegistry.java
index b5ecfa4..ff4ecbe 100644
--- a/common/src/main/java/dev/galacticraft/dynamicdimensions/api/DynamicDimensionRegistry.java
+++ b/common/src/main/java/dev/galacticraft/dynamicdimensions/api/DynamicDimensionRegistry.java
@@ -122,6 +122,13 @@ default boolean canDeleteDimension(@NotNull ResourceLocation id) {
*/
@Nullable ServerLevel createDynamicDimension(@NotNull ResourceLocation id, @NotNull ChunkGenerator chunkGenerator, @NotNull DimensionType type);
+ /**
+ * Registers a new dimension and applies optional dynamic-dimension properties.
+ *
+ * @since 0.10.0
+ */
+ @Nullable ServerLevel createDynamicDimension(@NotNull ResourceLocation id, @NotNull ChunkGenerator chunkGenerator, @NotNull DimensionType type, @NotNull DynamicDimensionProperties properties);
+
/**
* Registers a new dimension and updates all clients with the new dimension.
* If world data already exists for this dimension it will be used, otherwise it will be created.
@@ -137,6 +144,67 @@ default boolean canDeleteDimension(@NotNull ResourceLocation id) {
*/
@Nullable ServerLevel loadDynamicDimension(@NotNull ResourceLocation id, @NotNull ChunkGenerator chunkGenerator, @NotNull DimensionType type);
+ /**
+ * Loads a dynamic dimension and applies optional dynamic-dimension properties.
+ *
+ * @since 0.10.0
+ */
+ @Nullable ServerLevel loadDynamicDimension(@NotNull ResourceLocation id, @NotNull ChunkGenerator chunkGenerator, @NotNull DimensionType type, @NotNull DynamicDimensionProperties properties);
+
+ /**
+ * Sets properties for a dynamic dimension.
+ *
+ *
If the dimension is already loaded, compatible backends such as Sable
+ * may apply these immediately.
+ *
+ * @since 0.10.0
+ */
+ void setDimensionProperties(@NotNull ResourceKey key, @NotNull DynamicDimensionProperties properties);
+
+ /**
+ * Sets properties for a dynamic dimension.
+ *
+ * @since 0.10.0
+ */
+ default void setDimensionProperties(@NotNull ResourceLocation id, @NotNull DynamicDimensionProperties properties) {
+ this.setDimensionProperties(ResourceKey.create(Registries.DIMENSION, id), properties);
+ }
+
+ /**
+ * Gets the currently stored properties for a dynamic dimension.
+ *
+ * @since 0.10.0
+ */
+ @Nullable DynamicDimensionProperties getDimensionProperties(@NotNull ResourceKey key);
+
+ /**
+ * Gets the currently stored properties for a dynamic dimension.
+ *
+ * @since 0.10.0
+ */
+ default @Nullable DynamicDimensionProperties getDimensionProperties(@NotNull ResourceLocation id) {
+ return this.getDimensionProperties(ResourceKey.create(Registries.DIMENSION, id));
+ }
+
+ /**
+ * Clears stored properties for a dynamic dimension.
+ *
+ * If the dimension has been applied to a compatible backend such as Sable,
+ * this should also remove those backend properties.
+ *
+ * @since 0.10.0
+ */
+ void clearDimensionProperties(@NotNull ResourceKey key);
+
+ /**
+ * Clears stored properties for a dynamic dimension.
+ *
+ * @since 0.10.0
+ */
+ default void clearDimensionProperties(@NotNull ResourceLocation id) {
+ this.clearDimensionProperties(ResourceKey.create(Registries.DIMENSION, id));
+ }
+
/**
* Deletes a dynamic dimension from the server.
* This may delete the dimension files permanently.
diff --git a/common/src/main/java/dev/galacticraft/dynamicdimensions/api/event/DynamicDimensionLoadCallback.java b/common/src/main/java/dev/galacticraft/dynamicdimensions/api/event/DynamicDimensionLoadCallback.java
index 4e9b807..6e9b849 100644
--- a/common/src/main/java/dev/galacticraft/dynamicdimensions/api/event/DynamicDimensionLoadCallback.java
+++ b/common/src/main/java/dev/galacticraft/dynamicdimensions/api/event/DynamicDimensionLoadCallback.java
@@ -22,7 +22,11 @@
package dev.galacticraft.dynamicdimensions.api.event;
+import dev.galacticraft.dynamicdimensions.api.DynamicDimensionProperties;
+import dev.galacticraft.dynamicdimensions.api.DynamicDimensionRegistry;
import dev.galacticraft.dynamicdimensions.impl.platform.Services;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
@@ -58,5 +62,20 @@ interface DynamicDimensionLoader {
* @return the newly loaded level
*/
@NotNull ServerLevel loadDynamicDimension(@NotNull ResourceLocation id, @NotNull ChunkGenerator chunkGenerator, @NotNull DimensionType type);
+
+ /**
+ * Creates/loads a new dynamic dimension and applies the given properties before level construction,
+ * so that compatible backends (e.g. Sable) receive the correct physics data from the start.
+ * @param id the id of the dimension. Must be free in the level stem, dimension type, and level registries.
+ * @param chunkGenerator the chunk generator to generate the dimension with
+ * @param type the dimension type
+ * @param properties the dimension properties to apply
+ * @param server the current minecraft server
+ * @return the newly loaded level
+ */
+ default @NotNull ServerLevel loadDynamicDimension(@NotNull ResourceLocation id, @NotNull ChunkGenerator chunkGenerator, @NotNull DimensionType type, @NotNull DynamicDimensionProperties properties, @NotNull MinecraftServer server) {
+ DynamicDimensionRegistry.from(server).setDimensionProperties(ResourceKey.create(Registries.DIMENSION, id), properties);
+ return this.loadDynamicDimension(id, chunkGenerator, type);
+ }
}
}
diff --git a/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/DynamicDimensionRegistryImpl.java b/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/DynamicDimensionRegistryImpl.java
index 0c22171..967bce5 100644
--- a/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/DynamicDimensionRegistryImpl.java
+++ b/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/DynamicDimensionRegistryImpl.java
@@ -23,11 +23,13 @@
package dev.galacticraft.dynamicdimensions.impl;
import com.google.common.collect.ImmutableList;
+import dev.galacticraft.dynamicdimensions.api.DynamicDimensionProperties;
import dev.galacticraft.dynamicdimensions.api.DynamicDimensionRegistry;
import dev.galacticraft.dynamicdimensions.api.PlayerRemover;
import dev.galacticraft.dynamicdimensions.api.event.DynamicDimensionLoadCallback;
import dev.galacticraft.dynamicdimensions.impl.accessor.DynamicDimensionProvider;
import dev.galacticraft.dynamicdimensions.impl.accessor.PrimaryLevelDataAccessor;
+import dev.galacticraft.dynamicdimensions.impl.compat.DynamicDimensionPhysicsCompat;
import dev.galacticraft.dynamicdimensions.impl.mixin.*;
import dev.galacticraft.dynamicdimensions.impl.network.S2CPackets;
import dev.galacticraft.dynamicdimensions.impl.registry.RegistryUtil;
@@ -60,7 +62,9 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.stream.Collectors;
public class DynamicDimensionRegistryImpl implements DynamicDimensionRegistry {
@@ -69,6 +73,8 @@ public class DynamicDimensionRegistryImpl implements DynamicDimensionRegistry {
private final Registry dimTypes;
private final Registry stems;
+ private final Map, DynamicDimensionProperties> dimensionProperties = new HashMap<>();
+
public DynamicDimensionRegistryImpl(MinecraftServer server) {
this.server = server;
this.dimTypes = server.registryAccess().registryOrThrow(Registries.DIMENSION_TYPE);
@@ -81,6 +87,13 @@ public void loadDynamicDimensions() {
Constants.LOGGER.debug("Loading dynamic dimension '{}'", id);
ResourceKey key = ResourceKey.create(Registries.DIMENSION, id);
+ // If properties were already registered before this callback fires
+ // (e.g. by the caller calling setDimensionProperties first), stage them now.
+ DynamicDimensionProperties properties = this.dimensionProperties.get(key);
+ if (properties != null) {
+ DynamicDimensionPhysicsCompat.stage(key, properties);
+ }
+
return this.createDynamicLevel(id, chunkGenerator, type, key);
});
@@ -92,11 +105,58 @@ public void loadDynamicDimensions() {
return this.createDynamicLevel(id, generator, type, true);
}
+ @Override
+ public @Nullable ServerLevel createDynamicDimension(@NotNull ResourceLocation id, @NotNull ChunkGenerator generator, @NotNull DimensionType type, @NotNull DynamicDimensionProperties properties) {
+ ResourceKey key = ResourceKey.create(Registries.DIMENSION, id);
+ this.setDimensionProperties(key, properties);
+
+ ServerLevel level = this.createDynamicLevel(id, generator, type, true);
+ if (level == null) {
+ this.clearDimensionProperties(key);
+ }
+
+ return level;
+ }
+
@Override
public @Nullable ServerLevel loadDynamicDimension(@NotNull ResourceLocation id, @NotNull ChunkGenerator generator, @NotNull DimensionType type) {
return this.createDynamicLevel(id, generator, type, false);
}
+ @Override
+ public @Nullable ServerLevel loadDynamicDimension(@NotNull ResourceLocation id, @NotNull ChunkGenerator generator, @NotNull DimensionType type, @NotNull DynamicDimensionProperties properties) {
+ ResourceKey key = ResourceKey.create(Registries.DIMENSION, id);
+ this.setDimensionProperties(key, properties);
+
+ ServerLevel level = this.createDynamicLevel(id, generator, type, false);
+ if (level == null) {
+ this.clearDimensionProperties(key);
+ }
+
+ return level;
+ }
+
+ @Override
+ public void setDimensionProperties(@NotNull ResourceKey key, @NotNull DynamicDimensionProperties properties) {
+ this.dimensionProperties.put(key, properties);
+
+ if (this.server.getLevel(key) != null) {
+ DynamicDimensionPhysicsCompat.apply(key, properties);
+ }
+ }
+
+ @Override
+ public @Nullable DynamicDimensionProperties getDimensionProperties(@NotNull ResourceKey key) {
+ return this.dimensionProperties.get(key);
+ }
+
+
+ @Override
+ public void clearDimensionProperties(@NotNull ResourceKey key) {
+ this.dimensionProperties.remove(key);
+ DynamicDimensionPhysicsCompat.remove(key);
+ }
+
@Override
public boolean dynamicDimensionExists(@NotNull ResourceKey key) {
return this.dynamicDimensions.contains(key) || ((DynamicDimensionProvider) this.server).dynamicdimensions$isIdPendingCreation(key);
@@ -125,7 +185,9 @@ public boolean deleteDynamicDimension(@NotNull ResourceLocation id, @Nullable Pl
ResourceKey key = ResourceKey.create(Registries.DIMENSION, id);
if (!this.canDeleteDimension(key)) return false;
+ DynamicDimensionPhysicsCompat.remove(key);
((DynamicDimensionProvider) this.server).dynamicdimensions$removeLevel(key, remover, true);
+ this.dimensionProperties.remove(key);
return true;
}
@@ -136,7 +198,9 @@ public boolean unloadDynamicDimension(@NotNull ResourceLocation id, @Nullable Pl
ResourceKey key = ResourceKey.create(Registries.DIMENSION, id);
if (!this.canDeleteDimension(key)) return false;
+ DynamicDimensionPhysicsCompat.remove(key);
((DynamicDimensionProvider) this.server).dynamicdimensions$removeLevel(key, remover, false);
+
return true;
}
@@ -162,8 +226,14 @@ public boolean unloadDynamicDimension(@NotNull ResourceLocation id, @Nullable Pl
}
private @NotNull ServerLevel createDynamicLevel(ResourceKey key, WorldData worldData, LevelStem stem, ServerLevel overworld) {
- // -- start createLevels --
- final DerivedLevelData data = new DerivedLevelData(worldData, worldData.overworldData()); //todo: do we want separate data?
+ // Stage physics properties before ServerLevel construction so Sable's
+ // SubLevelPhysicsSystem.initialize() mixin can flush them before reading gravity.
+ DynamicDimensionProperties pendingProperties = this.dimensionProperties.get(key);
+ if (pendingProperties != null) {
+ DynamicDimensionPhysicsCompat.stage(key, pendingProperties);
+ }
+
+ final DerivedLevelData data = new DerivedLevelData(worldData, worldData.overworldData());
final ServerLevel level = new ServerLevel(
this.server,
((MinecraftServerAccessor) this.server).getExecutor(),
@@ -179,17 +249,13 @@ public boolean unloadDynamicDimension(@NotNull ResourceLocation id, @Nullable Pl
null
);
overworld.getWorldBorder().addListener(new BorderChangeListener.DelegateBorderChangeListener(level.getWorldBorder()));
- // -- end createLevels --
- // see PlayerList
level.getChunkSource().setSimulationDistance(((DistanceManagerAccessor) ((ServerChunkCacheAccessor) overworld.getChunkSource()).getDistanceManager()).getSimulationDistance());
level.getChunkSource().setViewDistance(((ChunkMapAccessor) overworld.getChunkSource().chunkMap).getViewDistance());
- // -- start prepareLevels --
ForcedChunksSavedData forcedChunksSavedData = level.getDataStorage().get(ForcedChunksSavedData.factory(), "chunks");
if (forcedChunksSavedData != null) {
LongIterator longIterator = forcedChunksSavedData.getChunks().iterator();
-
while (longIterator.hasNext()) {
long l = longIterator.nextLong();
ChunkPos chunkPos = new ChunkPos(l);
@@ -198,10 +264,14 @@ public boolean unloadDynamicDimension(@NotNull ResourceLocation id, @Nullable Pl
}
level.setSpawnSettings(this.server.isSpawningMonsters(), this.server.isSpawningAnimals());
- // -- end prepareLevels --
((DynamicDimensionProvider) this.server).dynamicdimensions$registerLevel(level);
+ // Belt-and-suspenders apply after level exists, covers setDimensionProperties called post-creation.
+ if (pendingProperties != null) {
+ DynamicDimensionPhysicsCompat.apply(key, pendingProperties);
+ }
+
final var serializedType = ((CompoundTag) DimensionType.DIRECT_CODEC.encode(stem.type().value(), RegistryOps.create(NbtOps.INSTANCE, this.server.registryAccess()), new CompoundTag()).getOrThrow());
for (ServerPlayer player : this.server.getPlayerList().getPlayers()) {
S2CPackets.sendCreateDimension(player, key.location(), serializedType);
diff --git a/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/compat/DynamicDimensionPhysicsCompat.java b/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/compat/DynamicDimensionPhysicsCompat.java
new file mode 100644
index 0000000..9bd1977
--- /dev/null
+++ b/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/compat/DynamicDimensionPhysicsCompat.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2021-2025 Team Galacticraft
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package dev.galacticraft.dynamicdimensions.impl.compat;
+
+import dev.galacticraft.dynamicdimensions.api.DynamicDimensionProperties;
+import dev.galacticraft.dynamicdimensions.impl.platform.Services;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.world.level.Level;
+
+import static dev.galacticraft.dynamicdimensions.impl.compat.SableDimensionPhysicsCompat.SABLE_MOD_ID;
+
+public final class DynamicDimensionPhysicsCompat {
+ private DynamicDimensionPhysicsCompat() {
+ }
+
+ public static void apply(ResourceKey key, DynamicDimensionProperties properties) {
+ if (Services.PLATFORM.isModLoaded(SABLE_MOD_ID)) {
+ SableDimensionPhysicsCompat.apply(key, properties);
+ }
+ }
+
+ public static void remove(ResourceKey key) {
+ if (Services.PLATFORM.isModLoaded(SABLE_MOD_ID)) {
+ SableDimensionPhysicsCompat.remove(key);
+ }
+ }
+
+ public static void stage(final ResourceKey key, final DynamicDimensionProperties properties) {
+ if (Services.PLATFORM.isModLoaded(SABLE_MOD_ID)) {
+ SableDimensionPhysicsCompat.stage(key, properties);
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/compat/SableDimensionPhysicsCompat.java b/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/compat/SableDimensionPhysicsCompat.java
new file mode 100644
index 0000000..7387062
--- /dev/null
+++ b/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/compat/SableDimensionPhysicsCompat.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2021-2025 Team Galacticraft
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package dev.galacticraft.dynamicdimensions.impl.compat;
+
+import dev.galacticraft.dynamicdimensions.api.DynamicDimensionProperties;
+import dev.galacticraft.dynamicdimensions.impl.Constants;
+import dev.galacticraft.dynamicdimensions.impl.platform.Services;
+import dev.ryanhcode.sable.physics.config.dimension_physics.DimensionPhysics;
+import dev.ryanhcode.sable.physics.config.dimension_physics.DimensionPhysicsData;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.world.level.Level;
+import org.joml.Vector3f;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public final class SableDimensionPhysicsCompat {
+ static final String SABLE_MOD_ID = "sable";
+
+ /**
+ * Properties staged before SubLevelPhysicsSystem.initialize() runs for a newly created dynamic dimension.
+ * Flushed into DIMENSION_PHYSICS_DATA by the mixin at the HEAD of initialize(), before gravity is ever read.
+ */
+ private static final Map, DynamicDimensionProperties> PENDING_PROPERTIES = new HashMap<>();
+
+ private SableDimensionPhysicsCompat() {
+ }
+
+ /**
+ * Call this synchronously at the very start of createDynamicDimension, before any deferred work is queued.
+ * Ensures properties are present in PENDING_PROPERTIES before SubLevelPhysicsSystem.initialize() fires.
+ */
+ static void stage(final ResourceKey key, final DynamicDimensionProperties properties) {
+ if (!Services.PLATFORM.isModLoaded(SABLE_MOD_ID)) {
+ return;
+ }
+ PENDING_PROPERTIES.put(key, properties);
+ }
+
+ /**
+ * Called from the mixin on SubLevelPhysicsSystem.initialize() at HEAD.
+ * Drains the pending entry for this dimension into DIMENSION_PHYSICS_DATA before gravity is read.
+ */
+ public static void flushForLevel(final ResourceKey key) {
+ final DynamicDimensionProperties pending = PENDING_PROPERTIES.remove(key);
+ if (pending == null || !Services.PLATFORM.isModLoaded(SABLE_MOD_ID)) {
+ return;
+ }
+
+ try {
+ final Map, DimensionPhysics> map = physicsMap();
+ final DimensionPhysics physics = buildPhysics(key, pending);
+ final DimensionPhysics existing = map.get(key);
+
+ if (existing == null || existing.priority() <= pending.priority()) {
+ map.put(key, physics);
+ clearDefault(key);
+ }
+ } catch (final Throwable throwable) {
+ Constants.LOGGER.warn(
+ "Failed to flush staged Sable physics for dynamic dimension '{}'",
+ key.location(),
+ throwable
+ );
+ }
+ }
+
+ /**
+ * Directly writes properties into DIMENSION_PHYSICS_DATA.
+ * Safe to call after the level exists, but on its own is insufficient — Rapier reads gravity only once
+ * at initialize() time. Use stage() + the mixin flush to cover that window instead.
+ */
+ static void apply(final ResourceKey key, final DynamicDimensionProperties properties) {
+ if (!Services.PLATFORM.isModLoaded(SABLE_MOD_ID)) {
+ return;
+ }
+
+ try {
+ final Map, DimensionPhysics> map = physicsMap();
+ final DimensionPhysics physics = buildPhysics(key, properties);
+ final DimensionPhysics existing = map.get(key);
+
+ if (existing == null || existing.priority() <= properties.priority()) {
+ map.put(key, physics);
+ clearDefault(key);
+ }
+ } catch (final Throwable throwable) {
+ Constants.LOGGER.warn(
+ "Failed to apply Sable physics properties for dynamic dimension '{}'",
+ key.location(),
+ throwable
+ );
+ }
+ }
+
+ static void remove(final ResourceKey key) {
+ if (!Services.PLATFORM.isModLoaded(SABLE_MOD_ID)) {
+ return;
+ }
+
+ // Clear any staged-but-not-yet-flushed entry so it doesn't leak into a future dimension with the same key.
+ PENDING_PROPERTIES.remove(key);
+
+ try {
+ physicsMap().remove(key);
+ } catch (final Throwable throwable) {
+ Constants.LOGGER.warn(
+ "Failed to remove Sable physics properties for dynamic dimension '{}'",
+ key.location(),
+ throwable
+ );
+ }
+ }
+
+ private static DimensionPhysics buildPhysics(final ResourceKey key, final DynamicDimensionProperties properties) {
+ return new DimensionPhysics(
+ key.location(),
+ properties.priority(),
+ Optional.of(properties.universalDrag()),
+ Optional.of(copy(properties.baseGravity())),
+ Optional.of(properties.basePressure()),
+ Optional.empty(),
+ Optional.of(copy(properties.magneticNorth()))
+ );
+ }
+
+ private static void clearDefault(final ResourceKey key) throws ReflectiveOperationException {
+ final Field field = DimensionPhysicsData.class.getDeclaredField("DEFAULT_DIMENSION_PHYSICS_DATA");
+ field.setAccessible(true);
+
+ @SuppressWarnings("unchecked")
+ final Map, DimensionPhysics> defaults =
+ (Map, DimensionPhysics>) field.get(null);
+
+ defaults.remove(key);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map, DimensionPhysics> physicsMap() throws ReflectiveOperationException {
+ final Field field = DimensionPhysicsData.class.getDeclaredField("DIMENSION_PHYSICS_DATA");
+ field.setAccessible(true);
+ return (Map, DimensionPhysics>) field.get(null);
+ }
+
+ private static Vector3f copy(final Vector3f vector) {
+ return new Vector3f(vector.x(), vector.y(), vector.z());
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/mixin/compat/sable/SableMixinPlugin.java b/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/mixin/compat/sable/SableMixinPlugin.java
new file mode 100644
index 0000000..45878ad
--- /dev/null
+++ b/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/mixin/compat/sable/SableMixinPlugin.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2021-2025 Team Galacticraft
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package dev.galacticraft.dynamicdimensions.impl.mixin.compat.sable;
+
+import dev.galacticraft.dynamicdimensions.impl.Constants;
+import org.objectweb.asm.tree.ClassNode;
+import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
+import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
+
+import java.util.List;
+import java.util.Set;
+
+public class SableMixinPlugin implements IMixinConfigPlugin {
+
+ private boolean sableLoaded;
+
+ @Override
+ public void onLoad(final String mixinPackage) {
+ try {
+ Class> loaderClass = Class.forName("net.fabricmc.loader.api.FabricLoader");
+ Object instance = loaderClass.getMethod("getInstance").invoke(null);
+ this.sableLoaded = (boolean) loaderClass.getMethod("isModLoaded", String.class).invoke(instance, "sable");
+ Constants.LOGGER.info("Sable detected via FabricLoader: {}", this.sableLoaded);
+ } catch (final Exception e) {
+ // Not on Fabric, fall back to checking for NeoForge mod list
+ try {
+ Class> modListClass = Class.forName("net.neoforged.fml.ModList");
+ Object modList = modListClass.getMethod("get").invoke(null);
+ this.sableLoaded = (boolean) modListClass.getMethod("isLoaded", String.class).invoke(modList, "sable");
+ Constants.LOGGER.info("Sable detected via ModList: {}", this.sableLoaded);
+ } catch (final Exception ex) {
+ this.sableLoaded = false;
+ Constants.LOGGER.info("Could not determine if Sable is loaded: {}", ex.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public boolean shouldApplyMixin(final String targetClassName, final String mixinClassName) {
+ return this.sableLoaded;
+ }
+
+ @Override public String getRefMapperConfig() { return null; }
+ @Override public void acceptTargets(Set myTargets, Set otherTargets) {}
+ @Override public List getMixins() { return null; }
+ @Override public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {}
+ @Override public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {}
+}
\ No newline at end of file
diff --git a/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/mixin/compat/sable/SubLevelPhysicsSystemMixin.java b/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/mixin/compat/sable/SubLevelPhysicsSystemMixin.java
new file mode 100644
index 0000000..27149b9
--- /dev/null
+++ b/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/mixin/compat/sable/SubLevelPhysicsSystemMixin.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2021-2025 Team Galacticraft
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package dev.galacticraft.dynamicdimensions.impl.mixin.compat.sable;
+
+import dev.galacticraft.dynamicdimensions.impl.compat.SableDimensionPhysicsCompat;
+import net.minecraft.server.level.ServerLevel;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(targets = "dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem", remap = false)
+public class SubLevelPhysicsSystemMixin {
+
+ @Shadow
+ private ServerLevel level;
+
+ @Inject(method = "initialize", at = @At("HEAD"))
+ private void dynamicdimensions$flushPendingPhysics(final CallbackInfo ci) {
+ SableDimensionPhysicsCompat.flushForLevel(this.level.dimension());
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/platform/services/PlatformHelper.java b/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/platform/services/PlatformHelper.java
index 1b5d031..07f77bc 100644
--- a/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/platform/services/PlatformHelper.java
+++ b/common/src/main/java/dev/galacticraft/dynamicdimensions/impl/platform/services/PlatformHelper.java
@@ -46,4 +46,6 @@ public interface PlatformHelper {
void invokeAddedEvent(@NotNull ResourceKey key, @NotNull ServerLevel level);
void invokeLoadEvent(MinecraftServer server, DynamicDimensionLoadCallback.DynamicDimensionLoader loader);
+
+ boolean isModLoaded(String modId);
}
diff --git a/common/src/main/resources/dynamicdimensions.sable.mixins.json b/common/src/main/resources/dynamicdimensions.sable.mixins.json
new file mode 100644
index 0000000..1a1600e
--- /dev/null
+++ b/common/src/main/resources/dynamicdimensions.sable.mixins.json
@@ -0,0 +1,12 @@
+{
+ "required": false,
+ "package": "dev.galacticraft.dynamicdimensions.impl.mixin.compat.sable",
+ "compatibilityLevel": "JAVA_21",
+ "plugin": "dev.galacticraft.dynamicdimensions.impl.mixin.compat.sable.SableMixinPlugin",
+ "mixins": [
+ "SubLevelPhysicsSystemMixin"
+ ],
+ "injectors": {
+ "defaultRequire": 0
+ }
+}
\ No newline at end of file
diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts
index fd9840e..5b4d2da 100644
--- a/fabric/build.gradle.kts
+++ b/fabric/build.gradle.kts
@@ -5,6 +5,8 @@ val fabricLoader = project.property("fabric.loader.version").toString()
val fabricAPI = project.property("fabric.api.version").toString()
val fabricModules = project.property("fabric.api.modules").toString().split(',')
val badpackets = project.property("badpackets.version").toString()
+val sable = project.property("sable.version").toString()
+val veil = project.property("veil.version").toString()
plugins {
id("fabric-loom")
@@ -60,6 +62,7 @@ dependencies {
}
modRuntimeOnly("net.fabricmc.fabric-api:fabric-api:$fabricAPI")
modRuntimeOnly("lol.bai:badpackets:fabric-$badpackets")
+ compileOnly("dev.ryanhcode.sable:sable-common-$minecraft:$sable")
}
tasks.compileJava {
diff --git a/fabric/src/main/java/dev/galacticraft/dynamicdimensions/impl/fabric/platform/FabricPlatformHelper.java b/fabric/src/main/java/dev/galacticraft/dynamicdimensions/impl/fabric/platform/FabricPlatformHelper.java
index ca3d689..91227c3 100644
--- a/fabric/src/main/java/dev/galacticraft/dynamicdimensions/impl/fabric/platform/FabricPlatformHelper.java
+++ b/fabric/src/main/java/dev/galacticraft/dynamicdimensions/impl/fabric/platform/FabricPlatformHelper.java
@@ -29,6 +29,7 @@
import dev.galacticraft.dynamicdimensions.impl.fabric.DynamicDimensionsFabric;
import dev.galacticraft.dynamicdimensions.impl.fabric.config.DynamicDimensionsConfigImpl;
import dev.galacticraft.dynamicdimensions.impl.platform.services.PlatformHelper;
+import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
@@ -72,4 +73,9 @@ public void invokeAddedEvent(@NotNull ResourceKey key, @NotNull ServerLev
public void invokeLoadEvent(MinecraftServer server, DynamicDimensionLoadCallback.DynamicDimensionLoader loader) {
DynamicDimensionsFabric.DIMENSION_LOAD_EVENT.invoker().loadDimensions(server, loader);
}
+
+ @Override
+ public boolean isModLoaded(String modId) {
+ return FabricLoader.getInstance().isModLoaded(modId);
+ }
}
diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json
index b2b6860..0e4986c 100644
--- a/fabric/src/main/resources/fabric.mod.json
+++ b/fabric/src/main/resources/fabric.mod.json
@@ -41,7 +41,8 @@
]
},
"mixins": [
- "${mod_id}.mixins.json"
+ "${mod_id}.mixins.json",
+ "${mod_id}.sable.mixins.json"
],
"accessWidener": "${mod_id}.accesswidener",
"depends": {
diff --git a/gradle.properties b/gradle.properties
index b973903..3e3bb5d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -5,7 +5,7 @@ mod.id=dynamicdimensions
mod.name=Dynamic Dimensions
mod.description=Library to facilitate the runtime addition and removal of Minecraft dimensions.
mod.license=MIT
-mod.version=0.9.1
+mod.version=0.10.0
# Minecraft setup
minecraft.version=1.21.1
@@ -34,3 +34,6 @@ fabric.api.modules=\
fabric-command-api-v2,\
fabric-lifecycle-events-v1,\
fabric-resource-loader-v0
+
+sable.version=1.2.2
+veil.version=1.21.1:3.6.2
\ No newline at end of file
diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts
index ea932ac..b36271d 100644
--- a/neoforge/build.gradle.kts
+++ b/neoforge/build.gradle.kts
@@ -11,6 +11,8 @@ val parchmentVersion = project.property("parchment.version").toString()
val badpackets = project.property("badpackets.version").toString()
+val sable = project.property("sable.version").toString()
+
plugins {
`java-library`
`maven-publish`
@@ -61,6 +63,7 @@ neoForge {
dependencies {
compileOnly(project(":common", "namedElements"))
runtimeOnly("lol.bai:badpackets:neo-$badpackets")
+ compileOnly("dev.ryanhcode.sable:sable-neoforge-$minecraft:$sable")
}
tasks.compileJava {
@@ -72,14 +75,17 @@ tasks.processResources {
// remove refmap on neoforge
doLast {
- file(outputs.files.asFileTree.first { it.name.equals("dynamicdimensions.mixins.json") }.apply {
- val parse = groovy.json.JsonSlurper().parse(this)!! as MutableMap<*, *>
- parse.remove("refmap")
- writeText(groovy.json.JsonOutput.toJson(parse))
- })
+ listOf("dynamicdimensions.mixins.json", "dynamicdimensions.sable.mixins.json").forEach { configName ->
+ val configFile = outputs.files.asFileTree.find { it.name == configName }
+ if (configFile != null) {
+ val parse = groovy.json.JsonSlurper().parse(configFile)!! as MutableMap<*, *>
+ parse.remove("refmap")
+ configFile.writeText(groovy.json.JsonOutput.toJson(parse))
+ }
+ }
}
}
tasks.javadoc {
source(project(":common").sourceSets.main.get().allJava)
-}
+}
\ No newline at end of file
diff --git a/neoforge/src/main/java/dev/galacticraft/dynamicdimensions/impl/forge/platform/ForgePlatformHelper.java b/neoforge/src/main/java/dev/galacticraft/dynamicdimensions/impl/forge/platform/ForgePlatformHelper.java
index c293e64..334b0fd 100644
--- a/neoforge/src/main/java/dev/galacticraft/dynamicdimensions/impl/forge/platform/ForgePlatformHelper.java
+++ b/neoforge/src/main/java/dev/galacticraft/dynamicdimensions/impl/forge/platform/ForgePlatformHelper.java
@@ -32,6 +32,7 @@
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
+import net.neoforged.fml.ModList;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@@ -84,4 +85,9 @@ public void invokeLoadEvent(MinecraftServer server, DynamicDimensionLoadCallback
loadCallback.loadDimensions(server, loader);
}
}
+
+ @Override
+ public boolean isModLoaded(String modId) {
+ return ModList.get().isLoaded(modId);
+ }
}
diff --git a/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/main/resources/META-INF/neoforge.mods.toml
index a2fa5ed..0863bd4 100644
--- a/neoforge/src/main/resources/META-INF/neoforge.mods.toml
+++ b/neoforge/src/main/resources/META-INF/neoforge.mods.toml
@@ -16,6 +16,8 @@ ${mod_description}
[[mixins]]
config="${mod_id}.mixins.json"
+[[mixins]]
+config = "${mod_id}.sable.mixins.json"
[[dependencies.${mod_id}]]
modId = "badpackets"