Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
539900eb14 | ||
|
|
9828590009 |
No files matched your search
@@ -44,7 +44,7 @@ Thirteen craft-persistence tests cover these backend boundaries, including worke
|
||||
|
||||
## Paper sheet and recipe definition
|
||||
|
||||
`CharacterSheets` renders one Paper sheet from a saved live record, with its current name and the exact offhand instruction. `fantasystick:sheet_version` (integer 1) and `character_id` (UUID string) identify the character, with no owner lock or duplicated progress snapshot. Identification is only a reference: it does not authorize entry or replace saved-state/session validation. Rendering from a dead record is refused.
|
||||
`CharacterSheets` renders one Paper sheet from a saved live record, with its current name and three separate lore lines: `Character sheet:`, `Place in off hand`, `to enter fantasy world.` Recipe previews and acknowledged crafted sheets use the same layout; existing items are not bulk-rewritten. `fantasystick:sheet_version` (integer 1) and `character_id` (UUID string) identify the character, with no owner lock or duplicated progress snapshot. Identification is only a reference: it does not authorize entry or replace saved-state/session validation. Rendering from a dead record is refused.
|
||||
|
||||
`CharacterFactory` allocates a fresh UUID and randomized two-part character name when explicitly called. It reuses the progression initializer for zero stats, level 1/zero XP, configured unspent points and full base health, with empty abilities/loadout/cooldowns and no prior death. Name collisions are allowed; identity is independent. Construction and recipe previews do not invoke allocation. Unit tests check fresh defaults/configured arbitrary-precision points, request-time allocation, seeded name variation and same-name/different-ID characters. A native composition test prepares a planned batch through YAML and verifies individually named sheets/IDs after native serialization without consuming or delivering inventory items. The assembled craft-event controller now invokes this allocator only for accepted current attempts.
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ public final class CharacterSheets {
|
||||
public static final NamespacedKey RECIPE = new NamespacedKey("fantasystick", "character_sheet");
|
||||
private static final NamespacedKey VERSION = new NamespacedKey("fantasystick", "sheet_version");
|
||||
private static final NamespacedKey CHARACTER = new NamespacedKey("fantasystick", "character_id");
|
||||
private static final String INSTRUCTION = "Character sheet: place in off hand to enter fantasy world.";
|
||||
private static final List<Component> INSTRUCTION = List.of(Component.text("Character sheet:"),
|
||||
Component.text("Place in off hand"), Component.text("to enter fantasy world."));
|
||||
|
||||
public ItemStack create(CharacterRecord character) {
|
||||
if (character.dead()) throw new IllegalArgumentException("Dead characters require authoritative revival before sheet creation");
|
||||
@@ -40,7 +41,7 @@ public final class CharacterSheets {
|
||||
var item = new ItemStack(Material.PAPER);
|
||||
var meta = item.getItemMeta();
|
||||
meta.displayName(Component.text(name));
|
||||
meta.lore(List.of(Component.text(INSTRUCTION)));
|
||||
meta.lore(INSTRUCTION);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ class CharacterBookNativeTest {
|
||||
f.players.put(a.core.id, a.player); f.players.put(b.core.id, b.player); f.activate(a);
|
||||
click(f, a); drainReads(f); // Old holder has a queued result, not a delivered book.
|
||||
a.core.inventory.setItemInOffHand(null); f.changed(a);
|
||||
f.receipts().find(a.core.id).toCompletableFuture().get(5, TimeUnit.SECONDS);
|
||||
f.awaitCharacterAndReceipt(a);
|
||||
var renamed = characters(f).renameCharacter(a.core.record.id(), "Traded 雪 character").get(5, TimeUnit.SECONDS).orElseThrow();
|
||||
runQueued(f); assertTrue(a.books.isEmpty());
|
||||
b.core.inventory.setItemInOffHand(new games.dmg.fantasystick.items.CharacterSheets().create(renamed));
|
||||
|
||||
@@ -84,6 +84,8 @@ class CharacterSheetsNativeTest {
|
||||
assertEquals(1, preview.getAmount());
|
||||
assertTrue(sheets.identify(preview).isEmpty());
|
||||
assertTrue(preview.getItemMeta().getPersistentDataContainer().isEmpty());
|
||||
assertEquals(List.of(Component.text("Character sheet:"), Component.text("Place in off hand"),
|
||||
Component.text("to enter fantasy world.")), preview.getItemMeta().lore());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +106,8 @@ class CharacterSheetsNativeTest {
|
||||
assertEquals(Material.PAPER, sheet.getType());
|
||||
assertEquals(1, sheet.getAmount());
|
||||
assertEquals(Component.text(record.name()), sheet.getItemMeta().displayName());
|
||||
assertEquals(List.of(Component.text("Character sheet: place in off hand to enter fantasy world.")), sheet.getItemMeta().lore());
|
||||
assertEquals(List.of(Component.text("Character sheet:"), Component.text("Place in off hand"),
|
||||
Component.text("to enter fantasy world.")), sheet.getItemMeta().lore());
|
||||
assertEquals(record.id(), sheets.identify(sheet).orElseThrow());
|
||||
assertEquals(Set.of(new NamespacedKey("fantasystick", "sheet_version"), new NamespacedKey("fantasystick", "character_id")),
|
||||
sheet.getItemMeta().getPersistentDataContainer().getKeys(), "No player owner, equipment or progress snapshot belongs on the sheet");
|
||||
|
||||
@@ -178,6 +178,9 @@ class FantasyStickLifecycleNativeTest {
|
||||
assertTrue(actor.fixture.view.getCursor().isEmpty());
|
||||
var complete = fixture.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(complete);
|
||||
complete.run();
|
||||
assertEquals(List.of(net.kyori.adventure.text.Component.text("Character sheet:"),
|
||||
net.kyori.adventure.text.Component.text("Place in off hand"),
|
||||
net.kyori.adventure.text.Component.text("to enter fantasy world.")), actor.fixture.view.getCursor().getItemMeta().lore());
|
||||
var id = new CharacterSheets().identify(actor.fixture.view.getCursor()).orElseThrow();
|
||||
var saved = new games.dmg.fantasystick.persistence.YamlCharacterRepository(directory).load(id).orElseThrow();
|
||||
assertEquals(new java.math.BigInteger(points), saved.unspentPoints());
|
||||
@@ -718,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());
|
||||
@@ -787,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);
|
||||
@@ -804,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(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1141,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();
|
||||
|
||||
@@ -234,7 +234,7 @@ class PartyBookNativeTest {
|
||||
open(f, a); var oldDisband = button(a, "[Disband party]");
|
||||
var sheet = a.core.inventory.getItemInOffHand().clone();
|
||||
a.core.inventory.setItemInOffHand(null); f.changed(a);
|
||||
f.receipts().find(a.core.id).toCompletableFuture().get(5, TimeUnit.SECONDS);
|
||||
f.awaitCharacterAndReceipt(a);
|
||||
b.core.inventory.setItemInOffHand(sheet); f.activate(b);
|
||||
open(f, b);
|
||||
assertTrue(text(b).contains(a.core.record.name()));
|
||||
|
||||
Reference in new issue
Block a user