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.ServerStartingEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
import net.minecraft.world.level.storage.LevelResource;
import com.livingworld.bootstrap.LivingWorldBootstrap;
import com.livingworld.core.LivingWorldConstants;
import com.livingworld.debug.DiagnosticCategory;
import com.livingworld.debug.LivingWorldLogger;
import com.livingworld.platform.neoforge.NeoForgePlatformAdapter;
/**
* Mod entrypoint for Living World.
@@ -29,11 +31,17 @@ public class LivingWorldMod {
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting...");
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());
NeoForge.EVENT_BUS.addListener(
ServerStartingEvent.class, event -> bootstrap.onServerStarting());
ServerStartingEvent.class,
event -> bootstrap.onServerStarting(
event.getServer().getWorldPath(LevelResource.ROOT)));
NeoForge.EVENT_BUS.addListener(
ServerStartedEvent.class, event -> bootstrap.onServerStarted());
NeoForge.EVENT_BUS.addListener(
@@ -1,48 +1,224 @@
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.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.
*
* 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.
*/
public void initialize() {
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "initialize — mod bootstrap initialized.");
public void initialize(PlatformAdapter platformAdapter) {
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.
*/
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.
*/
public void onServerStarting() {
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "onServerStarting — server starting.");
public void onServerStarting(Path worldSaveDirectory) {
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.
*/
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.
*/
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;
import java.util.stream.Collectors;
import java.util.function.Supplier;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType;
@@ -28,6 +29,18 @@ public final class LivingWorldCommandRoot {
RegionManager regionManager,
ModuleRegistry moduleRegistry,
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) {
throw new IllegalArgumentException("dispatcher must not be null");
}
@@ -45,15 +58,20 @@ public final class LivingWorldCommandRoot {
.requires(source -> source.hasPermission(OPERATOR_PERMISSION_LEVEL))
.then(Commands.literal("status")
.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("info")
.executes(context -> RegionInfoCommand.execute(
context.getSource(), regionManager))))
context.getSource(),
requireService(regionManager, "regionManager")))))
.then(Commands.literal("modules")
.then(Commands.literal("list")
.executes(context -> listModules(
context.getSource(), moduleRegistry))))
context.getSource(),
requireService(moduleRegistry, "moduleRegistry")))))
.then(Commands.literal("simulate")
.then(Commands.argument(
"ticks",
@@ -61,10 +79,21 @@ public final class LivingWorldCommandRoot {
1, SimulationManager.MAX_FORCED_SIMULATION_TICKS))
.executes(context -> SimulateCommand.execute(
context.getSource(),
simulationManager,
requireService(simulationManager, "simulationManager"),
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(
CommandSourceStack source,
RegionManager regionManager,
@@ -15,6 +15,9 @@ public final class LivingWorldConstants {
/** Human-readable mod name displayed in the mod list. */
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. */
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() {}
}
}