Wire Volume 1 server lifecycle

This commit is contained in:
George
2026-06-07 14:11:10 +01:00
parent 96747a37db
commit ae2a8db3ce
5 changed files with 327 additions and 18 deletions
@@ -7,11 +7,13 @@ import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.server.ServerStartedEvent; import net.neoforged.neoforge.event.server.ServerStartedEvent;
import net.neoforged.neoforge.event.server.ServerStartingEvent; import net.neoforged.neoforge.event.server.ServerStartingEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent; import net.neoforged.neoforge.event.server.ServerStoppingEvent;
import net.minecraft.world.level.storage.LevelResource;
import com.livingworld.bootstrap.LivingWorldBootstrap; import com.livingworld.bootstrap.LivingWorldBootstrap;
import com.livingworld.core.LivingWorldConstants; import com.livingworld.core.LivingWorldConstants;
import com.livingworld.debug.DiagnosticCategory; import com.livingworld.debug.DiagnosticCategory;
import com.livingworld.debug.LivingWorldLogger; import com.livingworld.debug.LivingWorldLogger;
import com.livingworld.platform.neoforge.NeoForgePlatformAdapter;
/** /**
* Mod entrypoint for Living World. * Mod entrypoint for Living World.
@@ -29,11 +31,17 @@ public class LivingWorldMod {
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting..."); LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting...");
this.bootstrap = new LivingWorldBootstrap(); this.bootstrap = new LivingWorldBootstrap();
this.bootstrap.initialize(); NeoForgePlatformAdapter platformAdapter = new NeoForgePlatformAdapter(
bootstrap::getWorldSaveDirectory,
bootstrap::registerCommands,
bootstrap::onServerTick);
this.bootstrap.initialize(platformAdapter);
eventBus.addListener(FMLCommonSetupEvent.class, event -> bootstrap.onCommonSetup()); eventBus.addListener(FMLCommonSetupEvent.class, event -> bootstrap.onCommonSetup());
NeoForge.EVENT_BUS.addListener( NeoForge.EVENT_BUS.addListener(
ServerStartingEvent.class, event -> bootstrap.onServerStarting()); ServerStartingEvent.class,
event -> bootstrap.onServerStarting(
event.getServer().getWorldPath(LevelResource.ROOT)));
NeoForge.EVENT_BUS.addListener( NeoForge.EVENT_BUS.addListener(
ServerStartedEvent.class, event -> bootstrap.onServerStarted()); ServerStartedEvent.class, event -> bootstrap.onServerStarted());
NeoForge.EVENT_BUS.addListener( NeoForge.EVENT_BUS.addListener(
@@ -1,48 +1,224 @@
package com.livingworld.bootstrap; package com.livingworld.bootstrap;
import com.livingworld.commands.LivingWorldCommandRoot;
import com.livingworld.config.DefaultConfigService;
import com.livingworld.config.SimulationConfig;
import com.livingworld.core.LivingWorldConstants;
import com.livingworld.core.services.CoreServices;
import com.livingworld.core.services.FileRegionPersistenceService;
import com.livingworld.core.services.ServiceRegistry;
import com.livingworld.core.simulation.DefaultTimeService;
import com.livingworld.core.simulation.SimulationManager;
import com.livingworld.core.simulation.SimulationScheduler;
import com.livingworld.debug.SimulationProfiler;
import com.livingworld.debug.DiagnosticCategory; import com.livingworld.debug.DiagnosticCategory;
import com.livingworld.debug.LivingWorldLogger; import com.livingworld.debug.LivingWorldLogger;
import com.livingworld.events.LivingWorldEventBus;
import com.livingworld.modules.ModuleContext;
import com.livingworld.modules.ModuleRegistry;
import com.livingworld.modules.ServerContext;
import com.livingworld.platform.PlatformAdapter;
import com.livingworld.regions.RegionFactory;
import com.livingworld.regions.RegionLifecycleController;
import com.livingworld.regions.RegionManager;
import com.livingworld.regions.RegionStorage;
import com.livingworld.regions.cache.RegionCache;
import com.livingworld.regions.query.RegionQueryEngine;
import com.mojang.brigadier.CommandDispatcher;
import java.nio.file.Path;
import net.minecraft.commands.CommandSourceStack;
/** /**
* Bootstrap class for Living World mod startup lifecycle. * Bootstrap class for Living World mod startup lifecycle.
*
* Each method logs its lifecycle stage. No gameplay logic, region/ecosystem,
* or persistence systems are implemented yet.
*/ */
public class LivingWorldBootstrap { public final class LivingWorldBootstrap {
private PlatformAdapter platformAdapter;
private Path worldSaveDirectory;
private ServiceRegistry services;
private RegionManager regionManager;
private ModuleRegistry moduleRegistry;
private SimulationManager simulationManager;
private boolean initialized;
private boolean serverReady;
/** /**
* Called once during mod construction. * Called once during mod construction.
*/ */
public void initialize() { public void initialize(PlatformAdapter platformAdapter) {
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "initialize — mod bootstrap initialized."); if (initialized) {
throw new IllegalStateException("bootstrap is already initialized");
}
if (platformAdapter == null) {
throw new IllegalArgumentException("platformAdapter must not be null");
}
this.platformAdapter = platformAdapter;
this.platformAdapter.registerCommands();
this.platformAdapter.registerServerTickHook();
this.platformAdapter.registerPlayerEventHooks();
this.initialized = true;
LivingWorldLogger.info(
DiagnosticCategory.BOOTSTRAP,
"Platform=" + platformAdapter.getPlatformName()
+ ", minecraft=" + platformAdapter.getMinecraftVersion()
+ ", loader=" + platformAdapter.getLoaderVersion()
+ ", dedicatedServer=" + platformAdapter.isDedicatedServer());
LivingWorldLogger.info(
DiagnosticCategory.BOOTSTRAP,
"initialize - mod bootstrap initialized.");
} }
/** /**
* Called during common (dedicated & integrated server) setup phase. * Called during common (dedicated & integrated server) setup phase.
*/ */
public void onCommonSetup() { public void onCommonSetup() {
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "onCommonSetup — common setup phase reached."); requireInitialized();
LivingWorldLogger.info(
DiagnosticCategory.BOOTSTRAP,
"onCommonSetup - common setup phase reached.");
} }
/** /**
* Called when the server is starting. * Called when the server is starting.
*/ */
public void onServerStarting() { public void onServerStarting(Path worldSaveDirectory) {
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "onServerStarting — server starting."); requireInitialized();
if (worldSaveDirectory == null) {
throw new IllegalArgumentException("worldSaveDirectory must not be null");
}
if (serverReady) {
throw new IllegalStateException("server services are already initialized");
}
this.worldSaveDirectory = worldSaveDirectory;
createServerServices(worldSaveDirectory);
this.serverReady = true;
LivingWorldLogger.info(
DiagnosticCategory.BOOTSTRAP,
"onServerStarting - services ready at " + worldSaveDirectory);
} }
/** /**
* Called when the server has finished starting. * Called when the server has finished starting.
*/ */
public void onServerStarted() { public void onServerStarted() {
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "onServerStarted — server started."); requireServerReady();
ServerContext context = new ServerContext();
moduleRegistry.getEnabledModules().forEach(module -> module.onServerStarted(context));
LivingWorldLogger.info(
DiagnosticCategory.BOOTSTRAP,
"onServerStarted - server started.");
} }
/** /**
* Called when the server is stopping. * Called when the server is stopping.
*/ */
public void onServerStopping() { public void onServerStopping() {
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "onServerStopping — server stopping."); if (!serverReady) {
return;
}
regionManager.flushAll();
moduleRegistry.shutdownAll();
serverReady = false;
LivingWorldLogger.info(
DiagnosticCategory.BOOTSTRAP,
"onServerStopping - persistence flushed and modules stopped.");
}
public void onServerTick() {
if (!serverReady) {
return;
}
long previousSimulationTick = simulationManager.getSimulationTickCounter();
simulationManager.onMinecraftServerTick();
if (simulationManager.getSimulationTickCounter() != previousSimulationTick) {
regionManager.saveDirtyRegions();
}
}
public void registerCommands(CommandDispatcher<CommandSourceStack> dispatcher) {
requireInitialized();
LivingWorldCommandRoot.registerDeferred(
dispatcher,
() -> requireService(regionManager, "regionManager"),
() -> requireService(moduleRegistry, "moduleRegistry"),
() -> requireService(simulationManager, "simulationManager"));
}
public Path getWorldSaveDirectory() {
return worldSaveDirectory;
}
public boolean isServerReady() {
return serverReady;
}
public ServiceRegistry getServices() {
requireServerReady();
return services;
}
private void createServerServices(Path saveDirectory) {
DefaultConfigService configService = new DefaultConfigService();
SimulationConfig config = configService.getSimulationConfig();
FileRegionPersistenceService persistenceService =
new FileRegionPersistenceService(
saveDirectory.resolve("living_world"),
LivingWorldConstants.MOD_VERSION);
RegionCache regionCache = new RegionCache();
RegionQueryEngine queryEngine = new RegionQueryEngine(regionCache);
regionManager = new RegionManager(
new RegionFactory(),
new RegionStorage(persistenceService),
regionCache,
queryEngine,
new RegionLifecycleController(),
config);
moduleRegistry = new ModuleRegistry();
LivingWorldEventBus eventBus = new LivingWorldEventBus();
DefaultTimeService timeService = new DefaultTimeService();
SimulationScheduler scheduler = new SimulationScheduler(config);
SimulationProfiler profiler = new SimulationProfiler();
simulationManager = new SimulationManager(
scheduler,
regionManager,
moduleRegistry,
eventBus,
timeService,
persistenceService,
profiler);
services = new ServiceRegistry();
services.register(CoreServices.CONFIG, configService);
services.register(CoreServices.REGIONS, regionManager);
services.register(CoreServices.SIMULATION, simulationManager);
services.register(CoreServices.PERSISTENCE, persistenceService);
services.register(CoreServices.EVENTS, eventBus);
services.register(CoreServices.MODULES, moduleRegistry);
services.register(CoreServices.TIME, timeService);
services.register(CoreServices.DEBUG, profiler);
services.lock();
moduleRegistry.initializeAll(new ModuleContext(services));
}
private void requireInitialized() {
if (!initialized) {
throw new IllegalStateException("bootstrap has not been initialized");
}
}
private void requireServerReady() {
requireInitialized();
if (!serverReady) {
throw new IllegalStateException("server services are not ready");
}
}
private static <T> T requireService(T service, String name) {
if (service == null) {
throw new IllegalStateException(name + " is not ready");
}
return service;
} }
} }
@@ -1,6 +1,7 @@
package com.livingworld.commands; package com.livingworld.commands;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.function.Supplier;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType;
@@ -28,6 +29,18 @@ public final class LivingWorldCommandRoot {
RegionManager regionManager, RegionManager regionManager,
ModuleRegistry moduleRegistry, ModuleRegistry moduleRegistry,
SimulationManager simulationManager) { SimulationManager simulationManager) {
registerDeferred(
dispatcher,
() -> regionManager,
() -> moduleRegistry,
() -> simulationManager);
}
public static void registerDeferred(
CommandDispatcher<CommandSourceStack> dispatcher,
Supplier<RegionManager> regionManager,
Supplier<ModuleRegistry> moduleRegistry,
Supplier<SimulationManager> simulationManager) {
if (dispatcher == null) { if (dispatcher == null) {
throw new IllegalArgumentException("dispatcher must not be null"); throw new IllegalArgumentException("dispatcher must not be null");
} }
@@ -45,15 +58,20 @@ public final class LivingWorldCommandRoot {
.requires(source -> source.hasPermission(OPERATOR_PERMISSION_LEVEL)) .requires(source -> source.hasPermission(OPERATOR_PERMISSION_LEVEL))
.then(Commands.literal("status") .then(Commands.literal("status")
.executes(context -> showStatus( .executes(context -> showStatus(
context.getSource(), regionManager, moduleRegistry, simulationManager))) context.getSource(),
requireService(regionManager, "regionManager"),
requireService(moduleRegistry, "moduleRegistry"),
requireService(simulationManager, "simulationManager"))))
.then(Commands.literal("region") .then(Commands.literal("region")
.then(Commands.literal("info") .then(Commands.literal("info")
.executes(context -> RegionInfoCommand.execute( .executes(context -> RegionInfoCommand.execute(
context.getSource(), regionManager)))) context.getSource(),
requireService(regionManager, "regionManager")))))
.then(Commands.literal("modules") .then(Commands.literal("modules")
.then(Commands.literal("list") .then(Commands.literal("list")
.executes(context -> listModules( .executes(context -> listModules(
context.getSource(), moduleRegistry)))) context.getSource(),
requireService(moduleRegistry, "moduleRegistry")))))
.then(Commands.literal("simulate") .then(Commands.literal("simulate")
.then(Commands.argument( .then(Commands.argument(
"ticks", "ticks",
@@ -61,10 +79,21 @@ public final class LivingWorldCommandRoot {
1, SimulationManager.MAX_FORCED_SIMULATION_TICKS)) 1, SimulationManager.MAX_FORCED_SIMULATION_TICKS))
.executes(context -> SimulateCommand.execute( .executes(context -> SimulateCommand.execute(
context.getSource(), context.getSource(),
simulationManager, requireService(simulationManager, "simulationManager"),
IntegerArgumentType.getInteger(context, "ticks")))))); IntegerArgumentType.getInteger(context, "ticks"))))));
} }
private static <T> T requireService(Supplier<T> supplier, String name) {
if (supplier == null) {
throw new IllegalArgumentException(name + " supplier must not be null");
}
T service = supplier.get();
if (service == null) {
throw new IllegalStateException(name + " is not ready");
}
return service;
}
private static int showStatus( private static int showStatus(
CommandSourceStack source, CommandSourceStack source,
RegionManager regionManager, RegionManager regionManager,
@@ -15,6 +15,9 @@ public final class LivingWorldConstants {
/** Human-readable mod name displayed in the mod list. */ /** Human-readable mod name displayed in the mod list. */
public static final String MOD_NAME = "Living World"; public static final String MOD_NAME = "Living World";
/** Current mod version. Keep aligned with the Gradle project version. */
public static final String MOD_VERSION = "0.1.0";
/** Current schema version for data persistence and migration tracking. */ /** Current schema version for data persistence and migration tracking. */
public static final int CURRENT_CORE_SCHEMA_VERSION = 1; public static final int CURRENT_CORE_SCHEMA_VERSION = 1;
@@ -0,0 +1,93 @@
package com.livingworld.bootstrap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.livingworld.core.services.CoreServices;
import com.livingworld.core.services.TimeService;
import com.livingworld.platform.PlatformAdapter;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class LivingWorldBootstrapTest {
@TempDir
Path temporaryDirectory;
@Test
void createsAndLocksCompleteServerServiceGraph() {
LivingWorldBootstrap bootstrap = new LivingWorldBootstrap();
TestPlatformAdapter platform = new TestPlatformAdapter(temporaryDirectory);
bootstrap.initialize(platform);
bootstrap.onCommonSetup();
bootstrap.onServerStarting(temporaryDirectory);
bootstrap.onServerStarted();
assertTrue(bootstrap.isServerReady());
assertEquals(1, platform.commandRegistrations);
assertEquals(1, platform.tickRegistrations);
assertTrue(bootstrap.getServices().isLocked());
assertTrue(bootstrap.getServices().isRegistered(CoreServices.CONFIG));
assertTrue(bootstrap.getServices().isRegistered(CoreServices.REGIONS));
assertTrue(bootstrap.getServices().isRegistered(CoreServices.SIMULATION));
assertTrue(bootstrap.getServices().isRegistered(CoreServices.PERSISTENCE));
assertTrue(bootstrap.getServices().isRegistered(CoreServices.EVENTS));
assertTrue(bootstrap.getServices().isRegistered(CoreServices.MODULES));
assertTrue(bootstrap.getServices().isRegistered(CoreServices.TIME));
assertTrue(bootstrap.getServices().isRegistered(CoreServices.DEBUG));
for (int tick = 0; tick < 100; tick++) {
bootstrap.onServerTick();
}
TimeService timeService = bootstrap.getServices().get(CoreServices.TIME);
assertEquals(1, timeService.getSimulationTick());
bootstrap.onServerStopping();
assertFalse(bootstrap.isServerReady());
assertTrue(Files.exists(temporaryDirectory.resolve("living_world/metadata.properties")));
}
@Test
void ignoresTicksUntilServerServicesAreReady() {
LivingWorldBootstrap bootstrap = new LivingWorldBootstrap();
bootstrap.initialize(new TestPlatformAdapter(temporaryDirectory));
bootstrap.onServerTick();
assertFalse(bootstrap.isServerReady());
}
@Test
void rejectsServerLifecycleBeforeInitialization() {
LivingWorldBootstrap bootstrap = new LivingWorldBootstrap();
assertThrows(
IllegalStateException.class,
() -> bootstrap.onServerStarting(temporaryDirectory));
}
private static final class TestPlatformAdapter implements PlatformAdapter {
private final Path saveDirectory;
private int commandRegistrations;
private int tickRegistrations;
private TestPlatformAdapter(Path saveDirectory) {
this.saveDirectory = saveDirectory;
}
@Override public String getPlatformName() { return "test"; }
@Override public String getMinecraftVersion() { return "test"; }
@Override public String getLoaderVersion() { return "test"; }
@Override public boolean isDedicatedServer() { return true; }
@Override public Path getWorldSaveDirectory() { return saveDirectory; }
@Override public void registerCommands() { commandRegistrations++; }
@Override public void registerServerTickHook() { tickRegistrations++; }
@Override public void registerPlayerEventHooks() {}
}
}