Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9837afc549 |
No files matched your search
@@ -752,7 +752,7 @@ public final class EquipmentSessionEvents implements Listener, AutoCloseable {
|
||||
UUID owner = event.getPlayer().getUniqueId();
|
||||
cancelCustody(owner);
|
||||
swaps.remove(owner);
|
||||
sessions.invalidateTail(owner);
|
||||
sessions.invalidateTail(owner, event.getView());
|
||||
}
|
||||
|
||||
private boolean reservedExchange(Player player, int slot) {
|
||||
|
||||
@@ -114,7 +114,12 @@ public final class EquipmentSessions implements AutoCloseable {
|
||||
boolean bookDelivered;
|
||||
boolean deliveryInvalidated;
|
||||
boolean conversionReported;
|
||||
boolean partyDefeatAcknowledged;
|
||||
boolean tailCallbackQueued;
|
||||
boolean defeatReturnTeleport;
|
||||
CharacterRecord deathRecord;
|
||||
ItemStack deathReference;
|
||||
Object deathNativeReference;
|
||||
org.bukkit.inventory.InventoryView deathView;
|
||||
Attempt(Player player, UUID owner, UUID character) {
|
||||
this.player = player; this.owner = owner; this.character = character;
|
||||
@@ -426,7 +431,12 @@ public final class EquipmentSessions implements AutoCloseable {
|
||||
}
|
||||
if (active.containsKey(owner)) return;
|
||||
cancelEntry(owner);
|
||||
if (owned.containsKey(owner)) {
|
||||
var previous = owned.get(owner);
|
||||
if (previous != null && previous.defeated) {
|
||||
retryTail(player, previous);
|
||||
return;
|
||||
}
|
||||
if (previous != null) {
|
||||
notice(owner, "BUSY", "Equipment persistence or recovery is pending; retry after it completes.");
|
||||
return;
|
||||
}
|
||||
@@ -673,30 +683,35 @@ public final class EquipmentSessions implements AutoCloseable {
|
||||
if (failure != null) { diagnostic(attempt, failure); conversionUncertain(attempt, "Death save failed or is uncertain"); return; }
|
||||
if (dead.isEmpty()) { conversionUncertain(attempt, "Character was missing or already dead; no new Tail authorized"); return; }
|
||||
attempt.deathResolved = true;
|
||||
attempt.deathRecord = dead.orElseThrow();
|
||||
if (closed) { conversionUncertain(attempt, "Shutdown before party defeat and Tail conversion"); return; }
|
||||
java.util.concurrent.CompletionStage<?> partyMutation;
|
||||
try { partyMutation = partyDefeat.apply(attempt.character); }
|
||||
catch (RuntimeException error) { diagnostic(attempt,error); conversionUncertain(attempt,"Party defeat mutation rejected"); return; }
|
||||
partyMutation.whenComplete((ignored,partyFailure) -> {
|
||||
if (partyFailure != null) { diagnostic(attempt,partyFailure); conversionUncertain(attempt,"Party defeat mutation failed"); return; }
|
||||
if (closed) { conversionUncertain(attempt,"Shutdown before Tail conversion"); return; }
|
||||
try {
|
||||
nextTick.accept(() -> {
|
||||
synchronized (EquipmentSessions.this) { deliverTail(attempt, dead.orElseThrow()); }
|
||||
});
|
||||
} catch (RuntimeException error) { diagnostic(attempt, error); conversionUncertain(attempt, "Tail callback rejected"); }
|
||||
synchronized (EquipmentSessions.this) {
|
||||
if (partyFailure != null) { diagnostic(attempt,partyFailure); conversionUncertain(attempt,"Party defeat mutation failed"); return; }
|
||||
if (closed) { conversionUncertain(attempt,"Shutdown before Tail conversion"); return; }
|
||||
attempt.partyDefeatAcknowledged = true;
|
||||
queueTail(attempt);
|
||||
}
|
||||
});
|
||||
});
|
||||
try {
|
||||
attempt.deathReference = copy(attempt.player.getInventory().getItemInOffHand());
|
||||
attempt.deathView = attempt.player.getOpenInventory();
|
||||
restoreExit(attempt);
|
||||
var restored = restoreExit(attempt);
|
||||
if (restored == EquipmentStorage.RestoreResult.RESTORED) {
|
||||
var retained = attempt.player.getInventory().getItemInOffHand();
|
||||
attempt.deathReference = copy(retained);
|
||||
attempt.deathNativeReference = nativeIdentity(retained);
|
||||
attempt.deathView = attempt.player.getOpenInventory();
|
||||
}
|
||||
// Read the current maximum after owned overlay cleanup, including legitimate
|
||||
// ordinary modifiers. Never reconstruct attributes from an entry snapshot.
|
||||
double maximum = Objects.requireNonNull(attempt.player.getAttribute(org.bukkit.attribute.Attribute.MAX_HEALTH)).getValue();
|
||||
event.setReviveHealth(maximum);
|
||||
attempt.player.setHealth(maximum); // Native cancellation only fills health when <= 0.
|
||||
notice(attempt.owner, "DEFEAT", "§cYour character was defeated.");
|
||||
attempt.defeatReturnTeleport = true;
|
||||
try {
|
||||
if (!attempt.player.teleport(attempt.entryLocation.clone())) {
|
||||
notice(attempt.owner, "DEFEAT_RETURN_FAILED", "Return to the session entry location was refused; you remain here.");
|
||||
@@ -704,6 +719,10 @@ public final class EquipmentSessions implements AutoCloseable {
|
||||
} catch (RuntimeException failure) {
|
||||
diagnostic(attempt, failure);
|
||||
notice(attempt.owner, "DEFEAT_RETURN_FAILED", "Return to the session entry location failed; you remain here.");
|
||||
} finally {
|
||||
attempt.defeatReturnTeleport = false;
|
||||
if (!attempt.deliveryInvalidated && owned.get(attempt.owner) == attempt && attempt.player.isOnline())
|
||||
attempt.deathView = attempt.player.getOpenInventory();
|
||||
}
|
||||
} catch (RuntimeException failure) {
|
||||
diagnostic(attempt, failure);
|
||||
@@ -727,21 +746,66 @@ public final class EquipmentSessions implements AutoCloseable {
|
||||
if (attempt != null && attempt.defeated) attempt.deliveryInvalidated = true;
|
||||
}
|
||||
|
||||
private void deliverTail(Attempt attempt, CharacterRecord dead) {
|
||||
if (attempt.bookAttempted) return;
|
||||
attempt.bookAttempted = true; // Even an exception after a native write never permits replay.
|
||||
/** The exact view closed by this plugin's defeat-return teleport may be rebound after teleport. */
|
||||
public synchronized void invalidateTail(UUID owner, org.bukkit.inventory.InventoryView closedView) {
|
||||
var attempt = owned.get(owner);
|
||||
if (attempt == null || !attempt.defeated) return;
|
||||
if (attempt.defeatReturnTeleport && attempt.deathView == closedView) {
|
||||
attempt.deathView = null;
|
||||
return;
|
||||
}
|
||||
attempt.deliveryInvalidated = true;
|
||||
}
|
||||
|
||||
private void queueTail(Attempt attempt) {
|
||||
if (closed || attempt.tailCallbackQueued || attempt.bookAttempted || attempt.bookDelivered
|
||||
|| attempt.deliveryInvalidated || !attempt.deathResolved || !attempt.partyDefeatAcknowledged
|
||||
|| !attempt.gearAcknowledged || attempt.deathRecord == null) return;
|
||||
attempt.tailCallbackQueued = true;
|
||||
try {
|
||||
if (!tailContext(attempt)
|
||||
|| !Objects.equals(attempt.deathReference, copy(attempt.player.getInventory().getItemInOffHand()))
|
||||
|| !sheets.identify(attempt.deathReference).filter(attempt.character::equals).isPresent()
|
||||
|| attempt.deathReference.getAmount() < 1
|
||||
|| attempt.deathReference.getAmount() > attempt.deathReference.getMaxStackSize()) {
|
||||
nextTick.accept(() -> {
|
||||
synchronized (EquipmentSessions.this) {
|
||||
attempt.tailCallbackQueued = false;
|
||||
deliverTail(attempt, attempt.deathRecord);
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException error) {
|
||||
attempt.tailCallbackQueued = false;
|
||||
conversionNotStarted(attempt, "Tail callback was rejected before its native write ("
|
||||
+ error.getClass().getSimpleName() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private void retryTail(Player player, Attempt attempt) {
|
||||
if (attempt.player != player || !player.isOnline() || attempt.bookAttempted || attempt.bookDelivered
|
||||
|| attempt.deliveryInvalidated || !tailCustody(attempt)) {
|
||||
attempt.deliveryInvalidated = true;
|
||||
notice(attempt.owner, "DEFEAT_INSPECTION_REQUIRED",
|
||||
"Death or Tail custody is ambiguous; items were retained for administrator inspection.");
|
||||
return;
|
||||
}
|
||||
if (!attempt.deathResolved || !attempt.partyDefeatAcknowledged || !attempt.gearAcknowledged
|
||||
|| attempt.deathRecord == null) {
|
||||
notice(attempt.owner, "DEFEAT_PENDING",
|
||||
"Death persistence, restoration or Tail prerequisites are still pending; no item was changed.");
|
||||
return;
|
||||
}
|
||||
attempt.tailCallbackQueued = false;
|
||||
deliverTail(attempt, attempt.deathRecord);
|
||||
}
|
||||
|
||||
private void deliverTail(Attempt attempt, CharacterRecord dead) {
|
||||
if (attempt.bookAttempted || attempt.bookDelivered) return;
|
||||
try {
|
||||
if (!tailContext(attempt) || !tailCustody(attempt)) {
|
||||
conversionUncertain(attempt, "Tail destination/context is stale or equipment restoration is unresolved"); return;
|
||||
}
|
||||
// Constructing a book cannot mutate native inventory and remains retryable if it fails.
|
||||
var book = new games.dmg.fantasystick.items.TailBooks().create(dead);
|
||||
attempt.bookAttempted = true; // From this point an exception may follow a native write; never replay.
|
||||
// An exact captured stack of copied references represents ONE character, not N
|
||||
// deaths. Replace that whole stack with ONE book; never scan/delete other copies,
|
||||
// move items, overwrite a changed stack, or grant into an available slot.
|
||||
var book = new games.dmg.fantasystick.items.TailBooks().create(dead);
|
||||
attempt.player.getInventory().setItemInOffHand(book);
|
||||
if (!tailContext(attempt) || !book.equals(attempt.player.getInventory().getItemInOffHand())) {
|
||||
conversionUncertain(attempt, "Context changed during Tail write; delivery may already have happened"); return;
|
||||
@@ -750,11 +814,24 @@ public final class EquipmentSessions implements AutoCloseable {
|
||||
release(attempt);
|
||||
sync(attempt);
|
||||
} catch (RuntimeException failure) {
|
||||
diagnostic(attempt, failure);
|
||||
conversionUncertain(attempt, "Tail native write failed or is uncertain; never replay");
|
||||
if (attempt.bookAttempted) {
|
||||
diagnostic(attempt, failure);
|
||||
conversionUncertain(attempt, "Tail native write failed or is uncertain; never replay");
|
||||
} else conversionNotStarted(attempt, "Tail construction failed before its native write ("
|
||||
+ failure.getClass().getSimpleName() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tailCustody(Attempt attempt) {
|
||||
var current = attempt.player.getInventory().getItemInOffHand();
|
||||
return attempt.deathReference != null && attempt.deathNativeReference != null
|
||||
&& Objects.equals(attempt.deathReference, copy(current))
|
||||
&& nativeIdentity(current) == attempt.deathNativeReference
|
||||
&& sheets.identify(attempt.deathReference).filter(attempt.character::equals).isPresent()
|
||||
&& attempt.deathReference.getAmount() >= 1
|
||||
&& attempt.deathReference.getAmount() <= attempt.deathReference.getMaxStackSize();
|
||||
}
|
||||
|
||||
private boolean tailContext(Attempt attempt) {
|
||||
if (closed || attempt.deliveryInvalidated || owned.get(attempt.owner) != attempt
|
||||
|| !attempt.player.isOnline() || attempt.player.getOpenInventory() != attempt.deathView) return false;
|
||||
@@ -763,6 +840,11 @@ public final class EquipmentSessions implements AutoCloseable {
|
||||
&& marker.phase() == EquipmentSessionMarker.Phase.RESTORED;
|
||||
}
|
||||
|
||||
private void conversionNotStarted(Attempt attempt, String reason) {
|
||||
diagnostic(attempt, new IllegalStateException(reason + "; character " + attempt.character
|
||||
+ ". The exact same-lifetime retained sheet may retry after acknowledged prerequisites."));
|
||||
}
|
||||
|
||||
private void conversionUncertain(Attempt attempt, String reason) {
|
||||
if (attempt.conversionReported) return;
|
||||
attempt.conversionReported = true;
|
||||
@@ -904,8 +986,18 @@ public final class EquipmentSessions implements AutoCloseable {
|
||||
? receipts.completeExit(attempt.owner, attempt.session, attempt.exitDelay)
|
||||
: receipts.recordExit(attempt.owner, attempt.session, attempt.exitDelay), (state, failure) -> {
|
||||
attempt.saving = false;
|
||||
if (failure != null) diagnostic(attempt, failure);
|
||||
else if (complete) { attempt.gearAcknowledged = true; release(attempt); }
|
||||
if (failure != null) {
|
||||
diagnostic(attempt, failure);
|
||||
if (attempt.defeated) conversionUncertain(attempt,
|
||||
"Equipment restoration persistence failed before Tail conversion");
|
||||
} else if (complete) {
|
||||
attempt.gearAcknowledged = true;
|
||||
if (attempt.defeated) queueTail(attempt);
|
||||
release(attempt);
|
||||
} else if (attempt.defeated) {
|
||||
conversionUncertain(attempt,
|
||||
"Equipment restoration was not acknowledged; Tail conversion remains unsafe");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -999,6 +1091,9 @@ public final class EquipmentSessions implements AutoCloseable {
|
||||
});
|
||||
} catch (RuntimeException failure) { fail(attempt, failure); }
|
||||
}
|
||||
private static Object nativeIdentity(ItemStack item) {
|
||||
return item == null || item.isEmpty() ? null : org.bukkit.craftbukkit.inventory.CraftItemStack.unwrap(item);
|
||||
}
|
||||
private static ItemStack copy(ItemStack item) { return item == null ? null : item.clone(); }
|
||||
private static ItemStack[] snapshot(Player player) {
|
||||
return Arrays.stream(player.getInventory().getContents()).map(EquipmentSessions::copy).toArray(ItemStack[]::new);
|
||||
|
||||
@@ -633,6 +633,110 @@ class EquipmentSessionEventsNativeTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test void crossWorldDefeatCloseRebindsProvedTailDeliveryWithOrWithoutArmor() throws Exception {
|
||||
for (boolean armored : new boolean[]{false, true}) try (var f = new Fixture()) {
|
||||
var a = f.actor;
|
||||
var overworld = org.mockito.Mockito.mock(org.bukkit.World.class);
|
||||
var end = org.mockito.Mockito.mock(org.bukkit.World.class);
|
||||
org.mockito.Mockito.when(overworld.getUID()).thenReturn(java.util.UUID.randomUUID());
|
||||
org.mockito.Mockito.when(end.getUID()).thenReturn(java.util.UUID.randomUUID());
|
||||
a.location = new org.bukkit.Location(overworld, 12, 70, -8, 45, 10);
|
||||
if (!armored) a.inventory.setArmorContents(new ItemStack[4]);
|
||||
var entry = a.location.clone(); f.core.activate(a);
|
||||
a.location = new org.bukkit.Location(end, 80, 64, 40);
|
||||
var closingView = a.view;
|
||||
a.teleport = () -> {
|
||||
f.dispatcher.accept(new org.bukkit.event.inventory.InventoryCloseEvent(closingView));
|
||||
a.view = null;
|
||||
};
|
||||
|
||||
f.dispatcher.accept(f.death(org.bukkit.damage.DamageType.OUT_OF_WORLD));
|
||||
f.core.tick();
|
||||
|
||||
assertEquals(entry, a.location);
|
||||
assertEquals(Material.WRITTEN_BOOK, a.inventory.getItemInOffHand().getType());
|
||||
assertTrue(f.core.characters.find(a.record.id()).get(5, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.orElseThrow().dead());
|
||||
assertNull(f.core.state(a).pending(), "Successful Tail delivery releases the completed receipt");
|
||||
var deadSheet = new games.dmg.fantasystick.items.CharacterSheets().create(a.record);
|
||||
a.inventory.setItemInOffHand(deadSheet); f.core.sessions.enter(a.player);
|
||||
f.core.characters.find(a.record.id()).get(5, java.util.concurrent.TimeUnit.SECONDS); f.core.tick();
|
||||
assertEquals("UNAVAILABLE_CHARACTER", f.core.notices.getLast().code(),
|
||||
"A completed defeat has no false persistence blocker and cannot reactivate");
|
||||
}
|
||||
}
|
||||
|
||||
@Test void exactRetainedSheetUseCompletesOnlyNeverStartedSameLifetimeTailConversion() throws Exception {
|
||||
try (var f = new Fixture()) {
|
||||
var a = f.actor; f.core.activate(a);
|
||||
var retained = a.inventory.getItemInOffHand().clone();
|
||||
f.dispatcher.accept(f.death(org.bukkit.damage.DamageType.GENERIC));
|
||||
var original = f.core.next();
|
||||
assertEquals(Material.PAPER, a.inventory.getItemInOffHand().getType());
|
||||
assertNull(f.core.state(a).pending(), "Ordinary restoration must be acknowledged before retry");
|
||||
|
||||
f.core.sessions.enter(a.player);
|
||||
assertEquals(Material.WRITTEN_BOOK, a.inventory.getItemInOffHand().getType());
|
||||
var delivered = a.inventory.getItemInOffHand().clone();
|
||||
original.run(); original.run(); f.core.sessions.enter(a.player);
|
||||
assertEquals(delivered, a.inventory.getItemInOffHand());
|
||||
assertEquals(1, a.teleports, "Retry never replays defeat restoration or return teleport");
|
||||
|
||||
a.inventory.setItemInOffHand(retained); f.core.sessions.enter(a.player);
|
||||
f.core.characters.find(a.record.id()).get(5, java.util.concurrent.TimeUnit.SECONDS); f.core.tick();
|
||||
assertEquals("UNAVAILABLE_CHARACTER", f.core.notices.getLast().code());
|
||||
assertEquals(Material.PAPER, a.inventory.getItemInOffHand().getType(),
|
||||
"The dead sheet cannot activate or create a second Tail after release");
|
||||
}
|
||||
|
||||
try (var f = new Fixture()) {
|
||||
var a = f.actor; f.core.activate(a);
|
||||
var copied = a.inventory.getItemInOffHand().clone();
|
||||
f.dispatcher.accept(f.death(org.bukkit.damage.DamageType.GENERIC));
|
||||
var original = f.core.next();
|
||||
a.inventory.setItemInOffHand(copied);
|
||||
f.core.sessions.enter(a.player);
|
||||
assertEquals(Material.PAPER, a.inventory.getItemInOffHand().getType());
|
||||
assertTrue(f.core.notices.getLast().message().contains("inspection"));
|
||||
original.run(); original.run();
|
||||
assertEquals(Material.PAPER, a.inventory.getItemInOffHand().getType(),
|
||||
"An apparently identical replacement sheet is not process-local custody proof");
|
||||
assertEquals(1, f.core.failures.size());
|
||||
f.core.expectedFailure = IllegalStateException.class;
|
||||
}
|
||||
}
|
||||
|
||||
@Test void rejectedOriginalTailCallbackRetainsNarrowSameLifetimeRetryAuthority() throws Exception {
|
||||
var entered = new java.util.concurrent.CountDownLatch(1);
|
||||
var release = new java.util.concurrent.CountDownLatch(1);
|
||||
var writes = new java.util.concurrent.atomic.AtomicInteger();
|
||||
try (var f = new Fixture((source, target) -> {
|
||||
if (writes.incrementAndGet() == 2) {
|
||||
entered.countDown();
|
||||
try { if (!release.await(5, java.util.concurrent.TimeUnit.SECONDS)) throw new java.io.IOException("Gate timeout"); }
|
||||
catch (InterruptedException interrupted) { throw new java.io.IOException(interrupted); }
|
||||
}
|
||||
EquipmentSessionsNativeTest.move(source, target);
|
||||
})) {
|
||||
var a = f.actor; f.core.activate(a);
|
||||
try {
|
||||
f.dispatcher.accept(f.death(org.bukkit.damage.DamageType.GENERIC));
|
||||
assertTrue(entered.await(5, java.util.concurrent.TimeUnit.SECONDS));
|
||||
assertNull(f.core.state(a).pending());
|
||||
f.core.rejectSchedule = true;
|
||||
} finally { release.countDown(); }
|
||||
f.core.characters.find(a.record.id()).get(5, java.util.concurrent.TimeUnit.SECONDS);
|
||||
assertEquals(Material.PAPER, a.inventory.getItemInOffHand().getType());
|
||||
assertEquals(1, f.core.failures.size()); f.core.expectedFailure = IllegalStateException.class;
|
||||
|
||||
f.core.rejectSchedule = false;
|
||||
f.core.sessions.enter(a.player);
|
||||
assertEquals(Material.WRITTEN_BOOK, a.inventory.getItemInOffHand().getType());
|
||||
assertNull(f.core.state(a).pending());
|
||||
assertEquals(1, a.teleports);
|
||||
}
|
||||
}
|
||||
|
||||
@Test void everyRegisteredDamageTypeUsesTheSameDeathBoundaryButOrdinaryAndPriorVetoesStayUntouched() throws Exception {
|
||||
int causes = 0;
|
||||
for (var type : org.bukkit.Registry.DAMAGE_TYPE) {
|
||||
@@ -717,6 +821,11 @@ class EquipmentSessionEventsNativeTest {
|
||||
assertEquals(1, f.core.failures.size()); assertInstanceOf(IllegalStateException.class, f.core.failures.getFirst().cause());
|
||||
f.core.expectedFailure = IllegalStateException.class;
|
||||
a.online = true; f.core.sessions.arrive(a.player); f.core.sessions.recover(a.player);
|
||||
if (scenario == 3) {
|
||||
f.core.sessions.enter(a.player);
|
||||
assertEquals("DEFEAT_INSPECTION_REQUIRED", f.core.notices.getLast().code(),
|
||||
"Reconnect loses same-lifetime Tail retry authority");
|
||||
}
|
||||
assertArrayEquals(before, a.snapshot()); assertTrue(f.core.tasks.isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -762,7 +871,7 @@ class EquipmentSessionEventsNativeTest {
|
||||
assertTrue(event.isCancelled()); assertArrayEquals(before, a.snapshot());
|
||||
assertTrue(a.marker().endsWith(exception ? "|RESTORING" : "|PACKED"));
|
||||
assertNotNull(f.core.state(a).pending()); assertEquals(a.normalMaximum, a.health);
|
||||
var callback = f.core.next(); callback.run(); callback.run();
|
||||
assertTrue(f.core.tasks.isEmpty(), "Unrestored equipment never queues a Tail write");
|
||||
assertArrayEquals(before, a.snapshot()); assertNotNull(f.core.state(a).pending());
|
||||
assertTrue(f.core.characters.find(a.record.id()).get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow().dead());
|
||||
assertEquals(exception ? 2 : 1, f.core.failures.size()); f.core.expectedFailure = IllegalStateException.class;
|
||||
@@ -790,6 +899,10 @@ class EquipmentSessionEventsNativeTest {
|
||||
assertTrue(entered.await(5, java.util.concurrent.TimeUnit.SECONDS));
|
||||
assertNull(f.core.state(a).pending(), "Gear receipt cleanup acknowledged first");
|
||||
assertEquals(Material.PAPER, a.inventory.getItemInOffHand().getType());
|
||||
f.core.sessions.enter(a.player);
|
||||
assertEquals("DEFEAT_PENDING", f.core.notices.getLast().code());
|
||||
assertEquals(Material.PAPER, a.inventory.getItemInOffHand().getType(),
|
||||
"Pending death persistence grants neither activation nor a Tail");
|
||||
competingRead.run();
|
||||
assertEquals("CHARACTER_BUSY", f.core.notices.getLast().code());
|
||||
assertTrue(f.core.sessions.active(copy.id).isEmpty()); assertTrue(f.core.tasks.isEmpty());
|
||||
@@ -824,7 +937,7 @@ class EquipmentSessionEventsNativeTest {
|
||||
assertEquals(Material.DIAMOND_SWORD, a.inventory.getItem(0).getType());
|
||||
assertEquals(sheet, a.inventory.getItemInOffHand());
|
||||
} finally { release.countDown(); }
|
||||
if (route == 0) f.core.tick();
|
||||
if (route == 0) assertTrue(f.core.tasks.isEmpty(), "Quit invalidates delivery before any Tail write");
|
||||
f.core.sessions.close(); f.core.sessions.drained().toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS);
|
||||
var dead = new games.dmg.fantasystick.persistence.YamlCharacterRepository(data).load(a.record.id()).orElseThrow();
|
||||
assertTrue(dead.dead()); assertEquals(sheet, a.inventory.getItemInOffHand()); assertEquals(2, writes.get());
|
||||
@@ -932,6 +1045,11 @@ class EquipmentSessionEventsNativeTest {
|
||||
callback.run(); a.offhandMutation = () -> {}; a.beforeOffhandMutation = () -> {};
|
||||
assertEquals(afterWrite ? Material.WRITTEN_BOOK : Material.PAPER, a.inventory.getItemInOffHand().getType());
|
||||
var current = a.snapshot(); callback.run(); f.core.sessions.recover(a.player);
|
||||
if (!afterWrite) {
|
||||
f.core.sessions.enter(a.player);
|
||||
assertEquals("DEFEAT_INSPECTION_REQUIRED", f.core.notices.getLast().code(),
|
||||
"A possible native write never authorizes retained-sheet retry");
|
||||
}
|
||||
assertArrayEquals(current, a.snapshot()); assertTrue(f.core.tasks.isEmpty());
|
||||
assertEquals(2, f.core.failures.size()); f.core.expectedFailure = IllegalStateException.class;
|
||||
assertTrue(f.core.characters.find(a.record.id()).get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow().dead());
|
||||
@@ -942,7 +1060,8 @@ class EquipmentSessionEventsNativeTest {
|
||||
try (var f = new Fixture()) {
|
||||
var a = f.actor; f.core.activate(a); var control = a.inventory.getItem(0).clone();
|
||||
a.inventory.setItem(0, new ItemStack(Material.EMERALD));
|
||||
f.dispatcher.accept(f.death(org.bukkit.damage.DamageType.GENERIC)); f.core.tick();
|
||||
f.dispatcher.accept(f.death(org.bukkit.damage.DamageType.GENERIC));
|
||||
assertTrue(f.core.tasks.isEmpty(), "Failed restoration queues no Tail write");
|
||||
assertNotNull(f.core.state(a).pending()); assertTrue(a.marker().endsWith("|PACKED"));
|
||||
var dead = f.core.characters.find(a.record.id()).get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow();
|
||||
a.inventory.setItem(20, a.inventory.getItem(0)); a.inventory.setItem(0, control);
|
||||
|
||||
@@ -285,7 +285,8 @@ class WholeCharacterTransferNativeTest {
|
||||
}
|
||||
if (ending == Ending.DEFEAT) {
|
||||
if (restoration != Restoration.SUCCESS) h.f.expectedFailure = IllegalStateException.class;
|
||||
h.f.tick(); // Actual asynchronous death acknowledgment / one-use Tail attempt.
|
||||
if (restoration == Restoration.SUCCESS)
|
||||
h.f.tick(); // Restoration acknowledgment permits the one-use Tail attempt.
|
||||
var dead = h.saved(record.id()); assertTrue(dead.dead()); assertProgress(renamed, dead);
|
||||
assertEquals(0, dead.health().signum());
|
||||
assertEquals(restoration == Restoration.SUCCESS ? Material.WRITTEN_BOOK : Material.PAPER,
|
||||
|
||||
Reference in new issue
Block a user