test(native): await operation and audit acknowledgments
This commit is contained in:
1 parent
6563f61f19
commit
cd4d5a1360
9 files changed
+257
-97
No files matched your search
+35
-15
@@ -40,7 +40,14 @@ class AdminActionConfigurationCatalogNativeTest {
|
||||
|
||||
private static JsonObject values(JsonObject row) { return row.getAsJsonObject("data"); }
|
||||
private static String outcome(JsonObject row) { return values(row).get("outcome").getAsString(); }
|
||||
private List<JsonObject> actions() throws Exception { return AdminActionEntryNativeTest.rows(data); }
|
||||
private List<JsonObject> actions(FantasyStickLifecycleNativeTest.Fixture f) throws Exception {
|
||||
return NativeTestAwait.readAudit(f, () -> AdminActionEntryNativeTest.rows(data));
|
||||
}
|
||||
private List<JsonObject> awaitAction(FantasyStickLifecycleNativeTest.Fixture f, int before) throws Exception {
|
||||
return NativeTestAwait.auditUntil(f, () -> since(AdminActionEntryNativeTest.rows(data), before),
|
||||
rows -> rows.size() >= 3 && "result".equals(values(rows.getLast()).get("stage").getAsString())
|
||||
&& !"pending".equals(outcome(rows.getLast())), "terminal configuration action export");
|
||||
}
|
||||
private List<JsonObject> domain(String suffix) throws Exception {
|
||||
return ReskillEventsNativeTest.records(data).stream()
|
||||
.filter(row -> row.get("type").getAsString().equals("games.dmg.minecraft.fantasystick." + suffix)).toList();
|
||||
@@ -75,11 +82,12 @@ class AdminActionConfigurationCatalogNativeTest {
|
||||
var block = new AdminActionEntryNativeTest.Entry(fixture, "command_block", true);
|
||||
assertEquals(suggested, console.map.tabComplete(console.sender, "fantasystick:fstickadmin config "));
|
||||
assertEquals(suggested, block.map.tabComplete(block.sender, "fstickadmin config "));
|
||||
assertTrue(actions().isEmpty(), "catalog and value completion must never be audited");
|
||||
assertTrue(actions(f).isEmpty(), "catalog and value completion must never be audited");
|
||||
var inventory = fixture.actor.core.snapshot();
|
||||
var configuration = Files.readAllBytes(data.resolve("config.yml"));
|
||||
var actors = List.of(player, console, block);
|
||||
var checks = new java.util.ArrayList<org.junit.jupiter.api.function.Executable>();
|
||||
var allRows = new java.util.ArrayList<JsonObject>();
|
||||
for (int family = 0; family < FAMILIES.size(); family++) {
|
||||
for (var key : FAMILIES.get(family)) {
|
||||
// Every key is exercised; each family also has a non-player sender, without tripling all reads.
|
||||
@@ -90,18 +98,21 @@ class AdminActionConfigurationCatalogNativeTest {
|
||||
assertNotNull(valueSuggestions, "real current/default completion for " + key);
|
||||
var current = readValue(data, key);
|
||||
if (current == null && !valueSuggestions.isEmpty()) current = valueSuggestions.getFirst();
|
||||
var before = actions().size(); var domainBefore = domain("config.read").size();
|
||||
var before = actions(f).size();
|
||||
var domainBefore = NativeTestAwait.readAudit(f, () -> domain("config.read")).size();
|
||||
var messages = entry.messages().size();
|
||||
assertTrue(entry.map.dispatch(entry.sender, "fstickadmin config " + key), key);
|
||||
assertTrue(entry.messages().size() > messages, "real owner must respond to " + key);
|
||||
assertTrue(entry.messages().subList(messages, entry.messages().size()).stream()
|
||||
.anyMatch(message -> message.contains(key + "=")), "key-specific read, not generic usage: " + key);
|
||||
FixtureAuditAcknowledgmentNativeTest.awaitOwnQuiescence(f);
|
||||
var rows = since(actions(), before);
|
||||
var rows = awaitAction(f, before);
|
||||
allRows.addAll(rows);
|
||||
var expectedCurrent = current;
|
||||
var capturedReads = since(domain("config.read"), domainBefore);
|
||||
FixtureAuditAcknowledgmentNativeTest.awaitOwnQuiescence(f);
|
||||
var stableCount = actions().size();
|
||||
var capturedReads = key.equals(FrostShardSettings.RANGE)
|
||||
? NativeTestAwait.auditUntil(f, () -> since(domain("config.read"), domainBefore),
|
||||
reads -> !reads.isEmpty(), "authoritative Frost configuration read")
|
||||
: NativeTestAwait.readAudit(f, () -> since(domain("config.read"), domainBefore));
|
||||
var stableCount = actions(f).size();
|
||||
checks.add(() -> {
|
||||
sequence(rows, kind, "completed");
|
||||
assertEquals("read_completed", values(rows.getLast()).get("reason").getAsString(), key);
|
||||
@@ -129,6 +140,8 @@ class AdminActionConfigurationCatalogNativeTest {
|
||||
});
|
||||
}
|
||||
}
|
||||
FixtureAuditAcknowledgmentNativeTest.awaitOwnQuiescence(f);
|
||||
assertEquals(allRows, actions(f), "no delayed duplicate or extra action after the complete catalog");
|
||||
assertAll("actual completed configuration key reads", checks);
|
||||
}
|
||||
}
|
||||
@@ -166,6 +179,7 @@ class AdminActionConfigurationCatalogNativeTest {
|
||||
var suggestions = player.map.tabComplete(player.sender, "fstickadmin config ");
|
||||
var actors = List.of(player, console, block);
|
||||
var checks = new java.util.ArrayList<org.junit.jupiter.api.function.Executable>();
|
||||
var allRows = new java.util.ArrayList<JsonObject>();
|
||||
for (int i = 0; i < WRITES.size(); i++) {
|
||||
var pair = WRITES.get(i); var key = pair[0]; var requested = pair[1];
|
||||
assertTrue(suggestions.contains(key), "write must use an exposed active key: " + key);
|
||||
@@ -175,15 +189,19 @@ class AdminActionConfigurationCatalogNativeTest {
|
||||
var old = readValue(data, key);
|
||||
if (old == null) { assertFalse(values.isEmpty(), key); old = values.getFirst(); }
|
||||
assertNotEquals(old, requested, "write must actually change effective value: " + key);
|
||||
var before = actions().size(); var messages = entry.messages().size();
|
||||
var before = actions(f).size(); var messages = entry.messages().size();
|
||||
assertTrue(entry.map.dispatch(entry.sender, "fantasystick:fstickadmin config " + key + " " + requested));
|
||||
awaitWrite(f, entry, key, requested, messages);
|
||||
assertEquals(requested, readValue(data, key), "actual durable save: " + key);
|
||||
assertTrue(entry.map.tabComplete(entry.sender, "fstickadmin config " + key + " ").contains(requested),
|
||||
"native effective setting must change, not just the file: " + key);
|
||||
assertTrue(entry.messages().size() > messages, "native owner feedback: " + key);
|
||||
FixtureAuditAcknowledgmentNativeTest.awaitOwnQuiescence(f);
|
||||
var rows = since(actions(), before);
|
||||
var rows = awaitAction(f, before);
|
||||
allRows.addAll(rows);
|
||||
if (key.equals(FrostShardSettings.RANGE))
|
||||
NativeTestAwait.auditUntil(f, () -> domain("config.changed"),
|
||||
changes -> changes.stream().anyMatch(row -> values(row).get("key").getAsString().equals(key)),
|
||||
"authoritative Frost configuration write");
|
||||
var expectedBefore = old;
|
||||
checks.add(() -> {
|
||||
sequence(rows, kind, "completed");
|
||||
@@ -197,7 +215,7 @@ class AdminActionConfigurationCatalogNativeTest {
|
||||
assertEquals(requested, result.get("after").getAsString(), key);
|
||||
assertEquals("untouched", readValue(data, "retained"));
|
||||
if (key.equals(FrostShardSettings.RANGE)) {
|
||||
var changed = domain("config.changed").stream()
|
||||
var changed = NativeTestAwait.readAudit(f, () -> domain("config.changed")).stream()
|
||||
.filter(row -> values(row).get("key").getAsString().equals(key)).toList();
|
||||
assertEquals(1, changed.size(), "no replacement or duplicate authoritative Frost event");
|
||||
assertEquals(values(changed.getFirst()).get("action_id").getAsString(),
|
||||
@@ -205,6 +223,8 @@ class AdminActionConfigurationCatalogNativeTest {
|
||||
}
|
||||
});
|
||||
}
|
||||
FixtureAuditAcknowledgmentNativeTest.awaitOwnQuiescence(f);
|
||||
assertEquals(allRows, actions(f), "no delayed duplicate or extra action after all writes");
|
||||
assertAll("actual acknowledged configuration owner writes", checks);
|
||||
}
|
||||
}
|
||||
@@ -272,17 +292,17 @@ class AdminActionConfigurationCatalogNativeTest {
|
||||
@Test void unknownKeyAndMalformedTypedValuesRefuseWithoutSavingOrLeakingFreeformInput() throws Exception {
|
||||
seed();
|
||||
try (var fixture = new QuestTrackingAuditNativeFixture(data)) {
|
||||
var f = fixture.f;
|
||||
var entry = new AdminActionEntryNativeTest.Entry(fixture, "player", true);
|
||||
for (var command : List.of("config unregistered-password SECRET_VALUE",
|
||||
"config " + FrostShardSettings.RANGE + " https://secret.invalid/token",
|
||||
"config " + RoleplaySettings.KEY + " SECRET_VALUE")) {
|
||||
var config = Files.readAllBytes(data.resolve("config.yml"));
|
||||
var inventory = fixture.actor.core.snapshot(); var before = actions().size();
|
||||
var inventory = fixture.actor.core.snapshot(); var before = actions(f).size();
|
||||
var messages = entry.messages().size();
|
||||
assertTrue(entry.map.dispatch(entry.sender, "fstickadmin " + command));
|
||||
assertTrue(entry.messages().size() > messages, "native validation must run");
|
||||
FixtureAuditAcknowledgmentNativeTest.awaitOwnQuiescence(fixture.f);
|
||||
var rows = since(actions(), before);
|
||||
var rows = awaitAction(f, before);
|
||||
sequence(rows, "player", "refused");
|
||||
assertFalse(rows.toString().contains("SECRET_VALUE"));
|
||||
assertFalse(rows.toString().contains("secret.invalid"));
|
||||
|
||||
+1
-4
@@ -451,10 +451,7 @@ class AdminActionIdentityAndEquipmentNativeTest {
|
||||
}
|
||||
private static <T> T observeExport(FantasyStickLifecycleNativeTest.Fixture f,
|
||||
java.util.concurrent.Callable<T> read, long remaining) throws Exception {
|
||||
var writer = (java.util.concurrent.ThreadPoolExecutor) FantasyStickLifecycleNativeTest.Fixture.field(
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "eventLog"), "worker");
|
||||
try { return writer.submit(read).get(remaining, TimeUnit.NANOSECONDS); }
|
||||
catch (java.util.concurrent.ExecutionException error) { throw new AssertionError(error.getCause()); }
|
||||
return NativeTestAwait.readAudit(f, read, remaining);
|
||||
}
|
||||
private static void awaitExport(FantasyStickLifecycleNativeTest.Fixture f,
|
||||
java.util.concurrent.Callable<Boolean> observed) throws Exception {
|
||||
|
||||
+44
-31
@@ -39,7 +39,8 @@ final class AdventureParticipationModeNativeTest {
|
||||
"adventureQuests");
|
||||
f.command.execute(admin.player, "fantasystick", new String[]{"adventure-site", "place", "camp",
|
||||
"--mode", "individual-only", "10", "Solo Camp"});
|
||||
await(f, () -> !adventures.snapshot().sites().isEmpty());
|
||||
await(f, () -> !adventures.snapshot().sites().isEmpty()
|
||||
&& admin.messages.stream().anyMatch(message -> message.contains("mode=individual-only")));
|
||||
var site = adventures.snapshot().sites().getFirst();
|
||||
assertEquals(AdventureQuestState.ParticipationMode.INDIVIDUAL_ONLY, site.participationMode());
|
||||
assertTrue(admin.messages.getLast().contains("mode=individual-only"));
|
||||
@@ -54,7 +55,8 @@ final class AdventureParticipationModeNativeTest {
|
||||
assertTrue(admin.messages.getLast().contains("individual-only"));
|
||||
f.command.execute(admin.player, "fantasystick", new String[]{"adventure-site", "mode",
|
||||
site.id().toString(), "party-required"});
|
||||
drain(f);
|
||||
await(f, () -> adventures.findSite(site.id()).orElseThrow().participationMode()
|
||||
== AdventureQuestState.ParticipationMode.PARTY_REQUIRED);
|
||||
assertEquals(AdventureQuestState.ParticipationMode.PARTY_REQUIRED,
|
||||
adventures.findSite(site.id()).orElseThrow().participationMode());
|
||||
int count = adventures.snapshot().sites().size();
|
||||
@@ -74,11 +76,14 @@ final class AdventureParticipationModeNativeTest {
|
||||
new String[]{"dungeon", "mode", ""}));
|
||||
f.command.execute(admin.player, "fantasystick", new String[]{"dungeon", "mode",
|
||||
dungeon.id().toString(), "party-available"});
|
||||
drain(f);
|
||||
await(f, () -> dungeons.find(dungeon.id()).orElseThrow().participationMode()
|
||||
== AdventureQuestState.ParticipationMode.PARTY_AVAILABLE);
|
||||
assertEquals(AdventureQuestState.ParticipationMode.PARTY_AVAILABLE,
|
||||
dungeons.find(dungeon.id()).orElseThrow().participationMode());
|
||||
f.command.execute(admin.player, "fantasystick", new String[]{"dungeon", "mode",
|
||||
dungeon.id().toString(), "party-required"}); drain(f);
|
||||
dungeon.id().toString(), "party-required"});
|
||||
await(f, () -> dungeons.find(dungeon.id()).orElseThrow().participationMode()
|
||||
== AdventureQuestState.ParticipationMode.PARTY_REQUIRED);
|
||||
var room = dungeons.addRadiusRoom(dungeon.id(), "Boss", 0, 64, 0, 2, 3)
|
||||
.get(5, TimeUnit.SECONDS);
|
||||
dungeons.setBoss(dungeon.id(), room.id(), org.bukkit.entity.EntityType.HUSK, "Keeper",
|
||||
@@ -86,9 +91,11 @@ final class AdventureParticipationModeNativeTest {
|
||||
dungeons.setCompletionReward(dungeon.id(), java.math.BigInteger.ZERO, java.math.BigInteger.ZERO)
|
||||
.get(5, TimeUnit.SECONDS);
|
||||
f.command.execute(admin.player, "fantasystick", new String[]{"dungeon", "enable",
|
||||
dungeon.id().toString()}); drain(f);
|
||||
dungeon.id().toString()});
|
||||
await(f, () -> dungeons.find(dungeon.id()).orElseThrow().enabled());
|
||||
f.command.execute(admin.player, "fantasystick", new String[]{"dungeon", "open",
|
||||
dungeon.id().toString()}); drain(f);
|
||||
dungeon.id().toString()});
|
||||
await(f, () -> adventures.findSite(dungeon.id()).isPresent() && adventures.offerOpen(dungeon.id()));
|
||||
assertEquals(AdventureQuestState.ParticipationMode.PARTY_REQUIRED,
|
||||
adventures.findSite(dungeon.id()).orElseThrow().participationMode());
|
||||
}
|
||||
@@ -120,8 +127,7 @@ final class AdventureParticipationModeNativeTest {
|
||||
var controller = (AdventureQuestController) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,
|
||||
"adventureQuestController");
|
||||
|
||||
controller.open(member.player); drain(f);
|
||||
var memberMenu = member.inventories.getLast();
|
||||
var memberMenu = open(f, controller, member);
|
||||
var personalItem = java.util.stream.IntStream.range(0, memberMenu.getSize())
|
||||
.mapToObj(memberMenu::getItem).filter(java.util.Objects::nonNull)
|
||||
.filter(item -> PlainTextComponentSerializer.plainText().serialize(item.getItemMeta().displayName())
|
||||
@@ -134,16 +140,19 @@ final class AdventureParticipationModeNativeTest {
|
||||
when(personalClick.getWhoClicked()).thenReturn(member.player);
|
||||
when(personalClick.getInventory()).thenReturn(memberMenu);
|
||||
when(personalClick.getCurrentItem()).thenReturn(personalItem);
|
||||
controller.click(personalClick); drain(f);
|
||||
int personalFeedback = member.messages.size();
|
||||
controller.click(personalClick);
|
||||
accepted(f, member, personalFeedback);
|
||||
var personalQuest = adventures.activeFor(member.core.record.id(), party.id()).orElseThrow();
|
||||
assertEquals(AdventureQuestState.UnitType.SOLO, personalQuest.unitType());
|
||||
assertEquals(Set.of(member.core.record.id()), personalQuest.snapshot());
|
||||
assertTrue(adventures.abandon(personalQuest.id(), personalQuest.unit()).get(5, TimeUnit.SECONDS));
|
||||
|
||||
controller.open(member.player); drain(f);
|
||||
var memberIndividualMenu = member.inventories.getLast();
|
||||
var memberIndividualMenu = open(f, controller, member);
|
||||
var individualItem = offer(memberIndividualMenu, "Solo Camp");
|
||||
click(controller, member, memberIndividualMenu, individualItem, false); drain(f);
|
||||
int memberFeedback = member.messages.size();
|
||||
click(controller, member, memberIndividualMenu, individualItem, false);
|
||||
accepted(f, member, memberFeedback);
|
||||
var memberIndividual = adventures.activeFor(member.core.record.id(), party.id()).orElseThrow();
|
||||
assertEquals(individual.id(), memberIndividual.site());
|
||||
assertEquals(AdventureQuestState.UnitType.SOLO, memberIndividual.unitType());
|
||||
@@ -158,9 +167,11 @@ final class AdventureParticipationModeNativeTest {
|
||||
|
||||
var partyless = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
partyless.core.location.setWorld(leader.world); f.players.put(partyless.core.id, partyless.player);
|
||||
f.activate(partyless); controller.open(partyless.player); drain(f);
|
||||
var partylessMenu = partyless.inventories.getLast();
|
||||
click(controller, partyless, partylessMenu, offer(partylessMenu, "Solo Camp"), false); drain(f);
|
||||
f.activate(partyless);
|
||||
var partylessMenu = open(f, controller, partyless);
|
||||
int partylessFeedback = partyless.messages.size();
|
||||
click(controller, partyless, partylessMenu, offer(partylessMenu, "Solo Camp"), false);
|
||||
accepted(f, partyless, partylessFeedback);
|
||||
var partylessQuest = adventures.activeFor(partyless.core.record.id(), null).orElseThrow();
|
||||
assertEquals(AdventureQuestState.UnitType.SOLO, partylessQuest.unitType());
|
||||
assertTrue(adventures.abandon(partylessQuest.id(), partylessQuest.unit()).get(5, TimeUnit.SECONDS));
|
||||
@@ -170,18 +181,18 @@ final class AdventureParticipationModeNativeTest {
|
||||
click(controller, partyless, partylessMenu, partylessRequired, true);
|
||||
assertTrue(partyless.messages.getLast().contains("Only the active party leader"));
|
||||
|
||||
controller.open(leader.player); drain(f);
|
||||
var leaderIndividualMenu = leader.inventories.getLast();
|
||||
var leaderIndividualMenu = open(f, controller, leader);
|
||||
var leaderIndividualItem = offer(leaderIndividualMenu, "Solo Camp");
|
||||
click(controller, leader, leaderIndividualMenu, leaderIndividualItem, false); drain(f);
|
||||
int leaderFeedback = leader.messages.size();
|
||||
click(controller, leader, leaderIndividualMenu, leaderIndividualItem, false);
|
||||
accepted(f, leader, leaderFeedback);
|
||||
var leaderIndividual = adventures.activeFor(leader.core.record.id(), party.id()).orElseThrow();
|
||||
assertEquals(AdventureQuestState.UnitType.SOLO, leaderIndividual.unitType());
|
||||
assertTrue(adventures.abandon(leaderIndividual.id(), leaderIndividual.unit()).get(5, TimeUnit.SECONDS));
|
||||
click(controller, leader, leaderIndividualMenu, leaderIndividualItem, true);
|
||||
assertTrue(leader.messages.getLast().contains("individual-only"));
|
||||
|
||||
controller.open(leader.player); drain(f);
|
||||
var leaderMenu = leader.inventories.getLast();
|
||||
var leaderMenu = open(f, controller, leader);
|
||||
var requiredItem = java.util.stream.IntStream.range(0, leaderMenu.getSize())
|
||||
.mapToObj(leaderMenu::getItem).filter(java.util.Objects::nonNull)
|
||||
.filter(item -> PlainTextComponentSerializer.plainText().serialize(item.getItemMeta().displayName())
|
||||
@@ -203,9 +214,10 @@ final class AdventureParticipationModeNativeTest {
|
||||
party = parties.create(leader.core.id).orElseThrow();
|
||||
assertTrue(parties.accept(member.core.id,
|
||||
parties.invite(leader.core.id, party.id(), member.core.id).orElseThrow().id()));
|
||||
int partyFeedback = leader.messages.size();
|
||||
controller.click(partyClick);
|
||||
UUID acceptedParty = party.id();
|
||||
await(f, () -> adventures.activeFor(leader.core.record.id(), acceptedParty).isPresent());
|
||||
accepted(f, leader, partyFeedback);
|
||||
var partyQuest = adventures.activeFor(leader.core.record.id(), acceptedParty).orElseThrow();
|
||||
assertEquals(AdventureQuestState.UnitType.PARTY, partyQuest.unitType());
|
||||
assertEquals(Set.of(leader.core.record.id(), member.core.record.id()), partyQuest.snapshot());
|
||||
@@ -228,18 +240,19 @@ final class AdventureParticipationModeNativeTest {
|
||||
when(event.getCurrentItem()).thenReturn(item); when(event.isRightClick()).thenReturn(right);
|
||||
controller.click(event);
|
||||
}
|
||||
private static void drain(FantasyStickLifecycleNativeTest.Fixture f) throws Exception {
|
||||
for (int attempt = 0; attempt < 20; attempt++) {
|
||||
var task = f.tasks.poll(50, TimeUnit.MILLISECONDS);
|
||||
if (task == null) break;
|
||||
task.run();
|
||||
}
|
||||
private static org.bukkit.inventory.Inventory open(FantasyStickLifecycleNativeTest.Fixture f,
|
||||
AdventureQuestController controller, FantasyStickLifecycleNativeTest.Actor actor) throws Exception {
|
||||
int before = actor.inventories.size();
|
||||
controller.open(actor.player);
|
||||
await(f, () -> actor.inventories.size() > before);
|
||||
return actor.inventories.getLast();
|
||||
}
|
||||
private static void accepted(FantasyStickLifecycleNativeTest.Fixture f,
|
||||
FantasyStickLifecycleNativeTest.Actor actor, int before) throws Exception {
|
||||
await(f, () -> actor.messages.stream().skip(before).anyMatch(message -> message.startsWith("Accepted ")));
|
||||
}
|
||||
private static void await(FantasyStickLifecycleNativeTest.Fixture f,
|
||||
java.util.function.BooleanSupplier condition) throws Exception {
|
||||
for (int attempt = 0; attempt < 50 && !condition.getAsBoolean(); attempt++) {
|
||||
drain(f); Thread.sleep(5);
|
||||
}
|
||||
assertTrue(condition.getAsBoolean());
|
||||
NativeTestAwait.until(f, condition, "requested participation-mode result");
|
||||
}
|
||||
}
|
||||
@@ -50,10 +50,10 @@ class DungeonNativeTest {
|
||||
assertTrue(f.command.tabComplete(admin.player,"fantasystick",new String[]{"dungeon",""}).contains("teleport"));
|
||||
assertEquals(List.of(disabled.id().toString(),enabled.id().toString()).stream().sorted().toList(),
|
||||
f.command.tabComplete(admin.player,"fantasystick",new String[]{"dungeon","teleport",""}));
|
||||
f.command.execute(admin.player,"fantasystick",new String[]{"dungeon","teleport",disabled.id().toString()});drain(f);drain(f);
|
||||
command(f,admin,"dungeon","teleport",disabled.id().toString());
|
||||
var first=teleports.getFirst();assertSame(destination.world,first.getWorld());assertEquals(34.75,first.getX());assertEquals(66.5,first.getY());assertEquals(18.25,first.getZ());assertEquals(91.5f,first.getYaw());assertEquals(-7.25f,first.getPitch());
|
||||
assertEquals("Teleported to Disabled Vault (dungeon entrance) at Dungeon Realm (34, 66, 18).",admin.messages.getLast());
|
||||
f.command.execute(admin.player,"fantasystick",new String[]{"dungeon","teleport",enabled.id().toString()});drain(f);drain(f);
|
||||
command(f,admin,"dungeon","teleport",enabled.id().toString());
|
||||
var second=teleports.getLast();assertEquals(-20.5,second.getX());assertEquals(55.25,second.getY());assertEquals(-33.75,second.getZ());assertEquals(-135f,second.getYaw());assertEquals(22f,second.getPitch());
|
||||
assertTrue(admin.messages.getLast().contains("Enabled Depths (dungeon entrance) at Dungeon Realm (-21, 55, -34)"));
|
||||
assertEquals(before,directory.snapshot());
|
||||
@@ -81,12 +81,12 @@ class DungeonNativeTest {
|
||||
actor.particleCalls.clear();pulse(f);assertFalse(actor.particleCalls.isEmpty());assertTrue(actor.particleCalls.size()<=1024);assertTrue(actor.particleCalls.stream().allMatch(call->call[0]==Particle.DUST));var particlePreferences=(games.dmg.fantasystick.persistence.PlayerFeedbackPreferences)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"feedbackPreferences");assertTrue(particlePreferences.publish(particlePreferences.setAllParticles(actor.core.id,false).toCompletableFuture().get(5,TimeUnit.SECONDS)));actor.particleCalls.clear();pulse(f);assertTrue(actor.particleCalls.isEmpty(),"All-particle opt-out suppresses dungeon-editor boundaries");assertTrue(particlePreferences.publish(particlePreferences.setAllParticles(actor.core.id,true).toCompletableFuture().get(5,TimeUnit.SECONDS)));var outsider=new FantasyStickLifecycleNativeTest.Actor(data);outsider.core.location.setWorld(actor.world);f.players.put(outsider.core.id,outsider.player);doReturn(true).when(outsider.player).hasPermission("fantasystick.admin");command(f,outsider,"dungeon","edit","start",dungeon.id().toString());assertTrue(outsider.messages.getLast().contains("editing lease"));f.command.execute(outsider.player,"fantasystick",new String[]{"dungeon","reward",dungeon.id().toString(),"90","90"});assertTrue(outsider.messages.getLast().contains("another administrator"));assertEquals(BigInteger.valueOf(30),dungeons.find(dungeon.id()).orElseThrow().completionReward().xp());assertTrue(outsider.particleCalls.isEmpty());
|
||||
command(f,actor,"dungeon","edit","done");actor.particleCalls.clear();pulse(f);assertTrue(actor.particleCalls.isEmpty());command(f,actor,"dungeon","enable",dungeon.id().toString());command(f,actor,"dungeon","open",dungeon.id().toString());
|
||||
var adventures=(AdventureQuestDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuests");assertTrue(adventures.snapshot().catalog().sites().contains(dungeon.id()));assertEquals(new AdventureQuestDirectory.Reward(BigInteger.valueOf(30),BigInteger.valueOf(5)),adventures.reward(adventures.findSite(dungeon.id()).orElseThrow()));
|
||||
f.activate(actor);var questController=(AdventureQuestController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuestController");questController.open(actor.player);drain(f);var menu=actor.inventories.getLast();assertEquals(Material.PAPER,menu.getItem(0).getType());var offerLore=menu.getItem(0).getItemMeta().lore().toString();assertTrue(offerLore.contains("Completion trophy pool: 30 XP, 5 coins"));assertTrue(offerLore.contains("additional variable XP, coins and native loot"));var click=mock(InventoryClickEvent.class);when(click.getWhoClicked()).thenReturn(actor.player);when(click.getInventory()).thenReturn(menu);when(click.getCurrentItem()).thenReturn(menu.getItem(0));questController.click(click);drain(f);assertEquals(dungeon.id(),adventures.active().getFirst().site());var partyA=new FantasyStickLifecycleNativeTest.Actor(data);var partyB=new FantasyStickLifecycleNativeTest.Actor(data);for(var member:List.of(partyA,partyB)){member.core.location.setWorld(actor.world);member.core.location.setX(100);f.players.put(member.core.id,member.player);doReturn(java.util.Set.of()).when(member.player).getActivePotionEffects();doReturn(true).when(member.player).addPotionEffect(any(org.bukkit.potion.PotionEffect.class),anyBoolean());f.activate(member);}var parties=PartyLifecycleNativeTest.parties(f);var party=parties.create(partyA.core.id).orElseThrow();assertTrue(parties.accept(partyB.core.id,parties.invite(partyA.core.id,party.id(),partyB.core.id).orElseThrow().id()));var catalog=adventures.snapshot().catalog();assertEquals(AdventureQuestDirectory.Acceptance.APPLIED,adventures.accept(catalog.revision(),dungeon.id(),AdventureQuestState.UnitType.PARTY,party.id(),Set.of(partyA.core.record.id(),partyB.core.record.id())).get(5,TimeUnit.SECONDS).status());
|
||||
f.activate(actor);var questController=(AdventureQuestController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuestController");var menu=open(f,questController,actor);assertEquals(Material.PAPER,menu.getItem(0).getType());var offerLore=menu.getItem(0).getItemMeta().lore().toString();assertTrue(offerLore.contains("Completion trophy pool: 30 XP, 5 coins"));assertTrue(offerLore.contains("additional variable XP, coins and native loot"));var click=mock(InventoryClickEvent.class);when(click.getWhoClicked()).thenReturn(actor.player);when(click.getInventory()).thenReturn(menu);when(click.getCurrentItem()).thenReturn(menu.getItem(0));int acceptedFeedback=actor.messages.size();questController.click(click);await(f,()->actor.messages.stream().skip(acceptedFeedback).anyMatch(message->message.startsWith("Accepted ")),actor.messages);assertEquals(dungeon.id(),adventures.active().getFirst().site());var partyA=new FantasyStickLifecycleNativeTest.Actor(data);var partyB=new FantasyStickLifecycleNativeTest.Actor(data);for(var member:List.of(partyA,partyB)){member.core.location.setWorld(actor.world);member.core.location.setX(100);f.players.put(member.core.id,member.player);doReturn(java.util.Set.of()).when(member.player).getActivePotionEffects();doReturn(true).when(member.player).addPotionEffect(any(org.bukkit.potion.PotionEffect.class),anyBoolean());f.activate(member);}var parties=PartyLifecycleNativeTest.parties(f);var party=parties.create(partyA.core.id).orElseThrow();assertTrue(parties.accept(partyB.core.id,parties.invite(partyA.core.id,party.id(),partyB.core.id).orElseThrow().id()));var catalog=adventures.snapshot().catalog();assertEquals(AdventureQuestDirectory.Acceptance.APPLIED,adventures.accept(catalog.revision(),dungeon.id(),AdventureQuestState.UnitType.PARTY,party.id(),Set.of(partyA.core.record.id(),partyB.core.record.id())).get(5,TimeUnit.SECONDS).status());
|
||||
var storage=(CharacterStorage)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characters");var before=storage.find(actor.core.record.id()).get().orElseThrow();var partyABefore=storage.find(partyA.core.record.id()).get().orElseThrow();var partyBBefore=storage.find(partyB.core.record.id()).get().orElseThrow();for(var member:List.of(actor,partyA)){member.core.location.setX(first.bounds().minX()+.5);member.core.location.setY(first.bounds().minY()+1);member.core.location.setZ(first.bounds().minZ()+.5);}
|
||||
pulse(f);drain(f);pulse(f);drain(f);var initialMobs=spawned.stream().filter(Husk.class::isInstance).toList();assertEquals(2,initialMobs.size());var ordinary=initialMobs.getFirst();assertEquals(games.dmg.fantasystick.progression.ManagedEncounterOrigin.Kind.DUNGEON,dungeonController.encounterOrigin(ordinary).orElseThrow().kind());var copiedSlot=mock(Husk.class);when(copiedSlot.getUniqueId()).thenReturn(UUID.randomUUID());var copiedProof=ordinary.getPersistentDataContainer();when(copiedSlot.getPersistentDataContainer()).thenReturn(copiedProof);assertTrue(dungeonController.encounterOrigin(copiedSlot).isEmpty(),"a copied valid dungeon slot tag is not an authenticated registered spawn");var nativeDrop=new ItemStack(Material.ROTTEN_FLESH);var forgedDeath=new EntityDeathEvent(copiedSlot,mock(org.bukkit.damage.DamageSource.class),new ArrayList<>(List.of(nativeDrop)),7);f.dispatcher.accept(forgedDeath);assertEquals(7,forgedDeath.getDroppedExp());assertEquals(List.of(nativeDrop),forgedDeath.getDrops());assertTrue(dungeonController.encounterOrigin(ordinary).isPresent(),"copied callback did not consume the real dungeon slot");var survivor=initialMobs.getLast();assertNotEquals(dungeonController.encounterOrigin(ordinary),dungeonController.encounterOrigin(survivor),"each slot has independent kill authority");var survivorOrigin=dungeonController.encounterOrigin(survivor);var source=mock(org.bukkit.damage.DamageSource.class);when(source.getCausingEntity()).thenReturn(actor.player);when(source.getDirectEntity()).thenReturn(actor.player);var partySource=mock(org.bukkit.damage.DamageSource.class);when(partySource.getCausingEntity()).thenReturn(partyA.player);when(partySource.getDirectEntity()).thenReturn(partyA.player);f.dispatcher.accept(new EntityDamageByEntityEvent(partyA.player,ordinary,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,partySource,1));f.dispatcher.accept(new EntityDamageByEntityEvent(actor.player,ordinary,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,1));var drops=new ArrayList<ItemStack>();drops.add(new ItemStack(Material.ROTTEN_FLESH));var ordinaryDeath=new EntityDeathEvent(ordinary,source,drops,7);f.dispatcher.accept(ordinaryDeath);await(f,()->dungeons.runView(dungeon.id(),adventures.snapshot().catalog().revision()).map(run->run.defeatedSlots().size()==1).orElse(false),actor.messages);assertEquals(7,ordinaryDeath.getDroppedExp());assertFalse(ordinaryDeath.getDrops().isEmpty());var partial=dungeons.runView(dungeon.id(),adventures.snapshot().catalog().revision()).orElseThrow();assertEquals(1,partial.defeatedSlots().size());assertFalse(partial.clearedRooms().contains(first.id()));for(var member:List.of(actor,partyA))member.core.location.setX(100);pulse(f);verify(survivor,atLeastOnce()).remove();assertFalse(actorEffects.containsKey(org.bukkit.potion.PotionEffectType.SPEED));for(var member:List.of(actor,partyA)){member.core.location.setX(first.bounds().minX()+.5);member.core.location.setY(first.bounds().minY()+1);member.core.location.setZ(first.bounds().minZ()+.5);}pulse(f);drain(f);var respawned=spawned.stream().filter(Husk.class::isInstance).toList().getLast();assertNotSame(survivor,respawned);assertEquals(survivorOrigin,dungeonController.encounterOrigin(respawned),"respawned slot retains duplicate-safe kill authority");f.dispatcher.accept(new EntityDamageByEntityEvent(partyA.player,respawned,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,partySource,1));f.dispatcher.accept(new EntityDamageByEntityEvent(actor.player,respawned,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,1));f.dispatcher.accept(new EntityDeathEvent(respawned,source,new ArrayList<>(),0));await(f,()->dungeons.runView(dungeon.id(),adventures.snapshot().catalog().revision()).orElseThrow().clearedRooms().contains(first.id()),actor.messages);assertTrue(storage.find(partyB.core.record.id()).get().orElseThrow().experience().compareTo(partyBBefore.experience())>0,"ordinary party combat sharing remains active");
|
||||
var actorAfterKills=storage.find(actor.core.record.id()).get().orElseThrow();var partyAAfterKills=storage.find(partyA.core.record.id()).get().orElseThrow();var partyBAfterKills=storage.find(partyB.core.record.id()).get().orElseThrow();for(var member:List.of(actor,partyA,partyB)){member.core.location.setX(bossRoom.bounds().minX()+.5);member.core.location.setY(bossRoom.bounds().minY()+1);member.core.location.setZ(bossRoom.bounds().minZ()+.5);}pulse(f);drain(f);var firstBoss=spawned.stream().filter(Vindicator.class::isInstance).findFirst().orElseThrow();for(var member:List.of(actor,partyA,partyB))member.core.location.setX(100);pulse(f);verify(firstBoss,atLeastOnce()).remove();for(var member:List.of(actor,partyA,partyB)){member.core.location.setX(bossRoom.bounds().minX()+.5);member.core.location.setY(bossRoom.bounds().minY()+1);member.core.location.setZ(bossRoom.bounds().minZ()+.5);}pulse(f);drain(f);var boss=spawned.stream().filter(Vindicator.class::isInstance).toList().getLast();assertNotSame(firstBoss,boss);f.dispatcher.accept(new EntityDamageByEntityEvent(actor.player,boss,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,1));f.dispatcher.accept(new EntityDamageByEntityEvent(partyA.player,boss,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,partySource,1));f.dispatcher.accept(new EntityDeathEvent(boss,source,new ArrayList<>(),0));assertEquals(games.dmg.fantasystick.progression.ManagedEncounterOrigin.Kind.DUNGEON,dungeonController.encounterOrigin(boss).orElseThrow().kind(),"boss cleanup retains immutable managed origin authority");
|
||||
pulse(f);await(f,()->dungeons.runView(dungeon.id(),catalog.revision()).isPresent(),actor.messages);pulse(f);var initialMobs=spawned.stream().filter(Husk.class::isInstance).toList();assertEquals(2,initialMobs.size());var ordinary=initialMobs.getFirst();assertEquals(games.dmg.fantasystick.progression.ManagedEncounterOrigin.Kind.DUNGEON,dungeonController.encounterOrigin(ordinary).orElseThrow().kind());var copiedSlot=mock(Husk.class);when(copiedSlot.getUniqueId()).thenReturn(UUID.randomUUID());var copiedProof=ordinary.getPersistentDataContainer();when(copiedSlot.getPersistentDataContainer()).thenReturn(copiedProof);assertTrue(dungeonController.encounterOrigin(copiedSlot).isEmpty(),"a copied valid dungeon slot tag is not an authenticated registered spawn");var nativeDrop=new ItemStack(Material.ROTTEN_FLESH);var forgedDeath=new EntityDeathEvent(copiedSlot,mock(org.bukkit.damage.DamageSource.class),new ArrayList<>(List.of(nativeDrop)),7);f.dispatcher.accept(forgedDeath);assertEquals(7,forgedDeath.getDroppedExp());assertEquals(List.of(nativeDrop),forgedDeath.getDrops());assertTrue(dungeonController.encounterOrigin(ordinary).isPresent(),"copied callback did not consume the real dungeon slot");var survivor=initialMobs.getLast();assertNotEquals(dungeonController.encounterOrigin(ordinary),dungeonController.encounterOrigin(survivor),"each slot has independent kill authority");var survivorOrigin=dungeonController.encounterOrigin(survivor);var source=mock(org.bukkit.damage.DamageSource.class);when(source.getCausingEntity()).thenReturn(actor.player);when(source.getDirectEntity()).thenReturn(actor.player);var partySource=mock(org.bukkit.damage.DamageSource.class);when(partySource.getCausingEntity()).thenReturn(partyA.player);when(partySource.getDirectEntity()).thenReturn(partyA.player);f.dispatcher.accept(new EntityDamageByEntityEvent(partyA.player,ordinary,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,partySource,1));f.dispatcher.accept(new EntityDamageByEntityEvent(actor.player,ordinary,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,1));var drops=new ArrayList<ItemStack>();drops.add(new ItemStack(Material.ROTTEN_FLESH));var ordinaryDeath=new EntityDeathEvent(ordinary,source,drops,7);f.dispatcher.accept(ordinaryDeath);await(f,()->dungeons.runView(dungeon.id(),adventures.snapshot().catalog().revision()).map(run->run.defeatedSlots().size()==1).orElse(false),actor.messages);assertEquals(7,ordinaryDeath.getDroppedExp());assertFalse(ordinaryDeath.getDrops().isEmpty());var partial=dungeons.runView(dungeon.id(),adventures.snapshot().catalog().revision()).orElseThrow();assertEquals(1,partial.defeatedSlots().size());assertFalse(partial.clearedRooms().contains(first.id()));for(var member:List.of(actor,partyA))member.core.location.setX(100);pulse(f);verify(survivor,atLeastOnce()).remove();assertFalse(actorEffects.containsKey(org.bukkit.potion.PotionEffectType.SPEED));for(var member:List.of(actor,partyA)){member.core.location.setX(first.bounds().minX()+.5);member.core.location.setY(first.bounds().minY()+1);member.core.location.setZ(first.bounds().minZ()+.5);}pulse(f);await(f,()->spawned.stream().filter(Husk.class::isInstance).count()>initialMobs.size(),actor.messages);var respawned=spawned.stream().filter(Husk.class::isInstance).toList().getLast();assertNotSame(survivor,respawned);assertEquals(survivorOrigin,dungeonController.encounterOrigin(respawned),"respawned slot retains duplicate-safe kill authority");f.dispatcher.accept(new EntityDamageByEntityEvent(partyA.player,respawned,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,partySource,1));f.dispatcher.accept(new EntityDamageByEntityEvent(actor.player,respawned,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,1));f.dispatcher.accept(new EntityDeathEvent(respawned,source,new ArrayList<>(),0));await(f,()->dungeons.runView(dungeon.id(),adventures.snapshot().catalog().revision()).orElseThrow().clearedRooms().contains(first.id()),actor.messages);assertTrue(storage.find(partyB.core.record.id()).get().orElseThrow().experience().compareTo(partyBBefore.experience())>0,"ordinary party combat sharing remains active");
|
||||
var actorAfterKills=storage.find(actor.core.record.id()).get().orElseThrow();var partyAAfterKills=storage.find(partyA.core.record.id()).get().orElseThrow();var partyBAfterKills=storage.find(partyB.core.record.id()).get().orElseThrow();for(var member:List.of(actor,partyA,partyB)){member.core.location.setX(bossRoom.bounds().minX()+.5);member.core.location.setY(bossRoom.bounds().minY()+1);member.core.location.setZ(bossRoom.bounds().minZ()+.5);}pulse(f);await(f,()->spawned.stream().anyMatch(Vindicator.class::isInstance),actor.messages);var firstBoss=spawned.stream().filter(Vindicator.class::isInstance).findFirst().orElseThrow();for(var member:List.of(actor,partyA,partyB))member.core.location.setX(100);pulse(f);verify(firstBoss,atLeastOnce()).remove();for(var member:List.of(actor,partyA,partyB)){member.core.location.setX(bossRoom.bounds().minX()+.5);member.core.location.setY(bossRoom.bounds().minY()+1);member.core.location.setZ(bossRoom.bounds().minZ()+.5);}pulse(f);await(f,()->spawned.stream().filter(Vindicator.class::isInstance).count()>1,actor.messages);var boss=spawned.stream().filter(Vindicator.class::isInstance).toList().getLast();assertNotSame(firstBoss,boss);f.dispatcher.accept(new EntityDamageByEntityEvent(actor.player,boss,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,1));f.dispatcher.accept(new EntityDamageByEntityEvent(partyA.player,boss,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,partySource,1));f.dispatcher.accept(new EntityDeathEvent(boss,source,new ArrayList<>(),0));assertEquals(games.dmg.fantasystick.progression.ManagedEncounterOrigin.Kind.DUNGEON,dungeonController.encounterOrigin(boss).orElseThrow().kind(),"boss cleanup retains immutable managed origin authority");
|
||||
await(f,()->dungeons.runView(dungeon.id(),adventures.snapshot().catalog().revision()).map(DungeonState.Run::defeated).orElse(false)&&adventures.active().isEmpty()&&storage.find(actor.core.record.id()).join().orElseThrow().experience().compareTo(actorAfterKills.experience())>0,actor.messages);var run=dungeons.runView(dungeon.id(),adventures.snapshot().catalog().revision()).orElseThrow();assertTrue(run.defeated(),"dungeon run not defeated");assertEquals(boss.getUniqueId().toString(),run.completionAudit().get("boss_id"),"global completion must retain immutable native boss proof for recovery");assertFalse(run.clearedRooms().contains(skipped.id()),"boss death completes without clearing every room");assertTrue(adventures.active().isEmpty(),"boss did not close all dungeon quests");assertTrue(adventures.characterClaimed(catalog.revision(),dungeon.id(),actor.core.record.id()),"solo completion claim missing");assertTrue(adventures.characterClaimed(catalog.revision(),dungeon.id(),partyA.core.record.id()),"represented party member claim missing");assertTrue(adventures.characterClaimed(catalog.revision(),dungeon.id(),partyB.core.record.id()),"present snapshotted party member claim missing");var after=storage.find(actor.core.record.id()).get().orElseThrow();assertTrue(after.experience().compareTo(before.experience())>0,"ordinary dungeon kills award standard combat XP");assertTrue(after.coins().compareTo(before.coins())>0,"ordinary dungeon kills award standard character coins");assertTrue(after.experience().compareTo(actorAfterKills.experience())>0,"genuinely killed dungeon boss grants native-classified per-kill XP");assertTrue(after.coins().compareTo(actorAfterKills.coins())>0,"genuinely killed dungeon boss grants native-classified per-kill coins");assertEquals(AdventureQuestDirectory.Reward.empty(),adventures.unclaimed(catalog.revision(),dungeon.id(),actor.core.record.id()));var partyAAfter=storage.find(partyA.core.record.id()).get().orElseThrow();var partyBAfter=storage.find(partyB.core.record.id()).get().orElseThrow();assertTrue(partyAAfter.experience().compareTo(partyABefore.experience())>0,"party contributor receives normal kill XP");assertTrue(partyBAfter.experience().compareTo(partyBBefore.experience())>0);assertTrue(partyAAfter.experience().compareTo(partyAAfterKills.experience())>0);assertTrue(partyBAfter.experience().compareTo(partyBAfterKills.experience())>0);var trophies=(games.dmg.fantasystick.adventure.FishingDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"fishing");var soloTrophy=trophies.trophies(actor.core.record.id()).getFirst();var partyTrophy=trophies.trophies(partyA.core.record.id()).getFirst();assertEquals(games.dmg.fantasystick.adventure.FishingState.TrophyType.DUNGEON,soloTrophy.type());assertEquals("Trophy of Sunken Keep",soloTrophy.name());assertEquals(new games.dmg.fantasystick.adventure.FishingState.Reward(BigInteger.valueOf(30),BigInteger.valueOf(5)),soloTrophy.total());assertEquals(new games.dmg.fantasystick.adventure.FishingState.Reward(BigInteger.valueOf(30),BigInteger.valueOf(5)),partyTrophy.total());assertEquals(Set.of(partyA.core.record.id(),partyB.core.record.id()),partyTrophy.rewards().keySet());assertEquals(AdventureQuestDirectory.Reward.empty(),adventures.unclaimed(catalog.revision(),dungeon.id(),partyA.core.record.id()));var defeatedGeneration=adventures.snapshot().catalog().revision();command(f,actor,"dungeon","close",dungeon.id().toString(),"confirm");command(f,actor,"dungeon","open",dungeon.id().toString());assertTrue(actor.messages.getLast().contains("open refused"),actor.messages.toString());assertTrue(dungeons.runView(dungeon.id(),defeatedGeneration).orElseThrow().defeated(),"defeated generation changed");command(f,actor,"adventure","reroll","confirm");await(f,()->adventures.snapshot().reset()==null&&!adventures.snapshot().catalog().revision().equals(defeatedGeneration),actor.messages);assertEquals(1,trophies.trophies(actor.core.record.id()).size(),"reroll preserves earned dungeon trophy");var claimBase=storage.find(actor.core.record.id()).get().orElseThrow();var turn=(games.dmg.fantasystick.adventure.FishingController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"fishingController");var npcChunk=mock(Chunk.class);when(npcChunk.getWorld()).thenReturn(actor.world);when(npcChunk.getX()).thenReturn(actor.core.location.getBlockX()>>4);when(npcChunk.getZ()).thenReturn(actor.core.location.getBlockZ()>>4);when(npcChunk.getEntities()).thenReturn(new Entity[0]);when(actor.world.getChunkAt(anyInt(),anyInt())).thenReturn(npcChunk);when(actor.world.getLoadedChunks()).thenReturn(new Chunk[]{npcChunk});command(f,actor,"quest-giver","place","Dungeon Steward");var giverLocation=adventures.snapshot().givers().getFirst();actor.core.location.setX(giverLocation.x());actor.core.location.setZ(giverLocation.z());questController.reconcile(npcChunk);var giver=spawned.stream().filter(Villager.class::isInstance).map(Villager.class::cast).findFirst().orElseThrow(Line truncated
|
||||
await(f,()->exportedDungeonJourneyReady(data),actor.messages);
|
||||
NativeTestAwait.auditUntil(f, () -> exportedDungeonJourneyReady(data), Boolean.TRUE::equals, "complete dungeon journey exports");
|
||||
}
|
||||
var exported=exportedDungeonEvents(data);
|
||||
var changes=exported.stream().filter(event->event.get("type").getAsString()
|
||||
@@ -158,7 +158,7 @@ class DungeonNativeTest {
|
||||
.endsWith("adventure.reward_claimed")&&value.getAsJsonObject("data")
|
||||
.getAsJsonArray("sources").asList().stream().anyMatch(source->source.getAsJsonObject()
|
||||
.get("source_kind").getAsString().equals("dungeon_trophy")));
|
||||
}catch(Exception transientExport){return false;}
|
||||
}catch(Exception failure){throw new AssertionError(failure);}
|
||||
}
|
||||
private static List<com.google.gson.JsonObject> exportedDungeonEvents(Path root)throws Exception{
|
||||
try(var files=java.nio.file.Files.walk(root.resolve("events"))){
|
||||
@@ -288,7 +288,7 @@ class DungeonNativeTest {
|
||||
f.players.remove(leader.core.id);
|
||||
assertNull(f.plugin.getServer().getPlayer(leader.core.id),
|
||||
"the trophy carrier is offline when the boss dies");
|
||||
pulse(f);drain(f);pulse(f);drain(f);
|
||||
pulse(f);await(f,()->dungeons.runView(dungeon.id(),catalog.revision()).isPresent(),actor.messages);pulse(f);
|
||||
var boss=spawned.getLast();defeatedBoss[0]=boss.getUniqueId();
|
||||
var source=mock(org.bukkit.damage.DamageSource.class);
|
||||
when(source.getCausingEntity()).thenReturn(actor.player);
|
||||
@@ -309,7 +309,7 @@ class DungeonNativeTest {
|
||||
.map(DungeonState.Run::defeated).orElse(false),actor.messages);
|
||||
assertEquals(defeatedBoss[0].toString(),dungeons.runView(dungeon.id(),catalog.revision())
|
||||
.orElseThrow().completionAudit().get("boss_id"));
|
||||
assertEquals(0,exportedDungeonEvents(root).stream().filter(event->event.get("type")
|
||||
assertEquals(0,NativeTestAwait.readAudit(f, () -> exportedDungeonEvents(root)).stream().filter(event->event.get("type")
|
||||
.getAsString().endsWith("dungeon.completed")).count());
|
||||
}
|
||||
assertTrue(new games.dmg.fantasystick.persistence.YamlDungeonRepository(root).load().runs()
|
||||
@@ -317,9 +317,9 @@ class DungeonNativeTest {
|
||||
.equals(run.completionAudit().get("boss_id"))));
|
||||
try(var restarted=new FantasyStickLifecycleNativeTest.Fixture(root)){
|
||||
restarted.plugin.setEnabled(true);
|
||||
await(restarted,()->{try{return exportedDungeonEvents(root).stream().anyMatch(event->
|
||||
event.get("type").getAsString().endsWith("dungeon.completed"));}
|
||||
catch(Exception transientExport){return false;}},List.of());
|
||||
NativeTestAwait.auditUntil(restarted, () -> exportedDungeonEvents(root),
|
||||
records -> records.stream().anyMatch(event -> event.get("type").getAsString()
|
||||
.endsWith("dungeon.completed")), "replayed dungeon completion");
|
||||
var runs=(DungeonDirectory)FantasyStickLifecycleNativeTest.Fixture.field(restarted.plugin,"dungeons");
|
||||
await(restarted,()->runs.snapshot().runs().stream().anyMatch(run->run.defeated()
|
||||
&&defeatedBoss[0].toString().equals(run.completionAudit().get("boss_id"))
|
||||
@@ -394,8 +394,8 @@ class DungeonNativeTest {
|
||||
doAnswer(call->{var mob=mock(Vindicator.class);var pdc=new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry());var maximum=mock(org.bukkit.attribute.AttributeInstance.class);var damage=mock(org.bukkit.attribute.AttributeInstance.class);var equipment=mock(org.bukkit.inventory.EntityEquipment.class);var maximumBase=new double[]{20};var attackBase=new double[]{1};var health=new double[]{20};var effects=new LinkedHashSet<org.bukkit.potion.PotionEffect>();effects.add(new org.bukkit.potion.PotionEffect(org.bukkit.potion.PotionEffectType.WEAKNESS,200,0));maximumBases.put(mob,maximumBase);attackBases.put(mob,attackBase);bossEffects.put(mob,effects);bossEquipment.put(mob,equipment);when(mob.getPersistentDataContainer()).thenReturn(pdc);when(mob.getUniqueId()).thenReturn(UUID.randomUUID());when(mob.getType()).thenReturn(EntityType.VINDICATOR);when(mob.getWorld()).thenReturn(admin.world);when(mob.getLocation()).thenReturn(((Location)call.getArgument(0)).clone());when(mob.getAttribute(org.bukkit.attribute.Attribute.MAX_HEALTH)).thenReturn(maximum);when(mob.getAttribute(org.bukkit.attribute.Attribute.ATTACK_DAMAGE)).thenReturn(damage);when(maximum.getValue()).thenAnswer(ignored->maximumBase[0]);doAnswer(invocation->{maximumBase[0]=(Double)invocation.getArgument(0);return null;}).when(maximum).setBaseValue(anyDouble());doAnswer(invocation->{attackBase[0]=(Double)invocation.getArgument(0);return null;}).when(damage).setBaseValue(anyDouble());when(mob.getHealth()).thenAnswer(ignored->health[0]);doAnswer(invocation->{health[0]=(Double)invocation.getArgument(0);return null;}).when(mob).setHealth(anyDouble());when(mob.getEquipment()).thenReturn(equipment);when(mob.hasLineOfSight(any(Entity.class))).thenReturn(true);when(mob.getActivePotionEffects()).thenAnswer(ignored->Set.copyOf(effects));doAnswer(invocation->{effects.add(invocation.getArgument(0));return true;}).when(mob).addPotionEffect(any(org.bukkit.potion.PotionEffect.class));doAnswer(invocation->{effects.add(invocation.getArgument(0));return true;}).when(mob).addPotionEffect(any(org.bukkit.potion.PotionEffect.class),anyBoolean());doAnswer(invocation->{effects.removeIf(effect->effect.getType().equals(invocation.getArgument(0)));return null;}).when(mob).removePotionEffect(any(org.bukkit.potion.PotionEffectType.class));((java.util.function.Consumer<Vindicator>)call.getArgument(2)).accept(mob);spawned.add(mob);return mob;}).when(admin.world).spawn(any(Location.class),eq(Vindicator.class),any(java.util.function.Consumer.class));
|
||||
when(admin.world.getEntities()).thenAnswer(ignored->new ArrayList<Entity>(spawned));
|
||||
var dungeons=(DungeonDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"dungeons");var dungeon=dungeons.create("Mirror",worldId,0,64,0,0,0).get(5,TimeUnit.SECONDS);var room=dungeons.addRadiusRoom(dungeon.id(),"Boss",0,64,0,3,3).get(5,TimeUnit.SECONDS);dungeons.setBoss(dungeon.id(),room.id(),EntityType.HUSK,"Temporary",0,65,0,0,0).get(5,TimeUnit.SECONDS);dungeons.setCompletionReward(dungeon.id(),BigInteger.ZERO,BigInteger.ZERO).get(5,TimeUnit.SECONDS);command(f,admin,"dungeon","edit","start",dungeon.id().toString());var controller=(games.dmg.fantasystick.dungeon.DungeonController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"dungeonController");await(f,()->controller.editing(admin.core.id).isPresent(),admin.messages);admin.core.location.setX(.5);admin.core.location.setY(65);admin.core.location.setZ(.5);var sheet=admin.core.inventory.getItemInOffHand();admin.core.inventory.setItemInOffHand(null);admin.core.inventory.setItemInMainHand(sheet);command(f,admin,"dungeon","room","boss","assign-sheet","2");await(f,()->dungeons.custody(admin.core.record.id()).isPresent(),admin.messages);command(f,admin,"dungeon","edit","done");command(f,admin,"dungeon","enable",dungeon.id().toString());command(f,admin,"dungeon","open",dungeon.id().toString());var adventures=(AdventureQuestDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuests");await(f,()->adventures.snapshot().catalog()!=null&&adventures.snapshot().catalog().sites().contains(dungeon.id()),admin.messages);
|
||||
var fighter=new FantasyStickLifecycleNativeTest.Actor(root);fighter.core.location.setWorld(admin.world);fighter.core.location.setX(.5);fighter.core.location.setY(65);fighter.core.location.setZ(.5);f.players.put(fighter.core.id,fighter.player);doReturn(false).when(fighter.player).isDead();doReturn(true).when(fighter.player).addPotionEffect(any(org.bukkit.potion.PotionEffect.class),anyBoolean());var abilityHits=new ArrayList<Double>();fighter.damage=(amount,source)->abilityHits.add(amount);f.activate(fighter);var catalog=adventures.snapshot().catalog();assertEquals(AdventureQuestDirectory.Acceptance.APPLIED,adventures.accept(catalog.revision(),dungeon.id(),AdventureQuestState.UnitType.SOLO,fighter.core.record.id(),Set.of(fighter.core.record.id())).get(5,TimeUnit.SECONDS).status());var challenger=new FantasyStickLifecycleNativeTest.Actor(root);challenger.core.location.setWorld(admin.world);challenger.core.location.setX(1.5);challenger.core.location.setY(65);challenger.core.location.setZ(.5);f.players.put(challenger.core.id,challenger.player);doReturn(false).when(challenger.player).isDead();doReturn(true).when(challenger.player).addPotionEffect(any(org.bukkit.potion.PotionEffect.class),anyBoolean());challenger.damage=(amount,sourceEntity)->{};f.activate(challenger);assertEquals(AdventureQuestDirectory.Acceptance.APPLIED,adventures.accept(catalog.revision(),dungeon.id(),AdventureQuestState.UnitType.SOLO,challenger.core.record.id(),Set.of(challenger.core.record.id())).get(5,TimeUnit.SECONDS).status());pulse(f);drain(f);pulse(f);drain(f);assertFalse(spawned.isEmpty());var boss=spawned.getLast();assertEquals(70d,maximumBases.get(boss)[0]);assertEquals(26d,attackBases.get(boss)[0]);var equipment=bossEquipment.get(boss);verify(equipment).setChestplate(argThat(item->item!=null&&item.getType()==Material.COPPER_CHESTPLATE));verify(equipment).setLeggings(argThat(item->item!=null&&item.getType()==Material.GOLDEN_LEGGINGS));verify(equipment).setBoots(argThat(item->item!=null&&item.getType()==Material.IRON_BOOTS));assertTrue(bossEffects.get(boss).stream().anyMatch(effect->effect.getType().equals(org.bukkit.potion.PotionEffectType.SPEED)),bossEffects.get(boss)::toString);assertTrue(bossEffects.get(boss).stream().anyMatch(effect->effect.getType().equals(org.bukkit.potion.PotionEffectType.NIGHT_VISION)),bossEffects.get(boss)::toString);boss.setHealth(50);var source=mock(org.bukkit.damage.DamageSource.class);when(source.getCausingEntity()).thenReturn(fighter.player);when(source.getDirectEntity()).thenReturn(fighter.player);f.dispatcher.accept(new EntityDamageByEntityEvent(fighter.player,boss,EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,3));pulse(f);verify(boss,atLeastOnce()).setTarget(fighter.player);assertTrue(abilityHits.size()>=2,"Every ready offensive slot casts in the same pulse");int firstCasts=abilityHits.size();pulse(f);assertEquals(firstCasts,abilityHits.size(),"Boss-only cooldowns suppress immediate recasts");assertFalse(bossEffects.get(boss).stream().anyMatch(effect->effect.getType().equals(org.bukkit.potion.PotionEffectType.WEAKNESS)),"Purify removes a real harmful effect");var challengerSource=mock(org.bukkit.damage.DamageSource.class);when(challengerSource.getCausingEntity()).thenReturn(challenger.player);when(challengerSource.getDirectEntity()).thenReturn(challenger.player);f.dispatcher.accept(new EntityDamageByEntityEvent(challenger.player,boss,EntityDamageEvent.DamageCause.ENTITY_ATTACK,challengerSource,10));pulse(f);var targets=org.mockito.ArgumentCaptor.forClass(LivingEntity.class);verify(boss,atLeastOnce()).setTarget(targets.capture());assertSame(fighter.player,targets.getAllValues().getLast(),"Higher later threat cannot break fixation");fighter.core.location.setX(100);pulse(f);targets=org.mockito.ArgumentCaptor.forClass(LivingEntity.class);verify(boss,atLeastOnce()).setTarget(targets.capture());assertSame(challenger.player,targets.getAllValues().getLast(),"Ineligible fixation retargets highest remaining threat");
|
||||
challenger.core.location.setX(100);pulse(f);verify(boss,atLeastOnce()).remove();challenger.core.location.setX(.5);pulse(f);drain(f);var freshBoss=spawned.getLast();killedBossId[0]=freshBoss.getUniqueId();assertNotSame(boss,freshBoss);assertEquals(70d,maximumBases.get(freshBoss)[0]);f.dispatcher.accept(new EntityDamageByEntityEvent(challenger.player,freshBoss,EntityDamageEvent.DamageCause.ENTITY_ATTACK,challengerSource,2));f.dispatcher.accept(new EntityDeathEvent(freshBoss,challengerSource,new ArrayList<>(),0));await(f,()->dungeons.runView(dungeon.id(),catalog.revision()).map(DungeonState.Run::defeated).orElse(false),admin.messages);assertTrue(dungeons.custody(admin.core.record.id()).isPresent(),"Boss defeat keeps character custody for later days");await(f,()->{try{var published=exportedDungeonEvents(root);return published.stream().anyMatch(event->event.get("type").getAsString().endsWith("dungeon.completed"))&&published.stream().anyMatch(event->event.get("type").getAsString().endsWith("adventure.trophy_earned")&&event.getAsJsonObject("data").get("trophy_type").getAsString().equals("dungeon_trophy"));}catch(Exception transientExport){return false;}},admin.messages);challenger.core.location.setX(100);
|
||||
var fighter=new FantasyStickLifecycleNativeTest.Actor(root);fighter.core.location.setWorld(admin.world);fighter.core.location.setX(.5);fighter.core.location.setY(65);fighter.core.location.setZ(.5);f.players.put(fighter.core.id,fighter.player);doReturn(false).when(fighter.player).isDead();doReturn(true).when(fighter.player).addPotionEffect(any(org.bukkit.potion.PotionEffect.class),anyBoolean());var abilityHits=new ArrayList<Double>();fighter.damage=(amount,source)->abilityHits.add(amount);f.activate(fighter);var catalog=adventures.snapshot().catalog();assertEquals(AdventureQuestDirectory.Acceptance.APPLIED,adventures.accept(catalog.revision(),dungeon.id(),AdventureQuestState.UnitType.SOLO,fighter.core.record.id(),Set.of(fighter.core.record.id())).get(5,TimeUnit.SECONDS).status());var challenger=new FantasyStickLifecycleNativeTest.Actor(root);challenger.core.location.setWorld(admin.world);challenger.core.location.setX(1.5);challenger.core.location.setY(65);challenger.core.location.setZ(.5);f.players.put(challenger.core.id,challenger.player);doReturn(false).when(challenger.player).isDead();doReturn(true).when(challenger.player).addPotionEffect(any(org.bukkit.potion.PotionEffect.class),anyBoolean());challenger.damage=(amount,sourceEntity)->{};f.activate(challenger);assertEquals(AdventureQuestDirectory.Acceptance.APPLIED,adventures.accept(catalog.revision(),dungeon.id(),AdventureQuestState.UnitType.SOLO,challenger.core.record.id(),Set.of(challenger.core.record.id())).get(5,TimeUnit.SECONDS).status());pulse(f);await(f,()->dungeons.runView(dungeon.id(),catalog.revision()).isPresent(),admin.messages);pulse(f);assertFalse(spawned.isEmpty());var boss=spawned.getLast();assertEquals(70d,maximumBases.get(boss)[0]);assertEquals(26d,attackBases.get(boss)[0]);var equipment=bossEquipment.get(boss);verify(equipment).setChestplate(argThat(item->item!=null&&item.getType()==Material.COPPER_CHESTPLATE));verify(equipment).setLeggings(argThat(item->item!=null&&item.getType()==Material.GOLDEN_LEGGINGS));verify(equipment).setBoots(argThat(item->item!=null&&item.getType()==Material.IRON_BOOTS));assertTrue(bossEffects.get(boss).stream().anyMatch(effect->effect.getType().equals(org.bukkit.potion.PotionEffectType.SPEED)),bossEffects.get(boss)::toString);assertTrue(bossEffects.get(boss).stream().anyMatch(effect->effect.getType().equals(org.bukkit.potion.PotionEffectType.NIGHT_VISION)),bossEffects.get(boss)::toString);boss.setHealth(50);var source=mock(org.bukkit.damage.DamageSource.class);when(source.getCausingEntity()).thenReturn(fighter.player);when(source.getDirectEntity()).thenReturn(fighter.player);f.dispatcher.accept(new EntityDamageByEntityEvent(fighter.player,boss,EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,3));pulse(f);verify(boss,atLeastOnce()).setTarget(fighter.player);assertTrue(abilityHits.size()>=2,"Every ready offensive slot casts in the same pulse");int firstCasts=abilityHits.size();pulse(f);assertEquals(firstCasts,abilityHits.size(),"Boss-only cooldowns suppress immediate recasts");assertFalse(bossEffects.get(boss).stream().anyMatch(effect->effect.getType().equals(org.bukkit.potion.PotionEffectType.WEAKNESS)),"Purify removes a real harmful effect");var challengerSource=mock(org.bukkit.damage.DamageSource.class);when(challengerSource.getCausingEntity()).thenReturn(challenger.player);when(challengerSource.getDirectEntity()).thenReturn(challenger.player);f.dispatcher.accept(new EntityDamageByEntityEvent(challenger.player,boss,EntityDamageEvent.DamageCause.ENTITY_ATTACK,challengerSource,10));pulse(f);var targets=org.mockito.ArgumentCaptor.forClass(LivingEntity.class);verify(boss,atLeastOnce()).setTarget(targets.capture());assertSame(fighter.player,targets.getAllValues().getLast(),"Higher later threat cannot break fixation");fighter.core.location.setX(100);pulse(f);targets=org.mockito.ArgumentCaptor.forClass(LivingEntity.class);verify(boss,atLeastOnce()).setTarget(targets.capture());assertSame(challenger.player,targets.getAllValues().getLast(),"Ineligible fixation retargets highest remaining threat");
|
||||
challenger.core.location.setX(100);pulse(f);verify(boss,atLeastOnce()).remove();challenger.core.location.setX(.5);pulse(f);await(f,()->spawned.getLast()!=boss,admin.messages);var freshBoss=spawned.getLast();killedBossId[0]=freshBoss.getUniqueId();assertNotSame(boss,freshBoss);assertEquals(70d,maximumBases.get(freshBoss)[0]);f.dispatcher.accept(new EntityDamageByEntityEvent(challenger.player,freshBoss,EntityDamageEvent.DamageCause.ENTITY_ATTACK,challengerSource,2));f.dispatcher.accept(new EntityDeathEvent(freshBoss,challengerSource,new ArrayList<>(),0));await(f,()->dungeons.runView(dungeon.id(),catalog.revision()).map(DungeonState.Run::defeated).orElse(false),admin.messages);assertTrue(dungeons.custody(admin.core.record.id()).isPresent(),"Boss defeat keeps character custody for later days");await(f,()->{try{var published=exportedDungeonEvents(root);return published.stream().anyMatch(event->event.get("type").getAsString().endsWith("dungeon.completed"))&&published.stream().anyMatch(event->event.get("type").getAsString().endsWith("adventure.trophy_earned")&&event.getAsJsonObject("data").get("trophy_type").getAsString().equals("dungeon_trophy"));}catch(Exception transientExport){return false;}},admin.messages);challenger.core.location.setX(100);
|
||||
int disableFeedback = admin.messages.size();
|
||||
command(f,admin,"dungeon","disable",dungeon.id().toString(),"confirm");
|
||||
ReskillEventsNativeTest.await(f,()->admin.messages.stream().skip(disableFeedback)
|
||||
@@ -404,7 +404,7 @@ class DungeonNativeTest {
|
||||
assertFalse(adventures.offerOpen(dungeon.id()));
|
||||
assertTrue(adventures.findSite(dungeon.id()).isEmpty());
|
||||
admin.core.location.setX(100);command(f,admin,"dungeon","boss","recover-sheet",dungeon.id().toString());await(f,()->dungeons.custody(admin.core.record.id()).isEmpty(),admin.messages);var storage=(CharacterStorage)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characters");assertEquals(configuredBoss,storage.find(configuredBoss.id()).get(5,TimeUnit.SECONDS).orElseThrow());assertTrue(Arrays.stream(admin.core.inventory.getContents()).filter(Objects::nonNull).anyMatch(item->new games.dmg.fantasystick.items.CharacterSheets().identify(item).filter(configuredBoss.id()::equals).isPresent()));
|
||||
await(f,()->{try{var published=exportedDungeonEvents(root);return published.stream().anyMatch(event->event.get("type").getAsString().endsWith("dungeon.completed"))&&published.stream().anyMatch(event->event.get("type").getAsString().endsWith("adventure.trophy_earned")&&event.getAsJsonObject("data").get("trophy_type").getAsString().equals("dungeon_trophy"));}catch(Exception transientExport){return false;}},admin.messages);
|
||||
NativeTestAwait.auditUntil(f, () -> exportedDungeonEvents(root), published -> published.stream().anyMatch(event->event.get("type").getAsString().endsWith("dungeon.completed"))&&published.stream().anyMatch(event->event.get("type").getAsString().endsWith("adventure.trophy_earned")&&event.getAsJsonObject("data").get("trophy_type").getAsString().equals("dungeon_trophy")), "completion and trophy exports");
|
||||
}
|
||||
var exported=exportedDungeonEvents(root);
|
||||
var bossAwards=exported.stream().filter(event->event.get("type").getAsString()
|
||||
@@ -427,11 +427,21 @@ class DungeonNativeTest {
|
||||
}
|
||||
|
||||
@Test void questGiverPaginatesManualDungeonOffersBeyondAutomaticCount() throws Exception {
|
||||
java.nio.file.Files.writeString(data.resolve("config.yml"),"adventure-daily-site-count: 0\n");try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){f.plugin.setEnabled(true);var actor=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(actor.core.id,actor.player);UUID world=UUID.randomUUID();when(actor.world.getUID()).thenReturn(world);f.activate(actor);var directory=(AdventureQuestDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuests");var sites=new ArrayList<AdventureQuestState.Site>();for(int i=0;i<46;i++)sites.add(new AdventureQuestState.Site(UUID.randomUUID(),AdventureQuestState.SiteType.DUNGEON,"Dungeon "+i,world,i,64,0));var catalog=new AdventureQuestState.Catalog(java.time.LocalDate.now(java.time.ZoneOffset.UTC),UUID.randomUUID(),java.time.Instant.now(),sites.stream().map(AdventureQuestState.Site::id).toList());var field=directory.getClass().getDeclaredField("snapshot");field.setAccessible(true);field.set(directory,new AdventureQuestState(sites,List.of(),catalog,List.of(),Set.of(),Set.of(),Map.of()));var controller=(AdventureQuestController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuestController");controller.open(actor.player);drain(f);var first=actor.inventories.getLast();assertEquals(54,first.getSize());for(int slot=0;slot<45;slot++)assertEquals(Material.PAPER,first.getItem(slot).getType());assertEquals(Material.ARROW,first.getItem(53).getType());var click=mock(InventoryClickEvent.class);when(click.getWhoClicked()).thenReturn(actor.player);when(click.getInventory()).thenReturn(first);when(click.getCurrentItem()).thenReturn(first.getItem(53));controller.click(click);drain(f);var second=actor.inventories.getLast();assertNotSame(first,second);assertEquals(Material.PAPER,second.getItem(0).getType());assertTrue(second.getItem(0).getItemMeta().displayName().toString().contains("Dungeon 45"));}
|
||||
java.nio.file.Files.writeString(data.resolve("config.yml"),"adventure-daily-site-count: 0\n");try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){f.plugin.setEnabled(true);var actor=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(actor.core.id,actor.player);UUID world=UUID.randomUUID();when(actor.world.getUID()).thenReturn(world);f.activate(actor);var directory=(AdventureQuestDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuests");var sites=new ArrayList<AdventureQuestState.Site>();for(int i=0;i<46;i++)sites.add(new AdventureQuestState.Site(UUID.randomUUID(),AdventureQuestState.SiteType.DUNGEON,"Dungeon "+i,world,i,64,0));var catalog=new AdventureQuestState.Catalog(java.time.LocalDate.now(java.time.ZoneOffset.UTC),UUID.randomUUID(),java.time.Instant.now(),sites.stream().map(AdventureQuestState.Site::id).toList());var field=directory.getClass().getDeclaredField("snapshot");field.setAccessible(true);field.set(directory,new AdventureQuestState(sites,List.of(),catalog,List.of(),Set.of(),Set.of(),Map.of()));var controller=(AdventureQuestController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuestController");var first=open(f,controller,actor);assertEquals(54,first.getSize());for(int slot=0;slot<45;slot++)assertEquals(Material.PAPER,first.getItem(slot).getType());assertEquals(Material.ARROW,first.getItem(53).getType());var click=mock(InventoryClickEvent.class);when(click.getWhoClicked()).thenReturn(actor.player);when(click.getInventory()).thenReturn(first);when(click.getCurrentItem()).thenReturn(first.getItem(53));int beforePage=actor.inventories.size();controller.click(click);await(f,()->actor.inventories.size()>beforePage,actor.messages);var second=actor.inventories.getLast();assertNotSame(first,second);assertEquals(Material.PAPER,second.getItem(0).getType());assertTrue(second.getItem(0).getItemMeta().displayName().toString().contains("Dungeon 45"));}
|
||||
}
|
||||
|
||||
private static void command(FantasyStickLifecycleNativeTest.Fixture f,FantasyStickLifecycleNativeTest.Actor actor,String... args)throws Exception{f.command.execute(actor.player,"fantasystick",args);drain(f);}
|
||||
private static void command(FantasyStickLifecycleNativeTest.Fixture f,FantasyStickLifecycleNativeTest.Actor actor,String... args)throws Exception{
|
||||
// These serialized authoring routes respond only at their terminal native outcome.
|
||||
int before=actor.messages.size();
|
||||
f.command.execute(actor.player,"fantasystick",args);
|
||||
NativeTestAwait.until(f,()->actor.messages.size()>before,"native response to "+String.join(" ",args));
|
||||
}
|
||||
private static org.bukkit.inventory.Inventory open(FantasyStickLifecycleNativeTest.Fixture f,
|
||||
AdventureQuestController controller,FantasyStickLifecycleNativeTest.Actor actor)throws Exception{
|
||||
int before=actor.inventories.size();controller.open(actor.player);
|
||||
NativeTestAwait.until(f,()->actor.inventories.size()>before,"new Quest Giver menu");
|
||||
return actor.inventories.getLast();
|
||||
}
|
||||
private static void pulse(FantasyStickLifecycleNativeTest.Fixture f){for(var task:List.copyOf(f.pulses))task.run();}
|
||||
private static void drain(FantasyStickLifecycleNativeTest.Fixture f)throws Exception{Runnable task;for(int attempt=0;attempt<20;attempt++){task=f.tasks.poll(50,TimeUnit.MILLISECONDS);if(task==null)break;task.run();}}
|
||||
private static void await(FantasyStickLifecycleNativeTest.Fixture f,java.util.function.BooleanSupplier condition,Object evidence)throws Exception{for(int attempt=0;attempt<50&&!condition.getAsBoolean();attempt++){drain(f);Thread.sleep(5);}assertTrue(condition.getAsBoolean(),String.valueOf(evidence));}
|
||||
private static void await(FantasyStickLifecycleNativeTest.Fixture f,java.util.function.BooleanSupplier condition,Object evidence)throws Exception{NativeTestAwait.until(f,condition,"dungeon result: "+evidence);}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ class DungeonRoomEffectLifetimeNativeTest {
|
||||
fixture.actor.core.location.setX(70); fixture.shared.pulse();
|
||||
if (port.cancelRemoval) {
|
||||
assertEquals(port.aged(beforeExit, 1), port.current(PotionEffectType.SPEED));
|
||||
assertEquals(0, port.removals); assertTrue(rows("effect.ended").isEmpty());
|
||||
assertEquals(0, port.removals); assertTrue(NativeTestAwait.readAudit(fixture.f, () -> rows("effect.ended")).isEmpty());
|
||||
port.cancelRemoval = false; fixture.shared.pulse();
|
||||
}
|
||||
if (port.cancelExtend) {
|
||||
@@ -120,15 +120,15 @@ class DungeonRoomEffectLifetimeNativeTest {
|
||||
assertEquals(fixture.quest, fixture.shared.directory.findQuest(fixture.quest.id()).orElseThrow());
|
||||
// Observe gameplay exports while the logger is open; shutdown may retain valid durable intents.
|
||||
try {
|
||||
// These owner intents were submitted by the already verified native calls.
|
||||
// A ready main-thread callback must not consume this IO-only 100 x 50 ms budget.
|
||||
for (int attempt = 0; attempt < 100
|
||||
&& (rows("effect.applied").size() != 1 || rows("effect.ended").size() != 1); attempt++)
|
||||
Thread.sleep(50);
|
||||
assertEquals(1, rows("effect.applied").size());
|
||||
assertEquals(1, rows("effect.ended").size());
|
||||
var exported = NativeTestAwait.auditUntil(fixture.f,
|
||||
() -> List.of(rows("effect.applied"), rows("effect.ended")),
|
||||
rows -> rows.get(0).size() == 1 && rows.get(1).size() == 1,
|
||||
"one applied and one ended room effect");
|
||||
assertEquals(1, exported.get(0).size());
|
||||
assertEquals(1, exported.get(1).size());
|
||||
} catch (AssertionError failure) {
|
||||
var applied = rows("effect.applied"); var ended = rows("effect.ended");
|
||||
var applied = NativeTestAwait.readAudit(fixture.f, () -> rows("effect.applied"));
|
||||
var ended = NativeTestAwait.readAudit(fixture.f, () -> rows("effect.ended"));
|
||||
throw new AssertionError("Room export fence: applied=" + applied.size()
|
||||
+ ", appliedLogical=" + applied.stream().map(row -> row.get("id").getAsString()).distinct().count()
|
||||
+ ", ended=" + ended.size()
|
||||
|
||||
@@ -22,6 +22,17 @@ class FeatureAuditSaturationNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
||||
|
||||
private static java.util.List<com.google.gson.JsonObject> rows(FantasyStickLifecycleNativeTest.Fixture f,
|
||||
Path root, String type) throws Exception {
|
||||
return NativeTestAwait.readAudit(f, () -> QuestTrackingEventsNativeTest.rows(root, type));
|
||||
}
|
||||
|
||||
private static void awaitRows(FantasyStickLifecycleNativeTest.Fixture f, Path root,
|
||||
String type, int count) throws Exception {
|
||||
NativeTestAwait.auditUntil(f, () -> QuestTrackingEventsNativeTest.rows(root, type),
|
||||
records -> records.size() >= count, type + " export");
|
||||
}
|
||||
|
||||
@Test void actualPluginStartupAutomaticallyExportsOriginalIntentAfterOtherOwnerCapacityReturns() throws Exception {
|
||||
var pluginData = data.resolve("startup-plugin");
|
||||
var saved = new CopyOnWriteArrayList<games.dmg.fantasystick.events.EventInput>();
|
||||
@@ -47,27 +58,26 @@ class FeatureAuditSaturationNativeTest {
|
||||
assertTrue(full.await(30, TimeUnit.SECONDS));
|
||||
try (var restarted = new FantasyStickLifecycleNativeTest.Fixture(pluginData)) {
|
||||
restarted.plugin.setEnabled(true); // Actual production startup invokes replay, not this test.
|
||||
ReskillEventsNativeTest.await(restarted, () -> {
|
||||
try { return ReskillEventsNativeTest.records(pluginData).stream()
|
||||
.filter(row -> row.get("type").getAsString().endsWith("game.started")).count() == 2; }
|
||||
catch (Exception failure) { throw new AssertionError(failure); }
|
||||
});
|
||||
assertTrue(rows(pluginData, "action.observed").stream().noneMatch(row -> row.get("id").getAsString().equals(proof.id().toString())));
|
||||
NativeTestAwait.auditUntil(restarted, () -> ReskillEventsNativeTest.records(pluginData),
|
||||
records -> records.stream().filter(row -> row.get("type").getAsString()
|
||||
.endsWith("game.started")).count() == 2, "second startup export");
|
||||
assertTrue(rows(restarted, pluginData, "action.observed").stream().noneMatch(row -> row.get("id").getAsString().equals(proof.id().toString())));
|
||||
assertTrue(Files.exists(pluginData.resolve("quest-tracking-event-intents").resolve(proof.id() + ".pending")));
|
||||
held.complete(LocalEventLog.Delivery.REJECTED);
|
||||
ReskillEventsNativeTest.await(restarted, () -> {
|
||||
try { return rows(pluginData, "action.observed").stream()
|
||||
.anyMatch(row -> row.get("id").getAsString().equals(proof.id().toString())); }
|
||||
catch (Exception failure) { throw new AssertionError(failure); }
|
||||
});
|
||||
var exported = rows(pluginData, "action.observed").stream()
|
||||
var replayed = NativeTestAwait.auditUntil(restarted,
|
||||
() -> QuestTrackingEventsNativeTest.rows(pluginData, "action.observed"),
|
||||
records -> records.stream().anyMatch(row -> row.get("id").getAsString().equals(proof.id().toString())),
|
||||
"original replay export");
|
||||
var exported = replayed.stream()
|
||||
.filter(row -> row.get("id").getAsString().equals(proof.id().toString())).toList();
|
||||
assertEquals(1, exported.size()); var row = exported.getFirst();
|
||||
assertEquals(proof.time().toString(), row.get("time").getAsString());
|
||||
assertEquals(proof.context().playerId().toString(), row.get("playerid").getAsString());
|
||||
for (var entry : proof.data().entrySet()) assertEquals(new com.google.gson.Gson().toJsonTree(entry.getValue()),
|
||||
row.getAsJsonObject("data").get(entry.getKey()), entry.getKey());
|
||||
assertFalse(Files.exists(pluginData.resolve("quest-tracking-event-intents").resolve(proof.id() + ".pending")));
|
||||
var pending = pluginData.resolve("quest-tracking-event-intents").resolve(proof.id() + ".pending");
|
||||
NativeTestAwait.until(restarted, () -> !Files.exists(pending), "export acknowledgment retired original intent");
|
||||
assertFalse(Files.exists(pending));
|
||||
}
|
||||
} finally { held.complete(LocalEventLog.Delivery.REJECTED); blocker.close(); blocker.drained().toCompletableFuture().get(10, TimeUnit.SECONDS); }
|
||||
}
|
||||
@@ -118,7 +128,7 @@ class FeatureAuditSaturationNativeTest {
|
||||
ReskillEventsNativeTest.await(f, () -> actor.messages.contains("Quest tracking is now on."));
|
||||
assertTrue(fixture.directory.trackingEnabled(quest.id(), actor.core.record.id()));
|
||||
assertEquals(Material.COMPASS, actor.core.inventory.getItem(0).getType());
|
||||
assertTrue(rows(data.resolve("logger-plugin"), "preference.read").isEmpty());
|
||||
assertTrue(rows(f, data.resolve("logger-plugin"), "preference.read").isEmpty());
|
||||
} finally {
|
||||
releaseDiagnostic.countDown(); f.plugin.getLogger().removeHandler(handler);
|
||||
held.complete(LocalEventLog.Delivery.REJECTED); blocker.close();
|
||||
@@ -165,9 +175,9 @@ class FeatureAuditSaturationNativeTest {
|
||||
assertArrayEquals(source, Files.readAllBytes(data.resolve("plugin/adventure-quests.yml")));
|
||||
var after = actor.core.snapshot();
|
||||
for (int i = 1; i < inventory.length; i++) assertEquals(inventory[i], after[i]);
|
||||
assertEquals(1, rows(data.resolve("plugin"), "preference.changed").size());
|
||||
assertTrue(rows(data.resolve("plugin"), "preference.read").isEmpty());
|
||||
assertTrue(rows(data.resolve("plugin"), "action.observed").isEmpty());
|
||||
assertEquals(1, rows(f, data.resolve("plugin"), "preference.changed").size());
|
||||
assertTrue(rows(f, data.resolve("plugin"), "preference.read").isEmpty());
|
||||
assertTrue(rows(f, data.resolve("plugin"), "action.observed").isEmpty());
|
||||
} finally {
|
||||
held.complete(LocalEventLog.Delivery.REJECTED);
|
||||
blocker.close();
|
||||
@@ -177,7 +187,7 @@ class FeatureAuditSaturationNativeTest {
|
||||
// Once capacity recovers, the actual registered query reaches parsed JSONL again.
|
||||
f.playerCommand.execute(actor.player, "fstick", new String[]{"adventure", "tracking"});
|
||||
awaitRows(f, data.resolve("plugin"), "preference.read", 1);
|
||||
assertTrue(rows(data.resolve("plugin"), "preference.read").getFirst().getAsJsonObject("data").get("saved_value").getAsBoolean());
|
||||
assertTrue(rows(f, data.resolve("plugin"), "preference.read").getFirst().getAsJsonObject("data").get("saved_value").getAsBoolean());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/** Test-only acknowledgments; gameplay time and production executors remain unchanged. */
|
||||
final class NativeTestAwait {
|
||||
private NativeTestAwait() { }
|
||||
|
||||
static void until(FantasyStickLifecycleNativeTest.Fixture fixture, BooleanSupplier completed,
|
||||
String description) throws Exception {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||
while (!completed.getAsBoolean()) pump(fixture, deadline, description);
|
||||
}
|
||||
|
||||
/** Returns the same strict snapshot that satisfied the predicate, never a later unfenced reread. */
|
||||
static <T> T auditUntil(FantasyStickLifecycleNativeTest.Fixture fixture, Callable<T> read,
|
||||
Predicate<T> completed, String description) throws Exception {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||
while (true) {
|
||||
var snapshot = readAudit(fixture, read, remaining(deadline, description));
|
||||
if (completed.test(snapshot)) return snapshot;
|
||||
pump(fixture, deadline, description);
|
||||
}
|
||||
}
|
||||
|
||||
private static void pump(FantasyStickLifecycleNativeTest.Fixture fixture, long deadline,
|
||||
String description) throws Exception {
|
||||
var task = fixture.tasks.poll(Math.min(remaining(deadline, description),
|
||||
TimeUnit.MILLISECONDS.toNanos(10)), TimeUnit.NANOSECONDS);
|
||||
if (task != null) task.run();
|
||||
}
|
||||
|
||||
private static long remaining(long deadline, String description) {
|
||||
long remaining = deadline - System.nanoTime();
|
||||
assertTrue(remaining > 0, "Missing acknowledgment within five seconds: " + description);
|
||||
return remaining;
|
||||
}
|
||||
|
||||
static <T> T readAudit(FantasyStickLifecycleNativeTest.Fixture fixture, Callable<T> read) throws Exception {
|
||||
return readAudit(fixture, read, TimeUnit.SECONDS.toNanos(5));
|
||||
}
|
||||
|
||||
static <T> T readAudit(FantasyStickLifecycleNativeTest.Fixture fixture, Callable<T> read,
|
||||
long remainingNanos) throws Exception {
|
||||
// Run the entire strict read on this fixture's serial writer, not merely a fence before
|
||||
// an unsynchronized read. Later appends cannot interleave with parsing the snapshot.
|
||||
var writer = (ExecutorService) FantasyStickLifecycleNativeTest.Fixture.field(
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(fixture.plugin, "eventLog"), "worker");
|
||||
try { return writer.submit(read).get(remainingNanos, TimeUnit.NANOSECONDS); }
|
||||
catch (ExecutionException failure) { throw new AssertionError(failure.getCause()); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class NativeTestAwaitNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
||||
|
||||
@Test void quietQueueAndUnrelatedCallbackDoNotAcknowledgeTheRequestedResult() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
var result = new AtomicBoolean();
|
||||
var unrelated = new AtomicBoolean();
|
||||
var returned = new CountDownLatch(1);
|
||||
var premature = new AtomicBoolean();
|
||||
f.tasks.add(() -> unrelated.set(true));
|
||||
var delayed = CompletableFuture.runAsync(() -> {
|
||||
try { premature.set(returned.await(200, TimeUnit.MILLISECONDS)); }
|
||||
catch (InterruptedException failure) { Thread.currentThread().interrupt(); throw new AssertionError(failure); }
|
||||
finally { f.tasks.add(() -> result.set(true)); }
|
||||
});
|
||||
try {
|
||||
NativeTestAwait.until(f, result::get, "requested native result");
|
||||
returned.countDown(); delayed.get(5, TimeUnit.SECONDS);
|
||||
assertTrue(unrelated.get());
|
||||
assertTrue(result.get());
|
||||
assertFalse(premature.get(), "an empty queue is not completion of delayed work");
|
||||
} finally {
|
||||
returned.countDown(); delayed.get(5, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test void acknowledgedResultReturnsWithoutConsumingUnrelatedQueuedWork() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
var unrelated = new AtomicBoolean();
|
||||
f.tasks.add(() -> unrelated.set(true));
|
||||
NativeTestAwait.until(f, () -> true, "already acknowledged");
|
||||
assertFalse(unrelated.get(), "acknowledgment is not a request to drain arbitrary work");
|
||||
assertEquals(1, f.tasks.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ class RareShopNativeTest {
|
||||
f.plugin.setEnabled(true);var actor=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(actor.core.id,actor.player);doReturn(true).when(actor.player).hasPermission("fantasystick.admin");UUID worldId=UUID.randomUUID();when(actor.world.getUID()).thenReturn(worldId);int x=actor.core.location.getBlockX()>>4,z=actor.core.location.getBlockZ()>>4;var chunk=mock(Chunk.class);when(chunk.getWorld()).thenReturn(actor.world);when(chunk.getX()).thenReturn(x);when(chunk.getZ()).thenReturn(z);when(chunk.getEntities()).thenReturn(new org.bukkit.entity.Entity[0]);when(actor.world.getChunkAt(x,z)).thenReturn(chunk);when(actor.world.isChunkLoaded(x,z)).thenReturn(true);when(actor.world.getEntities()).thenReturn(List.of());
|
||||
assertEquals(List.of("list","place","remove","restock"),f.command.tabComplete(actor.player,"fantasystick",new String[]{"rare-shop",""}));assertTrue(f.command.tabComplete(actor.player,"fantasystick",new String[]{"config","rare-"}).containsAll(List.of("rare-price-health-potion","rare-price-reskill","rare-stock-reskill-min","rare-stock-reskill-max","rare-price-magic-core","rare-stock-magic-core-min","rare-stock-magic-core-max","rare-price-phoenix-down","rare-stock-phoenix-down-min","rare-stock-phoenix-down-max","rare-phoenix-down-invulnerability-seconds")));assertEquals(List.of("20","2000"),f.command.tabComplete(actor.player,"fantasystick",new String[]{"config","rare-price-reskill",""}));assertEquals(List.of("30","3000"),f.command.tabComplete(actor.player,"fantasystick",new String[]{"config","rare-price-magic-core",""}));
|
||||
var villager=mock(Villager.class);when(villager.getLocation()).thenReturn(actor.core.location);var values=new HashMap<String,String>();var pdc=mock(PersistentDataContainer.class);when(villager.getPersistentDataContainer()).thenReturn(pdc);doAnswer(call->{values.put(((NamespacedKey)call.getArgument(0)).getKey(),call.getArgument(2));return null;}).when(pdc).set(any(NamespacedKey.class),eq(PersistentDataType.STRING),anyString());when(pdc.get(any(NamespacedKey.class),eq(PersistentDataType.STRING))).thenAnswer(call->values.get(((NamespacedKey)call.getArgument(0)).getKey()));doAnswer(call->{((java.util.function.Consumer<Villager>)call.getArgument(2)).accept(villager);return villager;}).when(actor.world).spawn(any(Location.class),eq(Villager.class),any(java.util.function.Consumer.class));
|
||||
f.command.execute(actor.player,"fantasystick",new String[]{"rare-shop","place","Curios"});await(f,()->((RareMerchantDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareMerchants")).all().size()==1);var directory=(RareMerchantDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareMerchants");var shop=directory.all().getFirst();assertEquals("Curios",shop.name());var controller=(RareMerchantController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareMerchantController");assertTrue(controller.tagged(villager));
|
||||
f.command.execute(actor.player,"fantasystick",new String[]{"rare-shop","place","Curios"});await(f,()->((RareMerchantDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareMerchants")).all().size()==1);var directory=(RareMerchantDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareMerchants");var shop=directory.all().getFirst();assertEquals("Curios",shop.name());var controller=(RareMerchantController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareMerchantController");NativeTestAwait.until(f,()->controller.tagged(villager),"saved merchant reached native spawn/tagging");assertTrue(controller.tagged(villager));
|
||||
var repository=new YamlCharacterRepository(data);var old=repository.load(actor.core.record.id()).orElseThrow();var purchasedStats=new java.util.EnumMap<>(old.stats());purchasedStats.put(games.dmg.fantasystick.progression.Stat.DEX,BigInteger.ONE);repository.replace(new CharacterRecord(old.id(),old.name(),old.revision().add(BigInteger.ONE),purchasedStats,old.level(),old.experience(),BigInteger.valueOf(39),old.health(),old.learned(),old.loadout(),old.passives(),old.cooldowns(),old.restrictedKills(),false,null,null,BigInteger.valueOf(150),true,old.rewardReceipts(),old.pvpCoinEscrow(),old.gear()),old.revision());f.activate(actor);
|
||||
int opened=actor.inventories.size(),books=actor.books.size();var main=new PlayerInteractEntityEvent(actor.player,villager,EquipmentSlot.HAND);var offhand=new PlayerInteractEntityEvent(actor.player,villager,EquipmentSlot.OFF_HAND);f.dispatcher.accept(main);f.dispatcher.accept(offhand);assertTrue(main.isCancelled());assertTrue(offhand.isCancelled());await(f,()->actor.inventories.size()==opened+1);assertEquals(books,actor.books.size());var menu=actor.inventories.getLast();var potion=menu.getItem(11);assertEquals(Material.POTION,potion.getType());assertTrue(net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText().serialize(potion.getItemMeta().displayName()).contains("Health Potion — 40 coins"));assertEquals("Stock: 1",net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText().serialize(potion.getItemMeta().lore().getFirst()));
|
||||
var menus=(RareShopMenus)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareShopMenus");var click=mock(InventoryClickEvent.class);when(click.getInventory()).thenReturn(menu);when(click.getWhoClicked()).thenReturn(actor.player);when(click.getCurrentItem()).thenReturn(potion);menus.click(click);var inventory=(RareInventoryDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"rareInventory");await(f,()->inventory.quantity(actor.core.record.id(),RareProduct.HEALTH_POTION).equals(BigInteger.ONE));var characters=(CharacterStorage)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characters");var sessions=(games.dmg.fantasystick.session.EquipmentSessions)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"sessions");await(f,()->sessions.active(actor.core.id).flatMap(sessions::currentRecord).map(value->value.coins().equals(BigInteger.valueOf(110))).orElse(false));var active=sessions.active(actor.core.id).orElseThrow(()->new AssertionError("Purchase removed active session: "+actor.messages));var record=sessions.currentRecord(active).orElseThrow(()->new AssertionError("Purchase left no current record: "+actor.messages));var injured=characters.saveHealth(record.id(),record.revision(),BigDecimal.valueOf(5),BigDecimal.valueOf(100)).join().orElseThrow(()->new AssertionError("Injury save was refused"));assertTrue(sessions.acknowledgeProgress(active,record.revision(),injured));actor.player.setHealth(5);
|
||||
|
||||
Reference in new issue
Block a user