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"