Public Access
This commit is contained in:
1 parent
c0bc91858e
commit
b06fbda461
10 files changed
+242
-5
No files matched your search
@@ -0,0 +1,39 @@
|
|||||||
|
# Age administration
|
||||||
|
|
||||||
|
Rivencraft owns each Age definition for the lifetime of a world. A definition
|
||||||
|
contains the Age UUID, canonical name, seed, generation version, runtime
|
||||||
|
dimension identifier, spawn, and deterministic traits. Definitions are stored
|
||||||
|
in the Overworld saved data and are never rerolled during ordinary loading.
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
An Age definition is created before its runtime dimension. If dimension
|
||||||
|
creation is interrupted, the saved definition remains authoritative and the
|
||||||
|
same Age is reconstructed the next time its Descriptive Book is used.
|
||||||
|
|
||||||
|
Registered Ages load when the server starts. A book also loads its destination
|
||||||
|
on demand when an administrator previously unloaded it. Unloading retains the
|
||||||
|
definition and world folder, so existing books continue to reference the same
|
||||||
|
Age and later reload the same terrain.
|
||||||
|
|
||||||
|
Rivencraft does not automatically delete Ages. Safe permanent deletion needs a
|
||||||
|
complete reference index for books in inventories, containers, Book Stands,
|
||||||
|
and unloaded chunks. Until that exists, retaining the definition prevents an
|
||||||
|
administrator action from silently orphaning books.
|
||||||
|
|
||||||
|
Destination-preview chunk loaders are temporary. Rivencraft releases them when
|
||||||
|
the book closes, the player travels, the player disconnects, the destination
|
||||||
|
Age is unloaded, or the server stops.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
The commands require permission level 2.
|
||||||
|
|
||||||
|
- `/rivencraft age list` lists every registered Age and whether its runtime
|
||||||
|
dimension is currently loaded.
|
||||||
|
- `/rivencraft age unload <dimension>` safely unloads one Age. Players in that
|
||||||
|
Age are handled by Dynamic Dimensions, and its definition and world data are
|
||||||
|
retained. Use the command suggestions to select a registered dimension.
|
||||||
|
|
||||||
|
Opening a Descriptive Book for an unloaded Age loads it again from its saved
|
||||||
|
definition.
|
||||||
@@ -46,6 +46,8 @@ acceptance issue and all required implementation issues are complete.
|
|||||||
Age references use UUIDs rather than dimension keys. The server-owned Age
|
Age references use UUIDs rather than dimension keys. The server-owned Age
|
||||||
registry maps each stable identity to its immutable definition and runtime
|
registry maps each stable identity to its immutable definition and runtime
|
||||||
dimension, keeping saved books valid if an Age is migrated or regenerated.
|
dimension, keeping saved books valid if an Age is migrated or regenerated.
|
||||||
|
Operational lifecycle and unload behavior are documented in
|
||||||
|
[AGES.md](AGES.md).
|
||||||
|
|
||||||
The core mod must remain usable without Create, Ponder, or Immersive Portals.
|
The core mod must remain usable without Create, Ponder, or Immersive Portals.
|
||||||
The built-in legacy animation is the dependency-free destination preview.
|
The built-in legacy animation is the dependency-free destination preview.
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ mod_name=Rivencraft
|
|||||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||||
mod_license=LGPL-3.0-or-later
|
mod_license=LGPL-3.0-or-later
|
||||||
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
||||||
mod_version=0.2.26.1
|
mod_version=0.2.27.0
|
||||||
immersive_portals_version=6.0.7
|
immersive_portals_version=6.0.7
|
||||||
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
||||||
# This should match the base package used for the mod sources.
|
# This should match the base package used for the mod sources.
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
|||||||
import net.dnorberg.rivencraft.registry.ModBiomeSources;
|
import net.dnorberg.rivencraft.registry.ModBiomeSources;
|
||||||
import net.dnorberg.rivencraft.event.BookStandConversionHandler;
|
import net.dnorberg.rivencraft.event.BookStandConversionHandler;
|
||||||
import net.dnorberg.rivencraft.event.AgeCelestialAngleSync;
|
import net.dnorberg.rivencraft.event.AgeCelestialAngleSync;
|
||||||
|
import net.dnorberg.rivencraft.event.AgeLifecycleEvents;
|
||||||
|
import net.dnorberg.rivencraft.command.RivencraftCommands;
|
||||||
import net.dnorberg.rivencraft.network.ModNetworking;
|
import net.dnorberg.rivencraft.network.ModNetworking;
|
||||||
import net.dnorberg.rivencraft.config.RivencraftConfig;
|
import net.dnorberg.rivencraft.config.RivencraftConfig;
|
||||||
import net.dnorberg.rivencraft.config.RivencraftClientConfig;
|
import net.dnorberg.rivencraft.config.RivencraftClientConfig;
|
||||||
@@ -43,6 +45,9 @@ public final class Rivencraft {
|
|||||||
NeoForge.EVENT_BUS.addListener(AgeWeatherService::onLevelTickPost);
|
NeoForge.EVENT_BUS.addListener(AgeWeatherService::onLevelTickPost);
|
||||||
NeoForge.EVENT_BUS.addListener(AgeCelestialAngleSync::onPlayerLoggedIn);
|
NeoForge.EVENT_BUS.addListener(AgeCelestialAngleSync::onPlayerLoggedIn);
|
||||||
NeoForge.EVENT_BUS.addListener(AgeCelestialAngleSync::onPlayerChangedDimension);
|
NeoForge.EVENT_BUS.addListener(AgeCelestialAngleSync::onPlayerChangedDimension);
|
||||||
|
NeoForge.EVENT_BUS.addListener(AgeLifecycleEvents::onPlayerLoggedOut);
|
||||||
|
NeoForge.EVENT_BUS.addListener(AgeLifecycleEvents::onServerStopping);
|
||||||
|
NeoForge.EVENT_BUS.addListener(RivencraftCommands::register);
|
||||||
LOGGER.info("Initializing Rivencraft");
|
LOGGER.info("Initializing Rivencraft");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package net.dnorberg.rivencraft.book;
|
package net.dnorberg.rivencraft.book;
|
||||||
|
|
||||||
import dev.galacticraft.dynamicdimensions.api.DynamicDimensionRegistry;
|
import dev.galacticraft.dynamicdimensions.api.DynamicDimensionRegistry;
|
||||||
|
import dev.galacticraft.dynamicdimensions.api.PlayerRemover;
|
||||||
|
import dev.galacticraft.dynamicdimensions.api.event.DimensionRemovedCallback;
|
||||||
import dev.galacticraft.dynamicdimensions.api.event.DynamicDimensionLoadCallback;
|
import dev.galacticraft.dynamicdimensions.api.event.DynamicDimensionLoadCallback;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.OptionalLong;
|
import java.util.OptionalLong;
|
||||||
@@ -56,6 +58,7 @@ public final class AgeDimensionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void register() {
|
public static void register() {
|
||||||
|
DimensionRemovedCallback.register((dimension, level) -> synchronizeImmersivePortals(level.getServer()));
|
||||||
DynamicDimensionLoadCallback.register((server, loader) -> {
|
DynamicDimensionLoadCallback.register((server, loader) -> {
|
||||||
ServerLevel overworld = server.overworld();
|
ServerLevel overworld = server.overworld();
|
||||||
for (AgeDefinition age : AgeRegistrySavedData.get(overworld).definitions()) {
|
for (AgeDefinition age : AgeRegistrySavedData.get(overworld).definitions()) {
|
||||||
@@ -115,6 +118,27 @@ public final class AgeDimensionService {
|
|||||||
return loaded;
|
return loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unloads an Age while retaining its saved definition and dimension data.
|
||||||
|
* A later book access reconstructs the same runtime level from that data.
|
||||||
|
*/
|
||||||
|
public static UnloadResult unload(MinecraftServer server, AgeDefinition definition) {
|
||||||
|
ResourceKey<Level> key = dimensionKey(definition);
|
||||||
|
if (server.getLevel(key) == null) {
|
||||||
|
return UnloadResult.ALREADY_UNLOADED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ModList.get().isLoaded("immersive_portals_core")) {
|
||||||
|
ImmersivePortalsChunkLoaders.closeForDimension(key);
|
||||||
|
}
|
||||||
|
boolean unloaded = DynamicDimensionRegistry.from(server)
|
||||||
|
.unloadDynamicDimension(definition.dimensionId(), PlayerRemover.DEFAULT);
|
||||||
|
if (!unloaded) {
|
||||||
|
return UnloadResult.FAILED;
|
||||||
|
}
|
||||||
|
return UnloadResult.UNLOADED;
|
||||||
|
}
|
||||||
|
|
||||||
private static void applyCelestialClock(ServerLevel level, AgeDefinition age) {
|
private static void applyCelestialClock(ServerLevel level, AgeDefinition age) {
|
||||||
if (age.traits().hasStoppedCelestialCycle()) {
|
if (age.traits().hasStoppedCelestialCycle()) {
|
||||||
// NeoForge rejects an exact zero rate. The smallest positive float
|
// NeoForge rejects an exact zero rate. The smallest positive float
|
||||||
@@ -396,4 +420,10 @@ public final class AgeDimensionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum UnloadResult {
|
||||||
|
UNLOADED,
|
||||||
|
ALREADY_UNLOADED,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -76,6 +76,7 @@ public final class LinkingService {
|
|||||||
LinkTarget target
|
LinkTarget target
|
||||||
) {
|
) {
|
||||||
if (ModList.get().isLoaded("immersive_portals_core")) {
|
if (ModList.get().isLoaded("immersive_portals_core")) {
|
||||||
|
ImmersivePortalsChunkLoaders.close(player);
|
||||||
ImmersivePortalsChunkLoaders.teleport(
|
ImmersivePortalsChunkLoaders.teleport(
|
||||||
player, destinationLevel, position, target.yaw(), target.pitch());
|
player, destinationLevel, position, target.yaw(), target.pitch());
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package net.dnorberg.rivencraft.command;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeDefinition;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeDimensionService;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
||||||
|
import net.minecraft.commands.Commands;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||||
|
|
||||||
|
/** Administrative commands for inspecting and safely unloading persistent Ages. */
|
||||||
|
public final class RivencraftCommands {
|
||||||
|
private RivencraftCommands() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void register(RegisterCommandsEvent event) {
|
||||||
|
event.getDispatcher().register(Commands.literal("rivencraft")
|
||||||
|
.requires(source -> source.hasPermission(2))
|
||||||
|
.then(Commands.literal("age")
|
||||||
|
.then(Commands.literal("list")
|
||||||
|
.executes(context -> listAges(context.getSource())))
|
||||||
|
.then(Commands.literal("unload")
|
||||||
|
.then(Commands.argument("dimension", StringArgumentType.string())
|
||||||
|
.suggests((context, builder) -> {
|
||||||
|
AgeRegistrySavedData.get(context.getSource().getLevel()).definitions()
|
||||||
|
.forEach(age -> builder.suggest(age.dimensionId().toString()));
|
||||||
|
return builder.buildFuture();
|
||||||
|
})
|
||||||
|
.executes(context -> unloadAge(
|
||||||
|
context.getSource(),
|
||||||
|
StringArgumentType.getString(context, "dimension")
|
||||||
|
))))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int listAges(net.minecraft.commands.CommandSourceStack source) {
|
||||||
|
var ages = AgeRegistrySavedData.get(source.getLevel()).definitions().stream()
|
||||||
|
.sorted(java.util.Comparator.comparing(AgeDefinition::name))
|
||||||
|
.toList();
|
||||||
|
source.sendSuccess(() -> Component.literal("Rivencraft Ages: " + ages.size()), false);
|
||||||
|
for (AgeDefinition age : ages) {
|
||||||
|
boolean loaded = source.getServer().getLevel(AgeDimensionService.dimensionKey(age)) != null;
|
||||||
|
source.sendSuccess(() -> Component.literal(
|
||||||
|
(loaded ? "[loaded] " : "[unloaded] ")
|
||||||
|
+ age.name() + " — " + age.dimensionId()
|
||||||
|
), false);
|
||||||
|
}
|
||||||
|
return ages.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int unloadAge(net.minecraft.commands.CommandSourceStack source, String requestedDimension) {
|
||||||
|
ResourceLocation dimension = ResourceLocation.tryParse(requestedDimension);
|
||||||
|
if (dimension == null) {
|
||||||
|
source.sendFailure(Component.literal("Invalid dimension id: " + requestedDimension));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
AgeDefinition age = AgeRegistrySavedData.get(source.getLevel()).findByDimension(dimension).orElse(null);
|
||||||
|
if (age == null) {
|
||||||
|
source.sendFailure(Component.literal("Unknown Rivencraft Age: " + dimension));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AgeDimensionService.UnloadResult result = AgeDimensionService.unload(source.getServer(), age);
|
||||||
|
return switch (result) {
|
||||||
|
case UNLOADED -> {
|
||||||
|
source.sendSuccess(() -> Component.literal(
|
||||||
|
"Unloaded " + age.name() + ". Its definition and world data were retained."
|
||||||
|
), true);
|
||||||
|
yield 1;
|
||||||
|
}
|
||||||
|
case ALREADY_UNLOADED -> {
|
||||||
|
source.sendFailure(Component.literal(age.name() + " is already unloaded."));
|
||||||
|
yield 0;
|
||||||
|
}
|
||||||
|
case FAILED -> {
|
||||||
|
source.sendFailure(Component.literal("Dynamic Dimensions refused to unload " + age.name() + "."));
|
||||||
|
yield 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
package net.dnorberg.rivencraft.compat;
|
package net.dnorberg.rivencraft.compat;
|
||||||
|
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.WeakHashMap;
|
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
import net.minecraft.world.phys.Vec3;
|
import net.minecraft.world.phys.Vec3;
|
||||||
import qouteall.imm_ptl.core.api.PortalAPI;
|
import qouteall.imm_ptl.core.api.PortalAPI;
|
||||||
import qouteall.imm_ptl.core.chunk_loading.ChunkLoader;
|
import qouteall.imm_ptl.core.chunk_loading.ChunkLoader;
|
||||||
@@ -14,12 +16,12 @@ import qouteall.q_misc_util.dimension.DimensionIntId;
|
|||||||
|
|
||||||
/** Isolated server bridge; this class is loaded only when Immersive Portals is installed. */
|
/** Isolated server bridge; this class is loaded only when Immersive Portals is installed. */
|
||||||
public final class ImmersivePortalsChunkLoaders {
|
public final class ImmersivePortalsChunkLoaders {
|
||||||
private static final Map<ServerPlayer, ChunkLoader> LOADERS = new WeakHashMap<>();
|
private static final Map<ServerPlayer, ChunkLoader> LOADERS = new IdentityHashMap<>();
|
||||||
|
|
||||||
private ImmersivePortalsChunkLoaders() {
|
private ImmersivePortalsChunkLoaders() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void open(ServerPlayer player, ServerLevel level, BlockPos center) {
|
public static synchronized void open(ServerPlayer player, ServerLevel level, BlockPos center) {
|
||||||
close(player);
|
close(player);
|
||||||
int centerChunkX = center.getX() >> 4;
|
int centerChunkX = center.getX() >> 4;
|
||||||
int centerChunkZ = center.getZ() >> 4;
|
int centerChunkZ = center.getZ() >> 4;
|
||||||
@@ -37,13 +39,28 @@ public final class ImmersivePortalsChunkLoaders {
|
|||||||
ImmPtlChunkTracking.immediatelyUpdateForPlayer(player);
|
ImmPtlChunkTracking.immediatelyUpdateForPlayer(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void close(ServerPlayer player) {
|
public static synchronized void close(ServerPlayer player) {
|
||||||
ChunkLoader loader = LOADERS.remove(player);
|
ChunkLoader loader = LOADERS.remove(player);
|
||||||
if (loader != null) {
|
if (loader != null) {
|
||||||
PortalAPI.removeChunkLoaderForPlayer(player, loader);
|
PortalAPI.removeChunkLoaderForPlayer(player, loader);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static synchronized void closeForDimension(ResourceKey<Level> dimension) {
|
||||||
|
LOADERS.entrySet().removeIf(entry -> {
|
||||||
|
if (!entry.getValue().dimension().equals(dimension)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
PortalAPI.removeChunkLoaderForPlayer(entry.getKey(), entry.getValue());
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void closeAll() {
|
||||||
|
LOADERS.forEach(PortalAPI::removeChunkLoaderForPlayer);
|
||||||
|
LOADERS.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/** Registers runtime dimensions and sends the revised ID map to connected clients. */
|
/** Registers runtime dimensions and sends the revised ID map to connected clients. */
|
||||||
public static void dimensionsChanged(MinecraftServer server) {
|
public static void dimensionsChanged(MinecraftServer server) {
|
||||||
DimensionIntId.onServerDimensionChanged(server);
|
DimensionIntId.onServerDimensionChanged(server);
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package net.dnorberg.rivencraft.event;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.neoforged.fml.ModList;
|
||||||
|
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
|
||||||
|
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
|
||||||
|
|
||||||
|
/** Server-authoritative cleanup for resources that exist only while a preview is active. */
|
||||||
|
public final class AgeLifecycleEvents {
|
||||||
|
private AgeLifecycleEvents() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onPlayerLoggedOut(PlayerEvent.PlayerLoggedOutEvent event) {
|
||||||
|
if (event.getEntity() instanceof ServerPlayer player
|
||||||
|
&& ModList.get().isLoaded("immersive_portals_core")) {
|
||||||
|
ImmersivePortalsChunkLoaders.close(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onServerStopping(ServerStoppingEvent event) {
|
||||||
|
if (ModList.get().isLoaded("immersive_portals_core")) {
|
||||||
|
ImmersivePortalsChunkLoaders.closeAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -152,6 +152,42 @@ public final class LinkingGameTests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty", timeoutTicks = 100)
|
||||||
|
public static void unloadedAgeReloadsWithTheSameDefinition(GameTestHelper helper) {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
AgeDefinition definition = AgeRegistrySavedData.resolveOrCreate(
|
||||||
|
helper.getLevel(),
|
||||||
|
new AgeReference(id, "Lifecycle Test Age " + id)
|
||||||
|
);
|
||||||
|
|
||||||
|
var server = helper.getLevel().getServer();
|
||||||
|
var first = AgeDimensionService.ensureLoaded(server, definition);
|
||||||
|
helper.assertTrue(first != null, "The lifecycle test Age must load");
|
||||||
|
helper.runAfterDelay(1, () -> {
|
||||||
|
helper.assertValueEqual(
|
||||||
|
AgeDimensionService.unload(server, definition),
|
||||||
|
AgeDimensionService.UnloadResult.UNLOADED,
|
||||||
|
"A loaded Age must unload without deleting its definition"
|
||||||
|
);
|
||||||
|
helper.runAfterDelay(1, () -> {
|
||||||
|
helper.assertTrue(server.getLevel(AgeDimensionService.dimensionKey(definition)) == null,
|
||||||
|
"An unloaded Age must leave the active server level map");
|
||||||
|
|
||||||
|
var reloaded = AgeDimensionService.ensureLoaded(server, definition);
|
||||||
|
helper.assertTrue(reloaded != null, "A book must be able to reload an archived Age");
|
||||||
|
helper.runAfterDelay(1, () -> {
|
||||||
|
var registered = server.getLevel(AgeDimensionService.dimensionKey(definition));
|
||||||
|
helper.assertTrue(registered != null, "The reloaded Age must rejoin the active server level map");
|
||||||
|
helper.assertValueEqual(registered.getSeed(), definition.seed(),
|
||||||
|
"Reloading an Age must preserve its immutable seed");
|
||||||
|
helper.assertValueEqual(registered.dimension().location(), definition.dimensionId(),
|
||||||
|
"Reloading an Age must preserve its dimension identifier");
|
||||||
|
helper.succeed();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty", timeoutTicks = 200)
|
@GameTest(template = "empty", timeoutTicks = 200)
|
||||||
public static void legacyBaseTerrainProfilesLoadRuntimeDimensions(GameTestHelper helper) {
|
public static void legacyBaseTerrainProfilesLoadRuntimeDimensions(GameTestHelper helper) {
|
||||||
for (var profile : AgeGenerationProfiles.all()) {
|
for (var profile : AgeGenerationProfiles.all()) {
|
||||||
|
|||||||
Reference in new issue
Block a user