|
|
|
@@ -721,6 +721,7 @@ class FantasyStickLifecycleNativeTest {
|
|
|
|
|
assertNull(saved.pending()); assertNull(saved.lastExit());
|
|
|
|
|
} else {
|
|
|
|
|
f.activate(a); sessions.exit(a.player);
|
|
|
|
|
f.awaitCharacterAndReceipt(a);
|
|
|
|
|
assertEquals(org.bukkit.GameMode.ADVENTURE, a.player.getGameMode());
|
|
|
|
|
var saved = f.receipts().find(a.core.id).toCompletableFuture().get().orElseThrow();
|
|
|
|
|
assertNotNull(saved.pending()); assertNotNull(saved.lastExit());
|
|
|
|
@@ -790,7 +791,7 @@ class FantasyStickLifecycleNativeTest {
|
|
|
|
|
var sessions = (games.dmg.fantasystick.session.EquipmentSessions) Fixture.field(f.plugin, "sessions");
|
|
|
|
|
sessions.exit(a.player);
|
|
|
|
|
assertEquals(org.bukkit.GameMode.CREATIVE, a.player.getGameMode());
|
|
|
|
|
f.receipts().find(a.core.id).toCompletableFuture().get();
|
|
|
|
|
f.awaitCharacterAndReceipt(a);
|
|
|
|
|
var b = new Actor(data); f.players.put(b.core.id, b.player);
|
|
|
|
|
b.core.gameMode = org.bukkit.GameMode.SURVIVAL;
|
|
|
|
|
f.activate(b);
|
|
|
|
@@ -807,27 +808,43 @@ class FantasyStickLifecycleNativeTest {
|
|
|
|
|
if (!value.equals("default")) java.nio.file.Files.writeString(directory.resolve("config.yml"), "character-swap-cooldown-seconds: " + value + "\n");
|
|
|
|
|
try (var f = new Fixture(directory)) {
|
|
|
|
|
f.plugin.setEnabled(true); var a = new Actor(directory); f.players.put(a.core.id, a.player); f.activate(a);
|
|
|
|
|
f.dispatcher.accept(new org.bukkit.event.player.PlayerQuitEvent(a.player, (net.kyori.adventure.text.Component) null));
|
|
|
|
|
var exit = f.receipts().find(a.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow().lastExit();
|
|
|
|
|
assertEquals(java.time.Duration.ofSeconds(value.equals("default") ? 900 : Long.parseLong(value)),
|
|
|
|
|
java.time.Duration.between(exit.exitedAt(), exit.deadline()));
|
|
|
|
|
assertEquals(a.core.record.id(), exit.characterId());
|
|
|
|
|
var other = new Actor(directory);
|
|
|
|
|
a.core.inventory.setItemInOffHand(other.core.inventory.getItemInOffHand().clone());
|
|
|
|
|
var before = a.core.snapshot();
|
|
|
|
|
f.dispatcher.accept(new org.bukkit.event.player.PlayerJoinEvent(a.player, (net.kyori.adventure.text.Component) null));
|
|
|
|
|
f.tick(); f.tick(); // Join receipt reconciliation, then actual coordinator eligibility.
|
|
|
|
|
if (value.equals("0")) {
|
|
|
|
|
f.tick(); assertTrue(a.core.marker().endsWith("|PACKED"), "Configured zero reaches entry, not only persisted arithmetic");
|
|
|
|
|
} else {
|
|
|
|
|
assertArrayEquals(before, a.core.snapshot()); assertTrue(a.core.marker().endsWith("|RESTORED"));
|
|
|
|
|
assertTrue(a.messages.getLast().matches("(?s).*Wait [1-9][0-9]* seconds.*same character may be resumed now.*"), a.messages::toString);
|
|
|
|
|
var state = f.receipts().find(a.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow();
|
|
|
|
|
assertNull(state.pending()); assertEquals(exit, state.lastExit());
|
|
|
|
|
a.core.inventory.setItemInOffHand(new CharacterSheets().create(a.core.record));
|
|
|
|
|
var characters = (CharacterStorage) Fixture.field(f.plugin, "characters");
|
|
|
|
|
var worker = (java.util.concurrent.ExecutorService) Fixture.field(characters, "worker");
|
|
|
|
|
var entered = new java.util.concurrent.CountDownLatch(1);
|
|
|
|
|
var release = new java.util.concurrent.CountDownLatch(1);
|
|
|
|
|
worker.execute(() -> {
|
|
|
|
|
entered.countDown();
|
|
|
|
|
try { release.await(); }
|
|
|
|
|
catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); }
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
assertTrue(entered.await(5, java.util.concurrent.TimeUnit.SECONDS));
|
|
|
|
|
f.dispatcher.accept(new org.bukkit.event.player.PlayerQuitEvent(a.player, (net.kyori.adventure.text.Component) null));
|
|
|
|
|
assertNotNull(f.receipts().find(a.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow().pending(),
|
|
|
|
|
"Quit restores native state but must retain ownership until health persistence completes");
|
|
|
|
|
release.countDown();
|
|
|
|
|
f.awaitCharacterAndReceipt(a);
|
|
|
|
|
var exit = f.receipts().find(a.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow().lastExit();
|
|
|
|
|
assertEquals(java.time.Duration.ofSeconds(value.equals("default") ? 900 : Long.parseLong(value)),
|
|
|
|
|
java.time.Duration.between(exit.exitedAt(), exit.deadline()));
|
|
|
|
|
assertEquals(a.core.record.id(), exit.characterId());
|
|
|
|
|
var other = new Actor(directory);
|
|
|
|
|
a.core.inventory.setItemInOffHand(other.core.inventory.getItemInOffHand().clone());
|
|
|
|
|
var before = a.core.snapshot();
|
|
|
|
|
f.dispatcher.accept(new org.bukkit.event.player.PlayerJoinEvent(a.player, (net.kyori.adventure.text.Component) null));
|
|
|
|
|
f.tick(); f.tick(); f.tick(); assertTrue(a.core.marker().endsWith("|PACKED"));
|
|
|
|
|
}
|
|
|
|
|
f.tick(); f.tick(); // Join receipt reconciliation, then actual coordinator eligibility.
|
|
|
|
|
if (value.equals("0")) {
|
|
|
|
|
f.tick(); assertTrue(a.core.marker().endsWith("|PACKED"), "Configured zero reaches entry, not only persisted arithmetic");
|
|
|
|
|
} else {
|
|
|
|
|
assertArrayEquals(before, a.core.snapshot()); assertTrue(a.core.marker().endsWith("|RESTORED"));
|
|
|
|
|
assertTrue(a.messages.getLast().matches("(?s).*Wait [1-9][0-9]* seconds.*same character may be resumed now.*"), a.messages::toString);
|
|
|
|
|
var state = f.receipts().find(a.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow();
|
|
|
|
|
assertNull(state.pending()); assertEquals(exit, state.lastExit());
|
|
|
|
|
a.core.inventory.setItemInOffHand(new CharacterSheets().create(a.core.record));
|
|
|
|
|
f.dispatcher.accept(new org.bukkit.event.player.PlayerJoinEvent(a.player, (net.kyori.adventure.text.Component) null));
|
|
|
|
|
f.tick(); f.tick(); f.tick(); assertTrue(a.core.marker().endsWith("|PACKED"));
|
|
|
|
|
}
|
|
|
|
|
} finally { release.countDown(); }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1144,6 +1161,12 @@ class FantasyStickLifecycleNativeTest {
|
|
|
|
|
games.dmg.fantasystick.persistence.EquipmentSessionStore receipts() {
|
|
|
|
|
return (games.dmg.fantasystick.persistence.EquipmentSessionStore) field(plugin, "receipts");
|
|
|
|
|
}
|
|
|
|
|
void awaitCharacterAndReceipt(Actor actor) throws Exception {
|
|
|
|
|
// A receipt read alone cannot await work submitted later by the character-save callback.
|
|
|
|
|
((CharacterStorage) field(plugin, "characters")).find(actor.core.record.id())
|
|
|
|
|
.get(5, java.util.concurrent.TimeUnit.SECONDS);
|
|
|
|
|
receipts().find(actor.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS);
|
|
|
|
|
}
|
|
|
|
|
void tick() throws Exception {
|
|
|
|
|
var task = tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS);
|
|
|
|
|
assertNotNull(task, "Missing deferred main-thread callback"); task.run();
|
|
|
|
|