Wire Volume 1 server lifecycle
This commit is contained in:
@@ -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() {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user