Fix milestone 11 simulation flow
This commit is contained in:
@@ -40,6 +40,7 @@ dependencies {
|
|||||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
|
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
|
||||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2'
|
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2'
|
||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2'
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.10.2'
|
||||||
|
testRuntimeOnly 'org.slf4j:slf4j-api:2.0.9'
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package com.livingworld;
|
package com.livingworld;
|
||||||
|
|
||||||
import net.neoforged.fml.common.Mod;
|
import net.neoforged.fml.common.Mod;
|
||||||
|
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
|
||||||
import net.neoforged.bus.api.IEventBus;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
|
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 com.livingworld.bootstrap.LivingWorldBootstrap;
|
import com.livingworld.bootstrap.LivingWorldBootstrap;
|
||||||
import com.livingworld.core.LivingWorldConstants;
|
import com.livingworld.core.LivingWorldConstants;
|
||||||
@@ -18,12 +23,22 @@ import com.livingworld.debug.LivingWorldLogger;
|
|||||||
public class LivingWorldMod {
|
public class LivingWorldMod {
|
||||||
|
|
||||||
public static final String MOD_ID = LivingWorldConstants.MOD_ID;
|
public static final String MOD_ID = LivingWorldConstants.MOD_ID;
|
||||||
|
private final LivingWorldBootstrap bootstrap;
|
||||||
|
|
||||||
public LivingWorldMod(IEventBus eventBus) {
|
public LivingWorldMod(IEventBus eventBus) {
|
||||||
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting...");
|
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World mod starting...");
|
||||||
|
|
||||||
// Delegate startup work to the bootstrap system.
|
this.bootstrap = new LivingWorldBootstrap();
|
||||||
new LivingWorldBootstrap().initialize();
|
this.bootstrap.initialize();
|
||||||
|
|
||||||
|
eventBus.addListener(FMLCommonSetupEvent.class, event -> bootstrap.onCommonSetup());
|
||||||
|
NeoForge.EVENT_BUS.addListener(
|
||||||
|
ServerStartingEvent.class, event -> bootstrap.onServerStarting());
|
||||||
|
NeoForge.EVENT_BUS.addListener(
|
||||||
|
ServerStartedEvent.class, event -> bootstrap.onServerStarted());
|
||||||
|
NeoForge.EVENT_BUS.addListener(
|
||||||
|
ServerStoppingEvent.class, event -> bootstrap.onServerStopping());
|
||||||
|
|
||||||
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World Bootstrap initialized successfully.");
|
LivingWorldLogger.info(DiagnosticCategory.BOOTSTRAP, "Living World Bootstrap initialized successfully.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,5 +51,7 @@ public record RegionUpdateJob(
|
|||||||
public static final Comparator<RegionUpdateJob> BY_PRIORITY_DESC =
|
public static final Comparator<RegionUpdateJob> BY_PRIORITY_DESC =
|
||||||
Comparator.comparingInt(RegionUpdateJob::priority)
|
Comparator.comparingInt(RegionUpdateJob::priority)
|
||||||
.reversed()
|
.reversed()
|
||||||
.thenComparingLong(RegionUpdateJob::queuedAtSimulationTick);
|
.thenComparingLong(RegionUpdateJob::queuedAtSimulationTick)
|
||||||
|
.thenComparing(job -> job.coordinate().stableId())
|
||||||
|
.thenComparing(job -> job.reason().name());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ public final class SimulationManager {
|
|||||||
int nextJobIndex = 0;
|
int nextJobIndex = 0;
|
||||||
try {
|
try {
|
||||||
for (; nextJobIndex < jobs.size(); nextJobIndex++) {
|
for (; nextJobIndex < jobs.size(); nextJobIndex++) {
|
||||||
if (hasExceededTimeBudget(startTimeNanos)) {
|
if (nextJobIndex > 0 && hasExceededTimeBudget(startTimeNanos)) {
|
||||||
requeueJobs(jobs, nextJobIndex);
|
requeueJobs(jobs, nextJobIndex);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
package com.livingworld.core.simulation;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import com.livingworld.config.SimulationConfig;
|
||||||
|
import com.livingworld.core.services.TimeService;
|
||||||
|
import com.livingworld.data.serialization.PersistenceReader;
|
||||||
|
import com.livingworld.data.serialization.PersistenceWriter;
|
||||||
|
import com.livingworld.events.BaseLivingWorldEvent;
|
||||||
|
import com.livingworld.events.LivingWorldEvent;
|
||||||
|
import com.livingworld.events.LivingWorldEventBus;
|
||||||
|
import com.livingworld.modules.ModuleContext;
|
||||||
|
import com.livingworld.modules.ModuleMetadata;
|
||||||
|
import com.livingworld.modules.ModuleRegistry;
|
||||||
|
import com.livingworld.modules.ModuleUpdateResult;
|
||||||
|
import com.livingworld.modules.RegionUpdateContext;
|
||||||
|
import com.livingworld.modules.ServerContext;
|
||||||
|
import com.livingworld.modules.SimulationModule;
|
||||||
|
import com.livingworld.regions.Region;
|
||||||
|
import com.livingworld.regions.RegionCoordinate;
|
||||||
|
import com.livingworld.regions.RegionFactory;
|
||||||
|
|
||||||
|
class SimulationManagerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void runsRequestedModulesPublishesEventsAndMarksChangedRegionDirty() {
|
||||||
|
Fixture fixture = new Fixture(25);
|
||||||
|
TestModule skipped = new TestModule("skipped", false, 0);
|
||||||
|
TestModule ecology = new TestModule("ecology", true, 0);
|
||||||
|
fixture.modules.register(skipped);
|
||||||
|
fixture.modules.register(ecology);
|
||||||
|
|
||||||
|
List<String> receivedEvents = new ArrayList<>();
|
||||||
|
fixture.eventBus.register("region_changed", event -> receivedEvents.add(event.sourceModuleId()));
|
||||||
|
fixture.queue(Set.of("ecology"));
|
||||||
|
|
||||||
|
fixture.manager.runSimulationCycle();
|
||||||
|
|
||||||
|
assertEquals(0, skipped.updateCount);
|
||||||
|
assertEquals(1, ecology.updateCount);
|
||||||
|
assertEquals(List.of("ecology"), receivedEvents);
|
||||||
|
assertTrue(fixture.region.isDirty());
|
||||||
|
assertEquals(1, fixture.regionManager.markDirtyCount);
|
||||||
|
assertEquals(1, fixture.timeService.getSimulationTick());
|
||||||
|
assertEquals(1, fixture.scheduler.getSimulationTickCounter());
|
||||||
|
assertEquals(1, fixture.profiler.startCount);
|
||||||
|
assertEquals(1, fixture.profiler.endCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noChangeResultDoesNotMarkRegionDirty() {
|
||||||
|
Fixture fixture = new Fixture(25);
|
||||||
|
fixture.modules.register(new TestModule("ecology", false, 0));
|
||||||
|
fixture.queue(Set.of());
|
||||||
|
|
||||||
|
fixture.manager.runSimulationCycle();
|
||||||
|
|
||||||
|
assertFalse(fixture.region.isDirty());
|
||||||
|
assertEquals(0, fixture.regionManager.markDirtyCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void forcedTicksRunCyclesWithoutWaitingForMinecraftInterval() {
|
||||||
|
Fixture fixture = new Fixture(25);
|
||||||
|
TestModule module = new TestModule("ecology", false, 0);
|
||||||
|
fixture.modules.register(module);
|
||||||
|
fixture.queue(Set.of());
|
||||||
|
|
||||||
|
fixture.manager.runForcedSimulationTicks(3);
|
||||||
|
|
||||||
|
assertEquals(1, module.updateCount);
|
||||||
|
assertEquals(3, fixture.timeService.getSimulationTick());
|
||||||
|
assertEquals(3, fixture.scheduler.getSimulationTickCounter());
|
||||||
|
assertEquals(0, fixture.scheduler.getMinecraftTickCounter());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void timeBudgetRequeuesJobsNotYetProcessed() {
|
||||||
|
Fixture fixture = new Fixture(1);
|
||||||
|
fixture.modules.register(new TestModule("slow", false, 10));
|
||||||
|
fixture.queueAt(new RegionCoordinate("minecraft:overworld", 0, 0));
|
||||||
|
Region secondRegion = new RegionFactory().createNewRegion(
|
||||||
|
new RegionCoordinate("minecraft:overworld", 1, 0), 0);
|
||||||
|
secondRegion.clearDirty();
|
||||||
|
fixture.regionManager.regions.put(secondRegion.getCoordinate(), secondRegion);
|
||||||
|
fixture.queueAt(secondRegion.getCoordinate());
|
||||||
|
|
||||||
|
fixture.manager.runSimulationCycle();
|
||||||
|
|
||||||
|
assertEquals(1, fixture.scheduler.getQueuedJobCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Fixture {
|
||||||
|
private final SimulationScheduler scheduler;
|
||||||
|
private final TestRegionManager regionManager = new TestRegionManager();
|
||||||
|
private final ModuleRegistry modules = new ModuleRegistry();
|
||||||
|
private final LivingWorldEventBus eventBus = new LivingWorldEventBus();
|
||||||
|
private final TestTimeService timeService = new TestTimeService();
|
||||||
|
private final TestProfiler profiler = new TestProfiler();
|
||||||
|
private final Region region;
|
||||||
|
private final SimulationManager manager;
|
||||||
|
|
||||||
|
private Fixture(int maxMillisecondsPerCycle) {
|
||||||
|
SimulationConfig config = new SimulationConfig();
|
||||||
|
config.setMaxMillisecondsPerCycle(maxMillisecondsPerCycle);
|
||||||
|
config.setEmergencyStopMilliseconds(Math.max(40, maxMillisecondsPerCycle));
|
||||||
|
this.scheduler = new SimulationScheduler(config);
|
||||||
|
this.region = new RegionFactory().createNewRegion(
|
||||||
|
new RegionCoordinate("minecraft:overworld", 0, 0), 0);
|
||||||
|
this.region.clearDirty();
|
||||||
|
this.regionManager.regions.put(region.getCoordinate(), region);
|
||||||
|
this.manager = new SimulationManager(
|
||||||
|
scheduler,
|
||||||
|
regionManager,
|
||||||
|
modules,
|
||||||
|
eventBus,
|
||||||
|
timeService,
|
||||||
|
new TestPersistenceService(),
|
||||||
|
profiler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queue(Set<String> requestedModules) {
|
||||||
|
scheduler.queueRegion(new RegionUpdateJob(
|
||||||
|
region.getCoordinate(),
|
||||||
|
5,
|
||||||
|
timeService.getSimulationTick(),
|
||||||
|
requestedModules,
|
||||||
|
UpdateReason.NORMAL_ROLLING_UPDATE));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queueAt(RegionCoordinate coordinate) {
|
||||||
|
scheduler.queueRegion(new RegionUpdateJob(
|
||||||
|
coordinate,
|
||||||
|
5,
|
||||||
|
timeService.getSimulationTick(),
|
||||||
|
Set.of(),
|
||||||
|
UpdateReason.NORMAL_ROLLING_UPDATE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestModule implements SimulationModule {
|
||||||
|
private final String id;
|
||||||
|
private final boolean changesRegion;
|
||||||
|
private final long sleepMilliseconds;
|
||||||
|
private int updateCount;
|
||||||
|
|
||||||
|
private TestModule(String id, boolean changesRegion, long sleepMilliseconds) {
|
||||||
|
this.id = id;
|
||||||
|
this.changesRegion = changesRegion;
|
||||||
|
this.sleepMilliseconds = sleepMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getModuleId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModuleMetadata getMetadata() {
|
||||||
|
return new ModuleMetadata(
|
||||||
|
id, id, "1.0.0", "", "1",
|
||||||
|
List.of(), List.of(), true, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModuleUpdateResult updateRegion(RegionUpdateContext context) {
|
||||||
|
updateCount++;
|
||||||
|
if (sleepMilliseconds > 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(sleepMilliseconds);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<LivingWorldEvent> events = changesRegion
|
||||||
|
? List.of(new BaseLivingWorldEvent("region_changed", 0, id))
|
||||||
|
: List.of();
|
||||||
|
return new ModuleUpdateResult(changesRegion, events, List.of(), 0, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void initialize(ModuleContext context) {}
|
||||||
|
@Override public void onServerStarted(ServerContext context) {}
|
||||||
|
@Override public void createDefaultRegionData(Region region) {}
|
||||||
|
@Override public void onLivingWorldEvent(LivingWorldEvent event) {}
|
||||||
|
@Override public void saveModuleData(PersistenceWriter writer) {}
|
||||||
|
@Override public void loadModuleData(PersistenceReader reader) {}
|
||||||
|
@Override public void shutdown() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestRegionManager implements RegionManager {
|
||||||
|
private final Map<RegionCoordinate, Region> regions = new LinkedHashMap<>();
|
||||||
|
private int markDirtyCount;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Region> resolve(RegionCoordinate coordinate) {
|
||||||
|
return Optional.ofNullable(regions.get(coordinate));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Region> resolveAll(List<RegionCoordinate> coordinates) {
|
||||||
|
return coordinates.stream().map(regions::get).filter(java.util.Objects::nonNull).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markDirty(Region region) {
|
||||||
|
markDirtyCount++;
|
||||||
|
region.markDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestPersistenceService implements PersistenceService {
|
||||||
|
@Override public void save(Region region) {}
|
||||||
|
@Override public Optional<Region> load(RegionCoordinate coordinate) { return Optional.empty(); }
|
||||||
|
@Override public boolean supportsMigration() { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestProfiler implements SimulationProfiler {
|
||||||
|
private int startCount;
|
||||||
|
private int endCount;
|
||||||
|
|
||||||
|
@Override public void startCycle(long simulationTick) { startCount++; }
|
||||||
|
@Override public void endCycle(long durationMs) { endCount++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestTimeService implements TimeService {
|
||||||
|
private final DefaultTimeService delegate = new DefaultTimeService();
|
||||||
|
|
||||||
|
@Override public long getSimulationTick() { return delegate.getSimulationTick(); }
|
||||||
|
@Override public LivingWorldCalendar getCalendar() { return delegate.getCalendar(); }
|
||||||
|
@Override public void advanceSimulationTick() { delegate.advanceSimulationTick(); }
|
||||||
|
@Override public void advanceSimulationTicks(long ticks) { delegate.advanceSimulationTicks(ticks); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package com.livingworld.core.simulation;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import com.livingworld.config.SimulationConfig;
|
||||||
|
import com.livingworld.regions.RegionCoordinate;
|
||||||
|
|
||||||
|
class SimulationSchedulerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pollsHighestPriorityFirstAndRespectsRegionBudget() {
|
||||||
|
SimulationConfig config = new SimulationConfig();
|
||||||
|
config.setMaxRegionsPerCycle(3);
|
||||||
|
SimulationScheduler scheduler = new SimulationScheduler(config);
|
||||||
|
|
||||||
|
scheduler.queueRegion(job(2, 0));
|
||||||
|
scheduler.queueRegion(job(10, 1));
|
||||||
|
scheduler.queueRegion(job(5, 2));
|
||||||
|
scheduler.queueRegion(job(8, 3));
|
||||||
|
|
||||||
|
List<RegionUpdateJob> jobs = scheduler.pollJobsForCycle();
|
||||||
|
|
||||||
|
assertEquals(List.of(10, 8, 5), jobs.stream().map(RegionUpdateJob::priority).toList());
|
||||||
|
assertEquals(1, scheduler.getQueuedJobCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void equalPriorityJobsUseQueueTickAsDeterministicTieBreaker() {
|
||||||
|
SimulationScheduler scheduler = new SimulationScheduler(new SimulationConfig());
|
||||||
|
scheduler.queueRegion(job(5, 20));
|
||||||
|
scheduler.queueRegion(job(5, 10));
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
List.of(10L, 20L),
|
||||||
|
scheduler.pollJobsForCycle().stream()
|
||||||
|
.map(RegionUpdateJob::queuedAtSimulationTick)
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void regionUpdateJobNormalizesAndCopiesRequestedModules() {
|
||||||
|
List<String> mutableModules = new ArrayList<>(List.of("soil"));
|
||||||
|
RegionUpdateJob job = new RegionUpdateJob(
|
||||||
|
new RegionCoordinate("minecraft:overworld", 0, 0),
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
Set.copyOf(mutableModules),
|
||||||
|
UpdateReason.NORMAL_ROLLING_UPDATE);
|
||||||
|
|
||||||
|
mutableModules.add("pollution");
|
||||||
|
|
||||||
|
assertEquals(Set.of("soil"), job.requestedModules());
|
||||||
|
assertThrows(UnsupportedOperationException.class,
|
||||||
|
() -> job.requestedModules().add("water"));
|
||||||
|
|
||||||
|
RegionUpdateJob emptyModules = new RegionUpdateJob(
|
||||||
|
new RegionCoordinate("minecraft:overworld", 1, 0),
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
UpdateReason.NORMAL_ROLLING_UPDATE);
|
||||||
|
assertEquals(Set.of(), emptyModules.requestedModules());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void simulationCounterAdvancesOnlyWhenCycleCompletes() {
|
||||||
|
SimulationConfig config = new SimulationConfig();
|
||||||
|
config.setSimulationIntervalTicks(2);
|
||||||
|
SimulationScheduler scheduler = new SimulationScheduler(config);
|
||||||
|
|
||||||
|
scheduler.onMinecraftTick();
|
||||||
|
scheduler.onMinecraftTick();
|
||||||
|
|
||||||
|
assertEquals(0, scheduler.getSimulationTickCounter());
|
||||||
|
scheduler.recordCompletedSimulationCycle();
|
||||||
|
assertEquals(1, scheduler.getSimulationTickCounter());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RegionUpdateJob job(int priority, long queuedAtTick) {
|
||||||
|
return new RegionUpdateJob(
|
||||||
|
new RegionCoordinate("minecraft:overworld", priority, 0),
|
||||||
|
priority,
|
||||||
|
queuedAtTick,
|
||||||
|
Set.of(),
|
||||||
|
UpdateReason.NORMAL_ROLLING_UPDATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ package com.livingworld.events;
|
|||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -122,6 +124,36 @@ public class LivingWorldEventBusTest {
|
|||||||
assertEquals(2, bus.getPublishedEventCount());
|
assertEquals(2, bus.getPublishedEventCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multipleEventsAtSameTickAreAllDelivered() {
|
||||||
|
LivingWorldEventBus bus = new LivingWorldEventBus();
|
||||||
|
List<String> delivered = new ArrayList<>();
|
||||||
|
bus.register("soil", event -> delivered.add(event.eventType()));
|
||||||
|
bus.register("water", event -> delivered.add(event.eventType()));
|
||||||
|
|
||||||
|
bus.publish(new BaseLivingWorldEvent("soil", 10L, "soil"));
|
||||||
|
bus.publish(new BaseLivingWorldEvent("water", 10L, "water"));
|
||||||
|
|
||||||
|
assertEquals(List.of("soil", "water"), delivered);
|
||||||
|
assertEquals(2, bus.getPublishedEventCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void recursivelyPublishedEventIsQueuedAndDelivered() {
|
||||||
|
LivingWorldEventBus bus = new LivingWorldEventBus();
|
||||||
|
List<String> delivered = new ArrayList<>();
|
||||||
|
bus.register("first", event -> {
|
||||||
|
delivered.add("first");
|
||||||
|
bus.publish(new BaseLivingWorldEvent("second", event.simulationTick(), "first"));
|
||||||
|
});
|
||||||
|
bus.register("second", event -> delivered.add("second"));
|
||||||
|
|
||||||
|
bus.publish(new BaseLivingWorldEvent("first", 10L, "test"));
|
||||||
|
|
||||||
|
assertEquals(List.of("first", "second"), delivered);
|
||||||
|
assertEquals(2, bus.getPublishedEventCount());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getListenerCountReturnsCorrectNumberOfListeners() {
|
void getListenerCountReturnsCorrectNumberOfListeners() {
|
||||||
LivingWorldEventBus bus = new LivingWorldEventBus();
|
LivingWorldEventBus bus = new LivingWorldEventBus();
|
||||||
@@ -178,4 +210,4 @@ public class LivingWorldEventBusTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.livingworld.modules;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import com.livingworld.data.serialization.PersistenceReader;
|
||||||
|
import com.livingworld.data.serialization.PersistenceWriter;
|
||||||
|
import com.livingworld.events.LivingWorldEvent;
|
||||||
|
import com.livingworld.regions.Region;
|
||||||
|
|
||||||
|
class ModuleRegistryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void preservesRegistrationOrder() {
|
||||||
|
ModuleRegistry registry = new ModuleRegistry();
|
||||||
|
registry.register(new StubModule("soil"));
|
||||||
|
registry.register(new StubModule("water"));
|
||||||
|
registry.register(new StubModule("pollution"));
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
List.of("soil", "water", "pollution"),
|
||||||
|
registry.getEnabledModules().stream().map(SimulationModule::getModuleId).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsMismatchedModuleAndMetadataIds() {
|
||||||
|
ModuleRegistry registry = new ModuleRegistry();
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> registry.register(new StubModule("soil") {
|
||||||
|
@Override
|
||||||
|
public ModuleMetadata getMetadata() {
|
||||||
|
return metadata("water");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class StubModule implements SimulationModule {
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
private StubModule(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String getModuleId() { return id; }
|
||||||
|
@Override public ModuleMetadata getMetadata() { return metadata(id); }
|
||||||
|
@Override public void initialize(ModuleContext context) {}
|
||||||
|
@Override public void onServerStarted(ServerContext context) {}
|
||||||
|
@Override public void createDefaultRegionData(Region region) {}
|
||||||
|
@Override public ModuleUpdateResult updateRegion(RegionUpdateContext context) {
|
||||||
|
return ModuleUpdateResult.noChange();
|
||||||
|
}
|
||||||
|
@Override public void onLivingWorldEvent(LivingWorldEvent event) {}
|
||||||
|
@Override public void saveModuleData(PersistenceWriter writer) {}
|
||||||
|
@Override public void loadModuleData(PersistenceReader reader) {}
|
||||||
|
@Override public void shutdown() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModuleMetadata metadata(String id) {
|
||||||
|
return new ModuleMetadata(
|
||||||
|
id, id, "1.0.0", "", "1",
|
||||||
|
List.of(), List.of(), true, true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user