From 413074c2fa359dcefd7e52462547d0a6935dc56d Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Sat, 26 Sep 2026 16:05:24 +0000 Subject: [PATCH] test: cover Archaeo behaviour and fix bugs the tests exposed Adds JUnit, Mockito and MockBukkit tests across every package, with JaCoCo reporting in the build and CI. The tests assert gameplay, player-facing text, data preservation and failure handling; none exist only to raise coverage. An opt-in pack-api-tests profile runs adapter contract tests against locally supplied ItemsAdder and MMOItems jars. Fixes found by the tests: - a failed /archaeo reload emptied the live catalogues; reload is now atomic - an off-hand camp kit showed its ghost but could not plant a camp - ItemsAdder ids such as glow:stone were parsed as vanilla materials - studied finds lost their dossier notes when the artifact left the catalogue - renaming a site after a restart blanked filed drawings not yet rebound - confirming a sketch signature could sign a held vanilla map instead - Spigot's RegistryAware biome keys could throw for unregistered biomes - stale camp-menu authority, relocations finishing after closure, outline timers removing their replacement, sketch maps keeping an old renderer, paused brushes overwriting shared progress, and MMOItems binding the wrong getItem overload Removes proven dead code: redundant guards whose callers already guarantee the condition, unused helpers and a legacy pick.tools fallback that could no longer run. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/build.yml | 8 + README.md | 28 + pom.xml | 67 + .../tfminecraft/archaeo/ArcheologyPlugin.java | 44 +- .../archaeo/command/ArchaeoCommand.java | 104 +- .../archaeo/config/ArtifactTemplate.java | 15 +- .../archaeo/config/CatalogRegistry.java | 233 ++-- .../archaeo/config/ConfigEnums.java | 3 +- .../config/InterpretationTemplate.java | 2 +- .../tfminecraft/archaeo/config/LabStain.java | 22 +- .../archaeo/config/MuseumSettings.java | 40 +- .../archaeo/establish/CampArchiveBook.java | 16 +- .../archaeo/establish/CampBoard.java | 49 +- .../archaeo/establish/CampClosure.java | 16 +- .../archaeo/establish/CampFindBoard.java | 15 +- .../archaeo/establish/CampFindsBoard.java | 20 +- .../archaeo/establish/CampIdentifyBoard.java | 16 +- .../archaeo/establish/CampListener.java | 178 ++- .../archaeo/establish/CampNames.java | 10 +- .../archaeo/establish/CampStaffBoard.java | 20 +- .../archaeo/establish/CampTemplate.java | 15 +- .../archaeo/establish/CampWoolPicker.java | 10 +- .../archaeo/establish/CampWools.java | 9 +- .../archaeo/establish/CampWorkerBoard.java | 33 +- .../archaeo/establish/EstablishListener.java | 26 +- .../archaeo/establish/EstablishService.java | 222 ++-- .../archaeo/establish/FindReportBook.java | 19 +- .../archaeo/excavation/DigTools.java | 14 +- .../archaeo/excavation/FindBreakCue.java | 3 - .../archaeo/excavation/FindDustService.java | 5 +- .../archaeo/excavation/HandPickService.java | 110 +- .../archaeo/excavation/LiftPlan.java | 18 +- .../archaeo/excavation/PrismFill.java | 9 +- .../archaeo/excavation/PrismListener.java | 10 +- .../excavation/PrismOutlineService.java | 9 +- .../archaeo/excavation/PrismWound.java | 8 +- .../archaeo/excavation/RecoverService.java | 138 +-- .../tfminecraft/archaeo/item/ItemMatcher.java | 39 +- .../net/tfminecraft/archaeo/item/ItemRef.java | 12 +- .../archaeo/item/PackPluginHook.java | 77 +- .../archaeo/item/RecoveredFindItem.java | 70 +- .../archaeo/item/RecoveredFindListener.java | 12 +- .../archaeo/item/SiteLabelRefresh.java | 16 +- .../archaeo/item/SketchSupplies.java | 31 +- .../tfminecraft/archaeo/item/ToolWear.java | 4 +- .../tfminecraft/archaeo/model/BuriedFind.java | 11 +- .../archaeo/museum/MuseumListener.java | 23 +- .../archaeo/prospect/ProspectService.java | 27 +- .../site/AutoRuinEvaluationLedger.java | 15 +- .../archaeo/site/ChunkRuinFitness.java | 10 +- .../archaeo/site/RuinAutoSpawner.java | 55 +- .../tfminecraft/archaeo/site/SiteCensus.java | 14 - .../tfminecraft/archaeo/site/SiteClosure.java | 3 - .../archaeo/site/SiteGenerator.java | 61 +- .../tfminecraft/archaeo/site/SitePurge.java | 43 +- .../archaeo/site/SiteRepository.java | 33 +- .../archaeo/sketch/CabinetCues.java | 10 - .../archaeo/sketch/CabinetLab.java | 96 +- .../archaeo/sketch/CabinetLabBoard.java | 107 +- .../archaeo/sketch/SketchCabinet.java | 19 +- .../tfminecraft/archaeo/sketch/SketchInk.java | 5 +- .../archaeo/sketch/SketchListener.java | 70 +- .../archaeo/sketch/SketchRenderer.java | 5 + .../archaeo/sketch/SketchService.java | 402 ++---- .../archaeo/sketch/SketchSession.java | 14 +- .../archaeo/tracker/TrackerService.java | 31 +- .../archaeo/ArcheologyPluginTest.java | 58 + .../archaeo/command/ArchaeoCommandTest.java | 601 +++++++++ .../archaeo/config/AutoRuinSettingsTest.java | 62 + .../archaeo/config/CatalogRegistryTest.java | 525 ++++++++ .../archaeo/config/CatalogValuesTest.java | 130 ++ .../archaeo/config/ConfigEnumsTest.java | 46 + .../config/ExcavationSettingsTest.java | 130 ++ .../establish/CampArchiveBookTest.java | 116 ++ .../archaeo/establish/CampBoardsTest.java | 296 +++++ .../archaeo/establish/CampClosureTest.java | 142 +++ .../archaeo/establish/CampFindBoardTest.java | 110 ++ .../establish/CampIdentifyBoardTest.java | 105 ++ .../archaeo/establish/CampListenerTest.java | 737 +++++++++++ .../archaeo/establish/CampNamesTest.java | 44 + .../archaeo/establish/CampSignsTest.java | 63 + .../archaeo/establish/CampTemplateTest.java | 103 ++ .../establish/EstablishListenerTest.java | 143 +++ .../establish/EstablishServiceTest.java | 746 +++++++++++ .../archaeo/establish/FindReportBookTest.java | 129 ++ .../excavation/FindDustServiceTest.java | 308 +++++ .../excavation/FindWoundLedgerTest.java | 90 ++ .../excavation/HandPickListenerTest.java | 235 ++++ .../excavation/HandPickServiceTest.java | 916 ++++++++++++++ .../archaeo/excavation/HoldCuePlanTest.java | 35 + .../archaeo/excavation/LiftPlanTest.java | 200 +++ .../excavation/NeighborTracesTest.java | 133 ++ .../archaeo/excavation/PrismFillTest.java | 53 + .../archaeo/excavation/PrismListenerTest.java | 395 ++++++ .../excavation/PrismOutlineServiceTest.java | 266 ++++ .../archaeo/excavation/PrismWoundTest.java | 196 +++ .../excavation/RecoverListenerTest.java | 125 ++ .../excavation/RecoverServiceTest.java | 802 ++++++++++++ .../excavation/VanillaBreakClockTest.java | 187 +++ .../archaeo/item/DigToolsTest.java | 197 +++ .../archaeo/item/HiddenPackApiLoader.java | 63 + .../archaeo/item/ItemMatcherPackIT.java | 208 ++++ .../tfminecraft/archaeo/item/ItemRefTest.java | 137 ++ .../archaeo/item/MissingPackApiTest.java | 59 + .../archaeo/item/PackPluginHookIT.java | 149 +++ .../archaeo/item/RecoveredFindItemTest.java | 302 +++++ .../item/RecoveredFindListenerTest.java | 223 ++++ .../archaeo/item/RoleItemsTest.java | 91 ++ .../archaeo/item/SketchSuppliesTest.java | 171 +++ .../archaeo/item/ToolWearTest.java | 78 ++ .../archaeo/model/BuriedFindTest.java | 224 ++++ .../tfminecraft/archaeo/model/SiteTest.java | 398 ++++++ .../archaeo/model/WorkerRecordTest.java | 78 ++ .../archaeo/museum/MuseumListenerTest.java | 236 +++- .../prospect/ProspectListenerTest.java | 72 ++ .../archaeo/prospect/ProspectServiceTest.java | 204 +++ .../site/AutoRuinEvaluationLedgerTest.java | 197 +++ .../archaeo/site/ChunkRuinFitnessTest.java | 107 ++ .../archaeo/site/GroundDatumTest.java | 75 ++ .../archaeo/site/RuinAutoSpawnerTest.java | 264 ++++ .../archaeo/site/SiteClosureTest.java | 87 ++ .../archaeo/site/SiteGeneratorTest.java | 396 ++++++ .../archaeo/site/SiteRepositoryTest.java | 427 +++++++ .../archaeo/site/SiteStoredItemsTest.java | 212 ++++ .../archaeo/sketch/CabinetLabBoardTest.java | 116 ++ .../archaeo/sketch/CabinetLabTest.java | 195 +++ .../sketch/SketchAutosaveStoreTest.java | 14 + .../archaeo/sketch/SketchCabinetTest.java | 90 ++ .../archaeo/sketch/SketchListenerTest.java | 430 +++++++ .../archaeo/sketch/SketchRendererTest.java | 94 +- .../archaeo/sketch/SketchServiceTest.java | 1100 +++++++++++++++++ .../archaeo/sketch/SketchSessionTest.java | 63 +- .../archaeo/sketch/SketchSheetTest.java | 17 + .../archaeo/tracker/TrackerServiceTest.java | 265 ++++ 134 files changed, 16374 insertions(+), 1771 deletions(-) create mode 100644 src/test/java/net/tfminecraft/archaeo/ArcheologyPluginTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/command/ArchaeoCommandTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/config/AutoRuinSettingsTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/config/CatalogRegistryTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/config/CatalogValuesTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/config/ConfigEnumsTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/config/ExcavationSettingsTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/establish/CampArchiveBookTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/establish/CampBoardsTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/establish/CampClosureTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/establish/CampFindBoardTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/establish/CampIdentifyBoardTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/establish/CampListenerTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/establish/CampNamesTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/establish/CampSignsTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/establish/CampTemplateTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/establish/EstablishListenerTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/establish/EstablishServiceTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/establish/FindReportBookTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/excavation/FindDustServiceTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/excavation/FindWoundLedgerTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/excavation/HandPickListenerTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/excavation/HandPickServiceTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/excavation/HoldCuePlanTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/excavation/LiftPlanTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/excavation/NeighborTracesTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/excavation/PrismFillTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/excavation/PrismListenerTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/excavation/PrismOutlineServiceTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/excavation/PrismWoundTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/excavation/RecoverListenerTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/excavation/RecoverServiceTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/excavation/VanillaBreakClockTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/item/DigToolsTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/item/HiddenPackApiLoader.java create mode 100644 src/test/java/net/tfminecraft/archaeo/item/ItemMatcherPackIT.java create mode 100644 src/test/java/net/tfminecraft/archaeo/item/ItemRefTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/item/MissingPackApiTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/item/PackPluginHookIT.java create mode 100644 src/test/java/net/tfminecraft/archaeo/item/RecoveredFindItemTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/item/RecoveredFindListenerTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/item/RoleItemsTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/item/SketchSuppliesTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/item/ToolWearTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/model/BuriedFindTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/model/SiteTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/model/WorkerRecordTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/prospect/ProspectListenerTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/prospect/ProspectServiceTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/site/AutoRuinEvaluationLedgerTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/site/ChunkRuinFitnessTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/site/GroundDatumTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/site/RuinAutoSpawnerTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/site/SiteClosureTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/site/SiteGeneratorTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/site/SiteRepositoryTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/site/SiteStoredItemsTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/sketch/CabinetLabBoardTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/sketch/CabinetLabTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/sketch/SketchCabinetTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/sketch/SketchListenerTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/sketch/SketchServiceTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/sketch/SketchSheetTest.java create mode 100644 src/test/java/net/tfminecraft/archaeo/tracker/TrackerServiceTest.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76bad4f..fe94278 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,3 +68,11 @@ jobs: name: unit-test-reports-${{ github.run_id }}-${{ github.run_attempt }} path: target/surefire-reports/ if-no-files-found: error + + - name: Upload coverage report + if: ${{ !cancelled() && hashFiles('target/site/jacoco/jacoco.xml') != '' }} + uses: actions/upload-artifact@v7 + with: + name: coverage-report-${{ github.run_id }}-${{ github.run_attempt }} + path: target/site/jacoco/ + if-no-files-found: error diff --git a/README.md b/README.md index 1f28b57..496e46e 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,34 @@ them before they ever reach the surface. Technical documentation is maintained in [TF-Minecraft/Docs](https://github.com/TF-Minecraft/Docs). +## Tests + +With Java 21 and Maven installed, run `mvn clean verify`. Tests exercise domain +rules, YAML persistence, and plugin workflows using JUnit, Mockito, and MockBukkit. +JaCoCo measures all production classes and writes its HTML report to +`target/site/jacoco/index.html` and machine-readable results to +`target/site/jacoco/jacoco.xml`. The build workflow uploads the coverage report. + +Tests should assert gameplay behavior, data preservation, or failure handling. +Do not add tests solely to execute a line or manufacture unreachable states to +increase coverage. MockBukkit tests do not replace testing on a real Paper server +with the optional ItemsAdder and MMOItems integrations installed. + +Adapter contract tests can also run against locally supplied plugin jars: + +```sh +mvn -Ppack-api-tests clean verify \ + -Ditemsadder.jar=/path/to/ItemsAdder.jar \ + -Dfastnbt.jar=/path/to/FastNbt-jar.jar \ + -Dmmoitems.jar=/path/to/MMOItems.jar \ + -Dmythiclib.jar=/path/to/MythicLib.jar +``` + +These tests bind the actual API classes and mock their external operations; they +do not start those plugins. The jars remain outside this repository and are used +only on the test runtime classpath. Use the FastNbt version declared by your +ItemsAdder jar. The ordinary test suite needs none of these files. + ## License Copyright (c) 2026 TF-Minecraft contributors. diff --git a/pom.xml b/pom.xml index 65332a8..a410ec1 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,18 @@ + + org.mockbukkit.mockbukkit + mockbukkit-v1.21 + 4.95.0 + test + + + org.mockito + mockito-core + 5.20.0 + test + junit junit @@ -38,7 +50,62 @@ + + + pack-api-tests + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Test.java + **/*IT.java + + + ${itemsadder.jar} + ${fastnbt.jar} + ${mmoitems.jar} + ${mythiclib.jar} + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + + org.apache.maven.surefire + surefire-junit4 + 3.5.4 + + + + + org.jacoco + jacoco-maven-plugin + 0.8.15 + + + prepare-agent + + + coverage-report + verify + report + + + + archaeo-${project.version} ${project.basedir}/src/main/java diff --git a/src/main/java/net/tfminecraft/archaeo/ArcheologyPlugin.java b/src/main/java/net/tfminecraft/archaeo/ArcheologyPlugin.java index 3962db0..1c87239 100644 --- a/src/main/java/net/tfminecraft/archaeo/ArcheologyPlugin.java +++ b/src/main/java/net/tfminecraft/archaeo/ArcheologyPlugin.java @@ -172,11 +172,10 @@ public void onEnable() { autoRuins, campClosure, sitePurge); + // plugin.yml in this jar declares the command, so Bukkit always returns it. PluginCommand pluginCommand = getCommand("archaeo"); - if (pluginCommand != null) { - pluginCommand.setExecutor(command); - pluginCommand.setTabCompleter(command); - } + pluginCommand.setExecutor(command); + pluginCommand.setTabCompleter(command); getLogger().info("Archaeo enabled. Loaded sites: " + sites.all().size()); } @@ -186,34 +185,17 @@ public void onEnable() { * @param matcher detected APIs, or vanilla-only */ public void bindItemMatcher(ItemMatcher matcher) { + // Callers (enable, reload, the pack-plugin hook) run only after onEnable built every item. ItemMatcher bound = matcher == null ? ItemMatcher.vanillaOnly() : matcher; - if (trackerItem != null) { - trackerItem.setMatcher(bound); - } - if (prospectItem != null) { - prospectItem.setMatcher(bound); - } - if (establishItem != null) { - establishItem.setMatcher(bound); - } - if (brushItem != null) { - brushItem.setMatcher(bound); - } - if (sketchSupplies != null) { - sketchSupplies.setMatcher(bound); - } - if (digTools != null) { - digTools.setMatcher(bound); - } - if (recoveredFindItem != null) { - recoveredFindItem.setMatcher(bound); - } - if (sketch != null) { - sketch.setMatcher(bound); - } - if (museum != null) { - museum.setMatcher(bound); - } + trackerItem.setMatcher(bound); + prospectItem.setMatcher(bound); + establishItem.setMatcher(bound); + brushItem.setMatcher(bound); + sketchSupplies.setMatcher(bound); + digTools.setMatcher(bound); + recoveredFindItem.setMatcher(bound); + sketch.setMatcher(bound); + museum.setMatcher(bound); } /** diff --git a/src/main/java/net/tfminecraft/archaeo/command/ArchaeoCommand.java b/src/main/java/net/tfminecraft/archaeo/command/ArchaeoCommand.java index 81549ff..b6380da 100644 --- a/src/main/java/net/tfminecraft/archaeo/command/ArchaeoCommand.java +++ b/src/main/java/net/tfminecraft/archaeo/command/ArchaeoCommand.java @@ -242,14 +242,10 @@ private boolean handleReload(CommandSender sender) { catalogs.items().sketchPencil(), catalogs.sketch().pencilUses()); plugin.sketch().setSettings(catalogs.sketch()); - if (autoRuins != null) { - autoRuins.setSettings(catalogs.autoRuins()); - } + autoRuins.setSettings(catalogs.autoRuins()); sites.loadAll(); sender.sendMessage("Reloaded Archaeo config, catalogs, and sites from disk."); - if (autoRuins != null) { - sender.sendMessage(autoRuins.statusLine()); - } + sender.sendMessage(autoRuins.statusLine()); } catch (RuntimeException exception) { sender.sendMessage("Reload failed: " + exception.getMessage()); } @@ -373,7 +369,7 @@ private static boolean giveStack( String givenLabel, String receivedMessage ) { - if (stack == null || stack.getType().isAir()) { + if (stack.getType().isAir()) { sender.sendMessage("Could not create that item. Check pack plugins and config.yml."); return true; } @@ -897,10 +893,6 @@ private boolean handleRuinTp(CommandSender sender, String[] args) { * @return {@code true} always (handled) */ private boolean handleRuinAuto(CommandSender sender, String[] args) { - if (autoRuins == null) { - sender.sendMessage("Auto-ruins are not available on this server."); - return true; - } if (args.length < 3) { sender.sendMessage("Usage: /archaeo ruin auto status|reset"); return true; @@ -944,7 +936,7 @@ private boolean handleCamps(CommandSender sender, String[] args) { sender.sendMessage("Console must name a player before a serial: /archaeo ruin camps #serial"); return true; } - return teleportToCamp(sender, (Player) sender, onlySerial.get(), null); + return teleportToCamp((Player) sender, onlySerial.get(), null); } director = CampNames.known(args[2]); if (director == null) { @@ -969,7 +961,7 @@ private boolean handleCamps(CommandSender sender, String[] args) { sender.sendMessage("Only a player can teleport to a camp."); return true; } - return teleportToCamp(sender, player, serial.get(), directorId); + return teleportToCamp(player, serial.get(), directorId); } List camps = sites.findDirectedCamps(directorId); String who = CampNames.of(sender instanceof Player player ? player : null, directorId); @@ -979,12 +971,12 @@ private boolean handleCamps(CommandSender sender, String[] args) { } if (camps.size() == 1 && sender instanceof Player player) { Site only = camps.getFirst(); - sendCampLine(sender, only, false); - return teleportPlayerToCamp(sender, player, only); + sendCampLine(sender, only, null); + return teleportPlayerToCamp(player, only); } sender.sendMessage(who + " directs " + camps.size() + " camp" + (camps.size() == 1 ? "" : "s") + ":"); for (Site camp : camps) { - sendCampLine(sender, camp, sender instanceof Player); + sendCampLine(sender, camp, sender instanceof Player player ? player : null); } if (camps.size() > 1 && sender instanceof Player) { sender.sendMessage("Click [tp] or run /archaeo ruin camps " @@ -997,51 +989,45 @@ private boolean handleCamps(CommandSender sender, String[] args) { /** * Teleports the issuer to a directed camp after checking the optional director filter. * - * @param sender who receives errors - * @param traveler player to move + * @param traveler issuing player, who also receives errors * @param serial site serial * @param expectedDirector required director, or {@code null} to accept any locked camp * @return {@code true} always */ - private boolean teleportToCamp( - CommandSender sender, - Player traveler, - int serial, - UUID expectedDirector - ) { + private boolean teleportToCamp(Player traveler, int serial, UUID expectedDirector) { Optional resolved = sites.findBySerial(serial); if (resolved.isEmpty()) { - sender.sendMessage("No site with serial #" + serial + "."); + traveler.sendMessage("No site with serial #" + serial + "."); return true; } Site site = resolved.get(); if (!site.isCampLocked() || site.getCampX() == null) { - sender.sendMessage(site.displayLabel() + " has no standing camp to teleport to."); + traveler.sendMessage(site.displayLabel() + " has no standing camp to teleport to."); return true; } if (expectedDirector != null && !site.isDirector(expectedDirector)) { - sender.sendMessage(site.displayLabel() + " is not directed by that player."); + traveler.sendMessage(site.displayLabel() + " is not directed by that player."); return true; } - return teleportPlayerToCamp(sender, traveler, site); + return teleportPlayerToCamp(traveler, site); } /** - * Moves {@code traveler} to the camp table block of {@code site}. + * Moves the issuing player to the camp table block of {@code site}. * - * @param sender who receives confirmations - * @param traveler player to move + * @param traveler issuing player, who also receives confirmations * @param site locked camp * @return {@code true} always */ - private boolean teleportPlayerToCamp(CommandSender sender, Player traveler, Site site) { + private boolean teleportPlayerToCamp(Player traveler, Site site) { World world = Bukkit.getWorld(site.getWorldName()); if (world == null) { - sender.sendMessage("World not loaded: " + site.getWorldName()); + traveler.sendMessage("World not loaded: " + site.getWorldName()); return true; } - if (site.getCampX() == null || site.getCampY() == null || site.getCampZ() == null) { - sender.sendMessage(site.displayLabel() + " has no camp coordinates."); + // The table's three coordinates are written together (Site#establish, dossier load). + if (site.getCampX() == null) { + traveler.sendMessage(site.displayLabel() + " has no camp coordinates."); return true; } Location destination = new Location( @@ -1050,12 +1036,9 @@ private boolean teleportPlayerToCamp(CommandSender sender, Player traveler, Site site.getCampY() + 1.0, site.getCampZ() + 0.5); traveler.teleport(destination); - sender.sendMessage("Teleported to " + site.displayLabel() + traveler.sendMessage("Teleported to " + site.displayLabel() + " · camp " + site.getCampX() + "," + site.getCampY() + "," + site.getCampZ() + " (" + site.getWorldName() + ")."); - if (traveler != sender) { - traveler.sendMessage("Staff moved you to camp " + site.publicName() + "."); - } return true; } @@ -1064,9 +1047,9 @@ private boolean teleportPlayerToCamp(CommandSender sender, Player traveler, Site * * @param sender who receives the line * @param site locked camp - * @param clickable whether to attach a {@code [tp]} run-command + * @param clickable player to receive a {@code [tp]} run-command, or {@code null} for plain text */ - private void sendCampLine(CommandSender sender, Site site, boolean clickable) { + private void sendCampLine(CommandSender sender, Site site, Player clickable) { String coords = site.getCampX() == null ? "unknown" : site.getCampX() + "," + site.getCampY() + "," + site.getCampZ(); @@ -1074,7 +1057,7 @@ private void sendCampLine(CommandSender sender, Site site, boolean clickable) { + " · " + site.getStatus().name().toLowerCase(Locale.ROOT) + " · " + site.getWorldName() + " · camp " + coords; - if (!clickable || !(sender instanceof Player player)) { + if (clickable == null) { sender.sendMessage(" " + body); return; } @@ -1087,7 +1070,7 @@ private void sendCampLine(CommandSender sender, Site site, boolean clickable) { tp.setHoverEvent(new HoverEvent( HoverEvent.Action.SHOW_TEXT, new Text("Teleport to this camp"))); - player.spigot().sendMessage(prefix, tp); + clickable.spigot().sendMessage(prefix, tp); } /** @@ -1127,9 +1110,6 @@ private Optional resolveQuery(CommandSender sender, String query) { * @return serial if the whole query is a number */ private Optional parseSerial(String raw) { - if (raw == null) { - return Optional.empty(); - } String trimmed = raw.trim(); if (trimmed.startsWith("#")) { trimmed = trimmed.substring(1).trim(); @@ -1316,7 +1296,8 @@ public List onTabComplete(CommandSender sender, Command command, String if (args.length == 3 && "stats".equalsIgnoreCase(args[1])) { return worldNamesStartingWith(args[2]); } - if (args.length >= 3 && "set-interest".equalsIgnoreCase(args[1])) { + // From here every ruin action has at least one token after it (args.length >= 3). + if ("set-interest".equalsIgnoreCase(args[1])) { if (args.length == 3) { return INTERESTS.stream() .filter(value -> value.startsWith(args[2].toLowerCase(Locale.ROOT))) @@ -1327,7 +1308,7 @@ public List onTabComplete(CommandSender sender, Command command, String args.length == 4, site -> site.getStatus() == SiteStatus.HIDDEN); } - if (args.length >= 3 && "close".equalsIgnoreCase(args[1])) { + if ("close".equalsIgnoreCase(args[1])) { String last = args[args.length - 1].toLowerCase(Locale.ROOT); if ("confirm".equals(last) && args.length > 3) { return List.of(); @@ -1341,7 +1322,7 @@ public List onTabComplete(CommandSender sender, Command command, String } return suggestions; } - if (args.length >= 3 && "delete".equalsIgnoreCase(args[1])) { + if ("delete".equalsIgnoreCase(args[1])) { String last = args[args.length - 1].toLowerCase(Locale.ROOT); if ("confirm".equals(last) && args.length > 3) { return List.of(); @@ -1355,7 +1336,7 @@ public List onTabComplete(CommandSender sender, Command command, String } return suggestions; } - if (args.length >= 3 && "create".equalsIgnoreCase(args[1])) { + if ("create".equalsIgnoreCase(args[1])) { if (args.length == 3) { return INTERESTS.stream() .filter(value -> value.startsWith(args[2].toLowerCase(Locale.ROOT))) @@ -1363,7 +1344,7 @@ public List onTabComplete(CommandSender sender, Command command, String } return List.of(); } - if (args.length >= 3 && "info".equalsIgnoreCase(args[1])) { + if ("info".equalsIgnoreCase(args[1]) || "tp".equalsIgnoreCase(args[1])) { String typed = Arrays.stream(args).skip(2).collect(Collectors.joining(" ")); List names = new ArrayList<>(sites.namesStartingWith(typed)); if (args.length == 3) { @@ -1377,21 +1358,7 @@ public List onTabComplete(CommandSender sender, Command command, String } return names; } - if (args.length >= 3 && "tp".equalsIgnoreCase(args[1])) { - String typed = Arrays.stream(args).skip(2).collect(Collectors.joining(" ")); - List names = new ArrayList<>(sites.namesStartingWith(typed)); - if (args.length == 3) { - String token = args[2].toLowerCase(Locale.ROOT); - for (Site site : sites.all()) { - String serial = "#" + site.getSerial(); - if (serial.startsWith(token) || String.valueOf(site.getSerial()).startsWith(token)) { - names.add(serial); - } - } - } - return names; - } - if (args.length >= 3 && "camps".equalsIgnoreCase(args[1])) { + if ("camps".equalsIgnoreCase(args[1])) { if (args.length == 3) { List suggestions = new ArrayList<>(onlineNamesStartingWith(args[2])); String token = args[2].toLowerCase(Locale.ROOT); @@ -1429,13 +1396,12 @@ public List onTabComplete(CommandSender sender, Command command, String /** * Name and serial suggestions for a staff site query, optionally filtered. * - * @param typed text after the subcommand + * @param needle text after the subcommand * @param includeSerials whether the current token can still be a serial * @param filter sites to offer * @return matching names and serials */ - private List suggestSiteQueries(String typed, boolean includeSerials, Predicate filter) { - String needle = typed == null ? "" : typed; + private List suggestSiteQueries(String needle, boolean includeSerials, Predicate filter) { List names = new ArrayList<>(); for (String name : sites.namesStartingWith(needle)) { boolean allowed = false; diff --git a/src/main/java/net/tfminecraft/archaeo/config/ArtifactTemplate.java b/src/main/java/net/tfminecraft/archaeo/config/ArtifactTemplate.java index fbe7a22..aece6c5 100644 --- a/src/main/java/net/tfminecraft/archaeo/config/ArtifactTemplate.java +++ b/src/main/java/net/tfminecraft/archaeo/config/ArtifactTemplate.java @@ -84,7 +84,8 @@ public ItemRef resolveRef(String chosen) { if (chosen != null && !chosen.isBlank()) { String token = chosen.trim(); for (ItemRef ref : items) { - if (matchesStored(ref, token)) { + // Vanilla tokens are the bare material name; "minecraft:" tokens resolve via parse below. + if (ref.commandToken().equalsIgnoreCase(token)) { return ref; } } @@ -110,16 +111,4 @@ public Material resolveItem(String chosen) { } return Material.BRICK; } - - /** - * @param ref catalog pool entry - * @param token stored dossier token - * @return whether this is the same item, including old vanilla names without a {@code minecraft:} prefix - */ - private static boolean matchesStored(ItemRef ref, String token) { - if (ref.commandToken().equalsIgnoreCase(token)) { - return true; - } - return ref.kind() == ItemRef.Kind.VANILLA && ref.primary().equalsIgnoreCase(token); - } } diff --git a/src/main/java/net/tfminecraft/archaeo/config/CatalogRegistry.java b/src/main/java/net/tfminecraft/archaeo/config/CatalogRegistry.java index 4b4257b..178e779 100644 --- a/src/main/java/net/tfminecraft/archaeo/config/CatalogRegistry.java +++ b/src/main/java/net/tfminecraft/archaeo/config/CatalogRegistry.java @@ -69,6 +69,10 @@ public CatalogRegistry(JavaPlugin plugin) { /** * Ensures default catalog files exist on disk (first run only), then reads them into memory. + * The required catalogues are parsed before anything is replaced, so a malformed file leaves + * the previously loaded catalogue whole (a failed {@code /archaeo reload} changes nothing). + * + * @throws IllegalStateException if a required section is missing */ public void load() { copyDefaultIfAbsent("config.yml"); @@ -81,9 +85,15 @@ public void load() { plugin.reloadConfig(); FileConfiguration config = plugin.getConfig(); + YamlConfiguration interestFile = yaml("interest.yml"); + Map loadedInterests = readInterests(interestFile, config); + Map loadedStrata = readStrata(yaml("strata.yml")); + Map loadedArtifacts = readArtifacts(yaml("artifacts.yml")); + Map loadedHints = readHints(yaml("hints.yml")); + loadItems(yaml("items.yml"), config); - loadInterests(yaml("interest.yml"), config); - loadGeneration(yaml("interest.yml"), config); + replace(interests, loadedInterests); + loadGeneration(interestFile, config); loadTracker(config); loadProspect(config); loadEstablish(config); @@ -96,13 +106,24 @@ public void load() { loadStaffPermission(config); loadRarities(config); loadRarityFromWeight(config); - loadStrata(yaml("strata.yml")); - loadArtifacts(yaml("artifacts.yml")); - loadHints(yaml("hints.yml")); + replace(strata, loadedStrata); + replace(artifacts, loadedArtifacts); + replace(hints, loadedHints); loadInterpretations(yaml("interpretations.yml")); loadMaterials(yaml("materials.yml")); } + /** + * @param live catalogue map + * @param loaded freshly parsed rows + * @param key type + * @param row type + */ + private static void replace(Map live, Map loaded) { + live.clear(); + live.putAll(loaded); + } + /** * @param level interest key from config * @return generation budget and detection settings for that level @@ -217,13 +238,10 @@ public String rarityLoreLine(String raw) { } /** - * @param id resolved rarity key + * @param id rarity key from {@link #resolveRarityId(String, int)}, never blank * @return lore line */ private String rarityLoreLineForId(String id) { - if (id == null || id.isBlank()) { - return rarities.getOrDefault("common", ArtifactRarity.COMMON.style()).loreLine(); - } String key = id.trim().toLowerCase(Locale.ROOT); RarityStyle style = rarities.get(key); if (style != null) { @@ -321,7 +339,7 @@ public List interpretationTypes() { public List interpretationTypes(FindProfile profile) { FindProfile key = profile == null ? FindProfile.OBJECT : profile; List ids = profileTypeIds.get(key); - if (ids == null || ids.isEmpty()) { + if (ids == null) { return interpretationTypes(); } List list = new ArrayList<>(); @@ -390,8 +408,9 @@ public List stationOffers( String artifactId, Set tags ) { + // Loaded types always carry at least one option. InterpretationType type = interpretationType(typeId); - if (type == null || type.options() == null || type.options().isEmpty()) { + if (type == null) { return List.of(); } FindProfile profile = profileOf(artifactId); @@ -424,14 +443,13 @@ public List stationOffers( pool.remove(floor); offers.add(floor); } - while (offers.size() < want && !pool.isEmpty()) { + // A draw only happens when the pool holds more than three phrases, so three are always taken. + while (offers.size() < want) { offers.add(takeWeighted(pool, rng, weightTags)); } - if (offers.size() > 1) { - int slot = rng.nextInt(offers.size()); - InterpretationTemplate first = offers.remove(0); - offers.add(slot, first); - } + int slot = rng.nextInt(offers.size()); + InterpretationTemplate first = offers.remove(0); + offers.add(slot, first); return List.copyOf(offers); } @@ -452,14 +470,12 @@ private static InterpretationTemplate takeWeighted( weights[i] = pool.get(i).suggestedBy(tags) ? 3 : 1; total += weights[i]; } - int roll = rng.nextInt(Math.max(1, total)); + // roll < total, so the walk always stops inside the pool. + int roll = rng.nextInt(total); int index = 0; - for (int i = 0; i < pool.size(); i++) { - roll -= weights[i]; - if (roll < 0) { - index = i; - break; - } + while (roll >= weights[index]) { + roll -= weights[index]; + index++; } return pool.remove(index); } @@ -680,15 +696,13 @@ private ItemRef parseItemId( * @return first non-blank string that is not a section */ private static String firstPath(Configuration root, String... paths) { - if (root == null) { - return null; - } for (String path : paths) { if (!root.contains(path) || root.isConfigurationSection(path)) { continue; } + // A present scalar always reads back as text. String value = root.getString(path); - if (value != null && !value.isBlank()) { + if (!value.isBlank()) { return value; } } @@ -710,9 +724,7 @@ private Map> loadExcavationItemLists( ) { Map> profiles = new LinkedHashMap<>(); ConfigurationSection tools = config.getConfigurationSection("excavation.tools"); - ConfigurationSection itemsExcavation = itemsFile == null - ? null - : itemsFile.getConfigurationSection("excavation"); + ConfigurationSection itemsExcavation = itemsFile.getConfigurationSection("excavation"); LinkedHashSet ids = new LinkedHashSet<>(fallback.excavationProfiles().keySet()); if (tools != null) { ids.addAll(tools.getKeys(false)); @@ -847,7 +859,7 @@ private void loadPick(FileConfiguration config) { loadLimits(excavation, fallback.limits()), firstBool(excavation, pickSection, fallback.neighborTraces(), "neighbor-traces"), loadCues(excavation, fallback.cues()), - loadProfiles(excavation, pickSection, fallback) + loadProfiles(excavation) ); } @@ -898,12 +910,12 @@ private CueSettings loadCues(ConfigurationSection excavation, CueSettings fallba * @return materials, possibly empty */ private Set materialsFromList(List raw, String path) { - if (raw == null || raw.isEmpty()) { + if (raw.isEmpty()) { return Set.of(); } EnumSet out = EnumSet.noneOf(Material.class); for (String token : raw) { - if (token == null || token.isBlank()) { + if (token.isBlank()) { continue; } String trimmed = token.trim(); @@ -927,7 +939,7 @@ private Set materialsFromList(List raw, String path) { * @param path warning path */ private void addTagMaterials(Set out, String tagToken, String path) { - if (tagToken == null || tagToken.isBlank()) { + if (tagToken.isBlank()) { return; } String key = tagToken.contains(":") @@ -1088,45 +1100,21 @@ private static int clampPercent(int value) { /** * Builds named tools from {@code excavation.tools} (whitelist, lifts, window; tempo is vanilla). + * One profile per whitelist from {@link #loadExcavationItemLists}; the packaged hand, light and + * heavy lists always survive there, so the result is never empty. * * @param excavation {@code excavation:} or {@code null} - * @param pickSection {@code pick:} or {@code null} - * @param fallback packaged profiles - * @return named tools; legacy {@code pick.tools} becomes one {@code hand} profile - */ - private List loadProfiles( - ConfigurationSection excavation, - ConfigurationSection pickSection, - PickSettings fallback - ) { + * @return named tools in whitelist order + */ + private List loadProfiles(ConfigurationSection excavation) { ConfigurationSection tools = excavation == null ? null : excavation.getConfigurationSection("tools"); - LinkedHashSet ids = new LinkedHashSet<>(items.excavationProfiles().keySet()); - if (tools != null) { - ids.addAll(tools.getKeys(false)); - } - if (!ids.isEmpty()) { - List profiles = new ArrayList<>(); - for (String id : ids) { - ConfigurationSection tool = tools == null ? null : tools.getConfigurationSection(id); - List materials = items.profileMaterials(id); - if (materials.isEmpty() && tool != null) { - materials = toolItems(tool); - } - if (materials.isEmpty()) { - continue; - } - profiles.add(readTool(id, tool, materials, ExcavationTool.packaged(id))); - } - if (!profiles.isEmpty()) { - return List.copyOf(profiles); - } - } - if (pickSection != null && !pickSection.getStringList("tools").isEmpty()) { - List materials = ItemRef.parseAll(plugin, pickSection.getStringList("tools")); - ExcavationTool inherit = ExcavationTool.hand(); - return List.of(readTool("hand", pickSection, materials, inherit)); + List profiles = new ArrayList<>(); + for (Map.Entry> entry : items.excavationProfiles().entrySet()) { + String id = entry.getKey(); + ConfigurationSection tool = tools == null ? null : tools.getConfigurationSection(id); + profiles.add(readTool(id, tool, entry.getValue(), ExcavationTool.packaged(id))); } - return fallback.profiles(); + return List.copyOf(profiles); } /** @@ -1187,31 +1175,26 @@ private static ExcavationTool readTool( * Reads {@code tempo:} — {@code vanilla} / omit → {@code 0}; a number → Archaeo metronome ticks. * Legacy {@code cue-ticks} is still accepted. Omitting tempo never inherits a packaged metronome. * - * @param section tool profile + * @param section tool profile, never {@code null} ({@link #readTool} handles a missing block) * @return cue ticks; {@code 0} means vanilla mining tempo */ private static int readTempo(ConfigurationSection section) { - if (section == null) { - return 0; - } if (section.contains("tempo")) { Object raw = section.get("tempo"); if (raw instanceof Number number) { return Math.max(0, number.intValue()); } - if (raw != null) { - String token = raw.toString().trim(); - if (token.isEmpty() - || token.equalsIgnoreCase("vanilla") - || token.equalsIgnoreCase("default") - || token.equalsIgnoreCase("auto")) { - return 0; - } - try { - return Math.max(0, Integer.parseInt(token)); - } catch (NumberFormatException ignored) { - return 0; - } + String token = raw.toString().trim(); + if (token.isEmpty() + || token.equalsIgnoreCase("vanilla") + || token.equalsIgnoreCase("default") + || token.equalsIgnoreCase("auto")) { + return 0; + } + try { + return Math.max(0, Integer.parseInt(token)); + } catch (NumberFormatException ignored) { + return 0; } } Integer legacy = sectionIntOrNull( @@ -1263,14 +1246,11 @@ private static boolean firstBool(ConfigurationSection first, ConfigurationSectio /** * First defined string list among {@code keys} on {@code section}. * - * @param section YAML map, or {@code null} + * @param section YAML map * @param keys preference order * @return list, or empty when none is present */ private static List firstStringList(ConfigurationSection section, String... keys) { - if (section == null || keys == null) { - return List.of(); - } for (String key : keys) { if (section.contains(key)) { return section.getStringList(key); @@ -1282,15 +1262,12 @@ private static List firstStringList(ConfigurationSection section, String /** * First defined double among {@code keys} on {@code section}. * - * @param section YAML map, or {@code null} + * @param section YAML map * @param fallback when none is present * @param keys preference order * @return value */ private static double firstDouble(ConfigurationSection section, double fallback, String... keys) { - if (section == null || keys == null) { - return fallback; - } for (String key : keys) { if (section.contains(key)) { return section.getDouble(key); @@ -1317,15 +1294,12 @@ private static int sectionInt(ConfigurationSection section, int fallback, String * @return first present float, or {@code fallback} */ private static Float sectionFloat(ConfigurationSection section, Float fallback, String... keys) { - if (section == null) { - return fallback; - } for (String key : keys) { if (!section.contains(key) || section.isConfigurationSection(key)) { continue; } String raw = section.getString(key); - if (raw == null || raw.isBlank()) { + if (raw.isBlank()) { return null; } return (float) section.getDouble(key); @@ -1357,9 +1331,6 @@ private static Integer sectionIntOrNull(ConfigurationSection section, String... * @return double value */ private static double sectionDouble(ConfigurationSection section, double fallback, String... keys) { - if (section == null) { - return fallback; - } for (String key : keys) { if (section.contains(key)) { return section.getDouble(key); @@ -1374,9 +1345,6 @@ private static double sectionDouble(ConfigurationSection section, double fallbac * @return first present string, or {@code null} */ private static String sectionString(ConfigurationSection section, String... keys) { - if (section == null) { - return null; - } for (String key : keys) { if (section.contains(key) && !section.isConfigurationSection(key)) { return section.getString(key); @@ -1496,11 +1464,11 @@ private static double readUnitInterval(ConfigurationSection section, String key, if (!section.contains(key)) { return fallback; } - if (section.isDouble(key) || section.isInt(key) || section.isLong(key)) { + if (section.get(key) instanceof Number) { return section.getDouble(key); } String raw = section.getString(key); - if (raw == null || raw.isBlank()) { + if (raw.isBlank()) { return fallback; } try { @@ -1579,7 +1547,7 @@ private LabSettings loadLab(ConfigurationSection section, LabSettings fallback) * @return packaged wipe sound */ private static org.bukkit.Sound defaultToolSound(String id) { - return switch (id == null ? "" : id.toLowerCase(java.util.Locale.ROOT)) { + return switch (id.toLowerCase(java.util.Locale.ROOT)) { case "water" -> org.bukkit.Sound.ITEM_BUCKET_EMPTY; case "air" -> org.bukkit.Sound.ITEM_BRUSH_BRUSHING_SAND; case "brush" -> org.bukkit.Sound.ITEM_BRUSH_BRUSHING_GENERIC; @@ -1592,7 +1560,7 @@ private static org.bukkit.Sound defaultToolSound(String id) { * @return packaged clean pane */ private static Material defaultCleanGlass(String id) { - return switch (id == null ? "" : id.toLowerCase(java.util.Locale.ROOT)) { + return switch (id.toLowerCase(java.util.Locale.ROOT)) { case "metal" -> Material.GRAY_STAINED_GLASS_PANE; case "organic" -> Material.LIME_STAINED_GLASS_PANE; case "stone" -> Material.LIGHT_GRAY_STAINED_GLASS_PANE; @@ -1605,7 +1573,7 @@ private static Material defaultCleanGlass(String id) { * @return packaged stain ids for that material */ private static List defaultStains(String id) { - return switch (id == null ? "" : id.toLowerCase(java.util.Locale.ROOT)) { + return switch (id.toLowerCase(java.util.Locale.ROOT)) { case "ceramic" -> List.of("limescale", "soil"); case "stone" -> List.of("limescale"); case "metal" -> List.of("rust"); @@ -1621,7 +1589,7 @@ private static List defaultStains(String id) { */ private void loadStaffPermission(org.bukkit.configuration.file.FileConfiguration config) { String node = config.getString("permissions.staff", "archaeo.admin"); - staffPermission = node == null || node.isBlank() ? "archaeo.admin" : node.trim(); + staffPermission = node.isBlank() ? "archaeo.admin" : node.trim(); } /** @@ -1635,7 +1603,7 @@ private void loadRarities(FileConfiguration config) { ConfigurationSection root = config.getConfigurationSection("rarity"); if (root != null) { for (String id : root.getKeys(false)) { - if (id == null || id.isBlank()) { + if (id.isBlank()) { continue; } String key = id.trim().toLowerCase(Locale.ROOT); @@ -1648,7 +1616,7 @@ private void loadRarities(FileConfiguration config) { continue; } String label = section.getString("label", fallback.label()); - if (label == null || label.isBlank()) { + if (label.isBlank()) { label = fallback.label(); } ChatColor color = parseChatColor(section.getString("color"), fallback.color()); @@ -1672,7 +1640,7 @@ private void loadRarityFromWeight(FileConfiguration config) { } List bands = new ArrayList<>(); for (String id : root.getKeys(false)) { - if (id == null || id.isBlank()) { + if (id.isBlank()) { continue; } String key = id.trim().toLowerCase(Locale.ROOT); @@ -1735,10 +1703,11 @@ private static ChatColor parseChatColor(String raw, ChatColor fallback) { * * @param interest parsed interest file * @param config root plugin config + * @return every interest level * @throws IllegalStateException if an interest-levels block is missing */ - private void loadInterests(YamlConfiguration interest, FileConfiguration config) { - interests.clear(); + private static Map readInterests(YamlConfiguration interest, FileConfiguration config) { + Map interests = new EnumMap<>(InterestLevel.class); ConfigurationSection root = sectionOr(interest, config, "interest-levels"); if (root == null) { throw new IllegalStateException("Missing interest-levels in interest.yml"); @@ -1762,15 +1731,18 @@ private void loadInterests(YamlConfiguration interest, FileConfiguration config) section.getDouble("disturbed-chance") )); } + return interests; } /** * Reads stratum depth bands from {@code strata.yml}. * * @param yaml parsed strata file + * @return stratum rows in file order + * @throws IllegalStateException if {@code strata} is missing */ - private void loadStrata(YamlConfiguration yaml) { - strata.clear(); + private static Map readStrata(YamlConfiguration yaml) { + Map strata = new LinkedHashMap<>(); ConfigurationSection root = yaml.getConfigurationSection("strata"); if (root == null) { throw new IllegalStateException("Missing strata in strata.yml"); @@ -1789,15 +1761,18 @@ private void loadStrata(YamlConfiguration yaml) { section.getBoolean("always-present", true) )); } + return strata; } /** * Reads find templates (size, tags, relic flag) from {@code artifacts.yml}. * * @param yaml parsed artifacts file + * @return artifact rows in file order + * @throws IllegalStateException if {@code artifacts} is missing */ - private void loadArtifacts(YamlConfiguration yaml) { - artifacts.clear(); + private Map readArtifacts(YamlConfiguration yaml) { + Map artifacts = new LinkedHashMap<>(); ConfigurationSection root = yaml.getConfigurationSection("artifacts"); if (root == null) { throw new IllegalStateException("Missing artifacts in artifacts.yml"); @@ -1823,6 +1798,7 @@ private void loadArtifacts(YamlConfiguration yaml) { section.getString("study-notes", "") )); } + return artifacts; } /** @@ -2004,9 +1980,11 @@ private void loadMaterials(YamlConfiguration yaml) { * Reads site hint texts and filter rules from {@code hints.yml}. * * @param yaml parsed hints file + * @return hint rows in file order + * @throws IllegalStateException if {@code hints} is missing */ - private void loadHints(YamlConfiguration yaml) { - hints.clear(); + private static Map readHints(YamlConfiguration yaml) { + Map hints = new LinkedHashMap<>(); ConfigurationSection root = yaml.getConfigurationSection("hints"); if (root == null) { throw new IllegalStateException("Missing hints in hints.yml"); @@ -2031,6 +2009,7 @@ private void loadHints(YamlConfiguration yaml) { section.contains("require-disturbed") ? section.getBoolean("require-disturbed") : null )); } + return hints; } /** @@ -2044,13 +2023,11 @@ private static ConfigurationSection sectionOr( FileConfiguration fallback, String path ) { - if (preferred != null) { - ConfigurationSection section = preferred.getConfigurationSection(path); - if (section != null) { - return section; - } + ConfigurationSection section = preferred.getConfigurationSection(path); + if (section != null) { + return section; } - return fallback == null ? null : fallback.getConfigurationSection(path); + return fallback.getConfigurationSection(path); } /** diff --git a/src/main/java/net/tfminecraft/archaeo/config/ConfigEnums.java b/src/main/java/net/tfminecraft/archaeo/config/ConfigEnums.java index 9754bb3..771e183 100644 --- a/src/main/java/net/tfminecraft/archaeo/config/ConfigEnums.java +++ b/src/main/java/net/tfminecraft/archaeo/config/ConfigEnums.java @@ -92,7 +92,6 @@ private static String fallbackKey(Sound sound) { if (sound == null) { return "none"; } - NamespacedKey key = sound.getKey(); - return key == null ? sound.toString() : key.toString(); + return sound.getKey().toString(); } } diff --git a/src/main/java/net/tfminecraft/archaeo/config/InterpretationTemplate.java b/src/main/java/net/tfminecraft/archaeo/config/InterpretationTemplate.java index 3d3a995..f6e6a4b 100644 --- a/src/main/java/net/tfminecraft/archaeo/config/InterpretationTemplate.java +++ b/src/main/java/net/tfminecraft/archaeo/config/InterpretationTemplate.java @@ -63,7 +63,7 @@ public boolean suggestedFor(String artifactId) { * @return whether this phrase may be offered */ public boolean appliesTo(FindProfile profile) { - if (profiles == null || profiles.isEmpty()) { + if (profiles.isEmpty()) { return true; } return profile != null && profiles.contains(profile); diff --git a/src/main/java/net/tfminecraft/archaeo/config/LabStain.java b/src/main/java/net/tfminecraft/archaeo/config/LabStain.java index 0193d93..21039d5 100644 --- a/src/main/java/net/tfminecraft/archaeo/config/LabStain.java +++ b/src/main/java/net/tfminecraft/archaeo/config/LabStain.java @@ -14,38 +14,38 @@ * @param tool rack tool id that wipes it */ public record LabStain(String id, String displayName, Material glass, String tool) { + // Rows come from LabSettings#defaults or CatalogRegistry#loadLab, which never leave a field + // null and never pick air for the pane (ConfigEnums#material falls back instead). + /** - * @param toolId cursor tool - * @return whether that tool wipes this stain + * @param toolId cursor tool, or {@code null} when the cursor is not a rack tool + * @return whether that tool wipes this stain; a blank {@code tool} accepts any cursor */ public boolean allowsTool(String toolId) { - if (tool == null || tool.isBlank()) { + if (tool.isBlank()) { return true; } - return tool.equalsIgnoreCase(toolId == null ? "" : toolId); + return tool.equalsIgnoreCase(toolId); } /** * @return pane used on the field */ public Material pane() { - return glass == null || glass.isAir() ? Material.BROWN_STAINED_GLASS_PANE : glass; + return glass; } /** - * @return name shown on the pane + * @return name shown on the pane; a blank {@code display-name} shows the stain key */ public String label() { - if (displayName == null || displayName.isBlank()) { - return id == null || id.isBlank() ? "Dirt" : id; - } - return displayName; + return displayName.isBlank() ? id : displayName; } /** * @return rack tool key, lower case */ public String toolId() { - return tool == null ? "" : tool.trim().toLowerCase(Locale.ROOT); + return tool.trim().toLowerCase(Locale.ROOT); } } diff --git a/src/main/java/net/tfminecraft/archaeo/config/MuseumSettings.java b/src/main/java/net/tfminecraft/archaeo/config/MuseumSettings.java index b28ef52..04cc27c 100644 --- a/src/main/java/net/tfminecraft/archaeo/config/MuseumSettings.java +++ b/src/main/java/net/tfminecraft/archaeo/config/MuseumSettings.java @@ -4,7 +4,6 @@ import net.tfminecraft.archaeo.item.ItemRef; import org.bukkit.Material; import org.bukkit.block.Block; -import org.bukkit.block.Shelf; import org.bukkit.entity.ArmorStand; import org.bukkit.entity.Entity; import org.bukkit.entity.ItemDisplay; @@ -36,9 +35,6 @@ public static MuseumSettings defaults() { * @return whether that material is on the whitelist */ public boolean allowsVanilla(Material material) { - if (material == null) { - return false; - } if (allowsNamed(material.name())) { return true; } @@ -50,9 +46,6 @@ public boolean allowsVanilla(Material material) { * @return whether this frame kind is listed */ public boolean allowsFrame(ItemFrame frame) { - if (frame == null) { - return false; - } String name = frame.getType().name(); if ("GLOW_ITEM_FRAME".equals(name)) { return allowsNamed("GLOW_ITEM_FRAME"); @@ -61,11 +54,12 @@ public boolean allowsFrame(ItemFrame frame) { } /** - * @param entity armor stand, item display, or ItemsAdder furniture - * @param matcher pack lookups + * Vanilla entity supports. ItemsAdder furniture entities are matched by {@link #allowsSupport}. + * + * @param entity frame, armor stand, or item display * @return whether this support is listed */ - public boolean allowsEntity(Entity entity, ItemMatcher matcher) { + public boolean allowsEntity(Entity entity) { if (entity == null) { return false; } @@ -78,14 +72,6 @@ public boolean allowsEntity(Entity entity, ItemMatcher matcher) { if (entity instanceof ItemDisplay) { return allowsNamed("ITEM_DISPLAY"); } - if (matcher == null) { - return false; - } - for (ItemRef ref : displays) { - if (matcher.matchesEntity(entity, ref)) { - return true; - } - } return false; } @@ -102,19 +88,12 @@ public boolean allowsSupport( Block block, ItemMatcher matcher ) { - if (allowsEntity(entity, matcher)) { + if (allowsEntity(entity)) { return true; } - if (block != null) { - if (allowsVanilla(block.getType())) { - return true; - } - if (block.getState() instanceof Shelf && allowsNamed("SHELF")) { - return true; - } - } - if (matcher == null) { - return false; + // Every shelf block is a *_SHELF material, so allowsVanilla already covers SHELF. + if (block != null && allowsVanilla(block.getType())) { + return true; } for (ItemRef ref : displays) { if (ref.kind() != ItemRef.Kind.ITEMSADDER) { @@ -134,9 +113,6 @@ public boolean allowsSupport( * @return whether that name is listed */ private boolean allowsNamed(String name) { - if (name == null) { - return false; - } String wanted = name.toUpperCase(Locale.ROOT); for (ItemRef ref : displays) { if (ref.kind() == ItemRef.Kind.VANILLA && wanted.equalsIgnoreCase(ref.primary())) { diff --git a/src/main/java/net/tfminecraft/archaeo/establish/CampArchiveBook.java b/src/main/java/net/tfminecraft/archaeo/establish/CampArchiveBook.java index f60ed66..b94d655 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/CampArchiveBook.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/CampArchiveBook.java @@ -37,8 +37,7 @@ public CampArchiveBook(JavaPlugin plugin) { * @return written book that opens the archive GUI */ public ItemStack create(Player closer, Site site) { - String author = closer.getName() == null ? "Director" : closer.getName(); - return create(author, site); + return create(closer.getName(), site); } /** @@ -51,9 +50,6 @@ public ItemStack create(Player closer, Site site) { public ItemStack create(String authorName, Site site) { ItemStack stack = new ItemStack(Material.WRITTEN_BOOK); BookMeta meta = (BookMeta) stack.getItemMeta(); - if (meta == null) { - return stack; - } String title = RecoveredFindItem.siteName(site); if (title.length() > 32) { title = title.substring(0, 32); @@ -97,15 +93,10 @@ public ItemStack stampCopy(ItemStack original, ItemStack crafted) { ItemStack stamped = crafted.clone(); BookMeta from = (BookMeta) original.getItemMeta(); BookMeta to = (BookMeta) stamped.getItemMeta(); - if (from == null || to == null) { - return crafted; - } if (from.hasDisplayName()) { to.setDisplayName(from.getDisplayName()); } - if (from.hasLore()) { - to.setLore(from.getLore()); - } + to.setLore(from.getLore()); String siteId = from.getPersistentDataContainer().get(siteIdKey, PersistentDataType.STRING); to.getPersistentDataContainer().set(markerKey, PersistentDataType.BYTE, (byte) 1); if (siteId != null) { @@ -126,9 +117,6 @@ public void refresh(ItemStack stack, Site site) { return; } BookMeta meta = (BookMeta) stack.getItemMeta(); - if (meta == null) { - return; - } meta.setDisplayName(displayName(site)); meta.setLore(loreOf(site)); stack.setItemMeta(meta); diff --git a/src/main/java/net/tfminecraft/archaeo/establish/CampBoard.java b/src/main/java/net/tfminecraft/archaeo/establish/CampBoard.java index ef6ac6f..e681c5f 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/CampBoard.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/CampBoard.java @@ -81,20 +81,6 @@ public UUID siteId() { return siteId; } - /** - * @return whether this copy includes director actions - */ - public boolean director() { - return director; - } - - /** - * @return whether Close is on this copy - */ - public boolean canClose() { - return canClose; - } - /** * {@inheritDoc} */ @@ -330,19 +316,18 @@ private static String biomeLabel(Site site) { site.centerBlockX(), site.getSurfaceY(), site.centerBlockZ()); - NamespacedKey key = biomeKey(biome); - if (key == null) { - return "unknown"; - } - return titleCase(key.getKey().replace('_', ' ')); + return titleCase(biomeKey(biome).getKey().replace('_', ' ')); } catch (RuntimeException ignored) { return "unknown"; } } /** + * Prefers Spigot's {@code RegistryAware#getKeyOrNull}, which paper-api 1.21.10 does not + * have, so it is looked up reflectively; otherwise falls back to {@link Biome#getKey()}. + * * @param biome chunk biome - * @return namespaced key, or {@code null} + * @return namespaced key */ @SuppressWarnings("deprecation") private static NamespacedKey biomeKey(Biome biome) { @@ -352,19 +337,16 @@ private static NamespacedKey biomeKey(Biome biome) { return key; } } catch (ReflectiveOperationException ignored) { - // Paper 1.21.10: RegistryAware helpers are absent + // paper-api 1.21.10 has no RegistryAware#getKeyOrNull; use getKey below. } return biome.getKey(); } /** - * @param raw biome id with spaces + * @param raw biome key path with spaces; namespaced keys are never empty * @return each word capitalised */ private static String titleCase(String raw) { - if (raw == null || raw.isBlank()) { - return "unknown"; - } String[] words = raw.split(" "); StringBuilder text = new StringBuilder(); for (int i = 0; i < words.length; i++) { @@ -375,10 +357,7 @@ private static String titleCase(String raw) { if (text.length() > 0) { text.append(' '); } - text.append(Character.toUpperCase(word.charAt(0))); - if (word.length() > 1) { - text.append(word.substring(1)); - } + text.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1)); } return text.toString(); } @@ -451,7 +430,7 @@ private static String findCounts(Site site, String stratumId) { int lost = 0; int total = 0; for (BuriedFind find : site.getFinds()) { - if (find.getStratumId() == null || !find.getStratumId().equals(stratumId)) { + if (!stratumId.equals(find.getStratumId())) { continue; } total++; @@ -509,13 +488,11 @@ static String statusLabel(SiteStatus status) { private static ItemStack named(Material material, String name, String... lore) { ItemStack stack = new ItemStack(material); ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.setDisplayName(name); - if (lore.length > 0) { - meta.setLore(List.of(lore)); - } - stack.setItemMeta(meta); + meta.setDisplayName(name); + if (lore.length > 0) { + meta.setLore(List.of(lore)); } + stack.setItemMeta(meta); return stack; } } diff --git a/src/main/java/net/tfminecraft/archaeo/establish/CampClosure.java b/src/main/java/net/tfminecraft/archaeo/establish/CampClosure.java index aa0c4c8..979ab3f 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/CampClosure.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/CampClosure.java @@ -46,11 +46,7 @@ public CampArchiveBook archiveBook() { * @return whether the camp is now closed */ public boolean close(CommandSender closer, Site site) { - if (closer == null || site == null || !site.isCampLocked()) { - return false; - } - if (!site.closeCamp()) { - closer.sendMessage("This excavation cannot be closed."); + if (closer == null || site == null || !site.closeCamp()) { return false; } sites.save(site); @@ -71,16 +67,14 @@ public boolean close(CommandSender closer, Site site) { * @param site just closed */ private void deliverFieldBook(CommandSender closer, Site site) { - String author = closer instanceof Player player && player.getName() != null - ? player.getName() - : "Staff"; - ItemStack book = archiveBook.create(author, site); if (closer instanceof Player player) { - giveStack(player, book); + giveStack(player, archiveBook.create(player, site)); return; } + ItemStack book = archiveBook.create("Staff", site); World world = plugin.getServer().getWorld(site.getWorldName()); - if (world == null || site.getCampX() == null || site.getCampY() == null || site.getCampZ() == null) { + // The dossier stores the camp origin as one unit, so a present X means Y and Z are there too. + if (world == null || site.getCampX() == null) { closer.sendMessage("No field book could be given from console."); return; } diff --git a/src/main/java/net/tfminecraft/archaeo/establish/CampFindBoard.java b/src/main/java/net/tfminecraft/archaeo/establish/CampFindBoard.java index 0239313..79c9c07 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/CampFindBoard.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/CampFindBoard.java @@ -181,8 +181,9 @@ private ItemStack conditionItem(BuriedFind find) { List lore = new ArrayList<>(); if (RecoveredFindItem.conditionKnown(find)) { lore.add(ChatColor.GRAY + "Conservation: " + ChatColor.WHITE + find.getConservation() + "%" - + (grade == null || grade.isBlank() ? "" : ChatColor.GRAY + " · " + grade)); - } else if (find.getState() == FindState.RECOVERED) { + + (grade.isBlank() ? "" : ChatColor.GRAY + " · " + grade)); + } else { + // Register rows have left the cut, so a piece that is neither cleaned nor lost is recovered. lore.add(ChatColor.DARK_GRAY + "Clean the piece to read its condition."); } if (find.isDisturbedBeforeDig()) { @@ -223,13 +224,11 @@ private ItemStack readingsItem(BuriedFind find) { private static ItemStack named(Material material, String name, String... lore) { ItemStack stack = new ItemStack(material); ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.setDisplayName(name); - if (lore.length > 0) { - meta.setLore(List.of(lore)); - } - stack.setItemMeta(meta); + meta.setDisplayName(name); + if (lore.length > 0) { + meta.setLore(List.of(lore)); } + stack.setItemMeta(meta); return stack; } } diff --git a/src/main/java/net/tfminecraft/archaeo/establish/CampFindsBoard.java b/src/main/java/net/tfminecraft/archaeo/establish/CampFindsBoard.java index f2020bf..090a873 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/CampFindsBoard.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/CampFindsBoard.java @@ -68,17 +68,15 @@ public Inventory getInventory() { * @param site excavation */ public void open(Player player, Site site) { + List register = site.cataloguedFinds(); finds.clear(); - for (BuriedFind find : site.cataloguedFinds()) { + for (BuriedFind find : register) { finds.add(find.getId()); } inventory = Bukkit.createInventory(this, 27, "Finds"); - int shown = Math.min(finds.size(), LIST_SLOTS); + int shown = Math.min(register.size(), LIST_SLOTS); for (int i = 0; i < shown; i++) { - BuriedFind find = site.findById(finds.get(i)).orElse(null); - if (find != null) { - inventory.setItem(i, rowItem(site, find)); - } + inventory.setItem(i, rowItem(site, register.get(i))); } if (director && site.getStatus() == SiteStatus.EXHAUSTED) { inventory.setItem(SLOT_REPORT, named( @@ -161,13 +159,9 @@ private static ItemStack named(ItemStack base, String name, String... lore) { ItemStack stack = base == null ? new ItemStack(Material.BRICK) : base.clone(); stack.setAmount(1); ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.setDisplayName(name); - if (lore.length > 0) { - meta.setLore(List.of(lore)); - } - stack.setItemMeta(meta); - } + meta.setDisplayName(name); + meta.setLore(List.of(lore)); + stack.setItemMeta(meta); return stack; } } diff --git a/src/main/java/net/tfminecraft/archaeo/establish/CampIdentifyBoard.java b/src/main/java/net/tfminecraft/archaeo/establish/CampIdentifyBoard.java index 375ec28..22f83bb 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/CampIdentifyBoard.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/CampIdentifyBoard.java @@ -190,17 +190,17 @@ String offerAt(int slot) { /** * @param site excavation - * @param template artifact, or {@code null} + * @param template artifact, or {@code null}; the catalogue always loads tag sets, if empty * @return tags that only weight the draw */ private Set weightTags(Site site, ArtifactTemplate template) { Set tags = new HashSet<>(); - if (template != null && template.tags() != null) { + if (template != null) { tags.addAll(template.tags()); } for (String hintId : site.getHintIds()) { var hint = catalogs.hint(hintId); - if (hint != null && hint.tags() != null) { + if (hint != null) { tags.addAll(hint.tags()); } } @@ -228,13 +228,9 @@ private static ItemStack offerItem(InterpretationTemplate option) { private static ItemStack named(Material material, String name, String... lore) { ItemStack stack = new ItemStack(material); ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.setDisplayName(name); - if (lore.length > 0) { - meta.setLore(List.of(lore)); - } - stack.setItemMeta(meta); - } + meta.setDisplayName(name); + meta.setLore(List.of(lore)); + stack.setItemMeta(meta); return stack; } } diff --git a/src/main/java/net/tfminecraft/archaeo/establish/CampListener.java b/src/main/java/net/tfminecraft/archaeo/establish/CampListener.java index 67e5ba4..2a28520 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/CampListener.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/CampListener.java @@ -215,7 +215,7 @@ public void onInteract(PlayerInteractEvent event) { Action action = event.getAction(); Block block = event.getClickedBlock(); if (event.getHand() != EquipmentSlot.HAND) { - if (action == Action.RIGHT_CLICK_BLOCK && block != null && locked(block)) { + if (block != null && action == Action.RIGHT_CLICK_BLOCK && locked(block)) { denyUse(event); } return; @@ -229,20 +229,13 @@ public void onInteract(PlayerInteractEvent event) { if (action != Action.RIGHT_CLICK_BLOCK && action != Action.RIGHT_CLICK_AIR) { return; } - if (block != null && locked(block)) { + Site camp = block == null ? null : lockedCamp(block); + if (camp != null) { denyUse(event); - Site site = sites.findLockedCampBlock( - block.getWorld().getName(), - block.getX(), - block.getY(), - block.getZ()).orElse(null); - if (site == null) { - return; - } if (establish.isRelocating(event.getPlayer())) { establish.tryCancelMove(event.getPlayer()); } - openBoard(event.getPlayer(), site); + openBoard(event.getPlayer(), camp); return; } if (tryOpenArchive(event.getPlayer(), event.getItem(), event)) { @@ -352,16 +345,12 @@ public void onBoardClick(InventoryClickEvent event) { if (!(event.getInventory().getHolder() instanceof CampBoard board)) { return; } - event.setCancelled(true); - if (!(event.getWhoClicked() instanceof Player player)) { + Player player = topClicker(event); + if (player == null) { return; } - if (event.getClickedInventory() == null || event.getClickedInventory() != event.getView().getTopInventory()) { - return; - } - Site site = sites.findById(board.siteId()).orElse(null); - if (site == null || !site.mayConsult()) { - player.closeInventory(); + Site site = consultable(player, board.siteId()); + if (site == null) { return; } int slot = event.getRawSlot(); @@ -370,7 +359,7 @@ public void onBoardClick(InventoryClickEvent event) { return; } if (slot == CampBoard.SLOT_PERSONAL) { - openStaff(player, site, board.director()); + openStaff(player, site, staffing(player, site)); return; } if (slot == CampBoard.SLOT_LIMITS) { @@ -381,11 +370,11 @@ public void onBoardClick(InventoryClickEvent event) { outline.show(player, site); return; } - if (slot == CampBoard.SLOT_CLOSE && board.canClose()) { + if (slot == CampBoard.SLOT_CLOSE && canCloseCamp(player, site)) { beginClosePrompt(player, site); return; } - if (!board.director()) { + if (!staffing(player, site)) { return; } if (slot == CampBoard.SLOT_RENAME) { @@ -419,16 +408,12 @@ public void onStaffClick(InventoryClickEvent event) { if (!(event.getInventory().getHolder() instanceof CampStaffBoard staff)) { return; } - event.setCancelled(true); - if (!(event.getWhoClicked() instanceof Player player)) { - return; - } - if (event.getClickedInventory() == null || event.getClickedInventory() != event.getView().getTopInventory()) { + Player player = topClicker(event); + if (player == null) { return; } - Site site = sites.findById(staff.siteId()).orElse(null); - if (site == null || !site.mayConsult()) { - player.closeInventory(); + Site site = consultable(player, staff.siteId()); + if (site == null) { return; } boolean director = staffing(player, site); @@ -467,16 +452,12 @@ public void onWorkerClick(InventoryClickEvent event) { if (!(event.getInventory().getHolder() instanceof CampWorkerBoard file)) { return; } - event.setCancelled(true); - if (!(event.getWhoClicked() instanceof Player player)) { + Player player = topClicker(event); + if (player == null) { return; } - if (event.getClickedInventory() == null || event.getClickedInventory() != event.getView().getTopInventory()) { - return; - } - Site site = sites.findById(file.siteId()).orElse(null); - if (site == null || !site.mayConsult()) { - player.closeInventory(); + Site site = consultable(player, file.siteId()); + if (site == null) { return; } int slot = event.getRawSlot(); @@ -509,16 +490,12 @@ public void onFindsClick(InventoryClickEvent event) { if (!(event.getInventory().getHolder() instanceof CampFindsBoard board)) { return; } - event.setCancelled(true); - if (!(event.getWhoClicked() instanceof Player player)) { - return; - } - if (event.getClickedInventory() == null || event.getClickedInventory() != event.getView().getTopInventory()) { + Player player = topClicker(event); + if (player == null) { return; } - Site site = sites.findById(board.siteId()).orElse(null); - if (site == null || !site.mayConsult()) { - player.closeInventory(); + Site site = consultable(player, board.siteId()); + if (site == null) { return; } int slot = event.getRawSlot(); @@ -546,16 +523,12 @@ public void onFindFileClick(InventoryClickEvent event) { if (!(event.getInventory().getHolder() instanceof CampFindBoard file)) { return; } - event.setCancelled(true); - if (!(event.getWhoClicked() instanceof Player player)) { + Player player = topClicker(event); + if (player == null) { return; } - if (event.getClickedInventory() == null || event.getClickedInventory() != event.getView().getTopInventory()) { - return; - } - Site site = sites.findById(file.siteId()).orElse(null); - if (site == null || !site.mayConsult()) { - player.closeInventory(); + Site site = consultable(player, file.siteId()); + if (site == null) { return; } int slot = event.getRawSlot(); @@ -578,16 +551,12 @@ public void onIdentifyClick(InventoryClickEvent event) { if (!(event.getInventory().getHolder() instanceof CampIdentifyBoard board)) { return; } - event.setCancelled(true); - if (!(event.getWhoClicked() instanceof Player player)) { - return; - } - if (event.getClickedInventory() == null || event.getClickedInventory() != event.getView().getTopInventory()) { + Player player = topClicker(event); + if (player == null) { return; } - Site site = sites.findById(board.siteId()).orElse(null); - if (site == null || !site.mayConsult()) { - player.closeInventory(); + Site site = consultable(player, board.siteId()); + if (site == null) { return; } int slot = event.getRawSlot(); @@ -627,9 +596,6 @@ private void fileStationReading(Player player, Site site, CampIdentifyBoard boar player.sendMessage("You are not authorised to write this record."); return; } - if (board.type() == null) { - return; - } BuriedFind find = site.findById(board.findId()).orElse(null); if (find == null || find.getState() != FindState.RECOVERED) { player.sendMessage("That find cannot be identified."); @@ -746,19 +712,12 @@ public void onWoolPickerClick(InventoryClickEvent event) { if (!(event.getInventory().getHolder() instanceof CampWoolPicker picker)) { return; } - event.setCancelled(true); - if (!(event.getWhoClicked() instanceof Player player)) { - return; - } - if (event.getClickedInventory() == null || event.getClickedInventory() != event.getView().getTopInventory()) { + Player player = topClicker(event); + if (player == null) { return; } Site site = sites.findById(picker.siteId()).orElse(null); - if (site == null || !site.isCampLocked()) { - player.closeInventory(); - return; - } - if (!site.isDirector(player.getUniqueId())) { + if (site == null || !staffing(player, site)) { player.closeInventory(); return; } @@ -798,7 +757,7 @@ public void onBoardDrag(InventoryDragEvent event) { @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) public void onChat(AsyncPlayerChatEvent event) { Player player = event.getPlayer(); - String raw = event.getMessage() == null ? "" : event.getMessage().trim(); + String raw = event.getMessage().trim(); if (establish.isRelocating(player) && raw.equalsIgnoreCase("cancel")) { event.setCancelled(true); plugin.getServer().getScheduler().runTask(plugin, () -> establish.tryCancelMove(player)); @@ -842,7 +801,7 @@ private void handleInviteChat(Player player, String raw) { return; } OfflinePlayer target = CampNames.known(raw); - if (target == null || target.getUniqueId() == null) { + if (target == null) { player.sendMessage("No player with that name has joined this server."); inviteForSite.put(player.getUniqueId(), siteId); return; @@ -859,10 +818,7 @@ private void handleInviteChat(Player player, String raw) { openStaff(player, site, true); return; } - if (!site.grantExcavator(target.getUniqueId())) { - player.sendMessage("Could not add that player."); - return; - } + site.grantExcavator(target.getUniqueId()); sites.save(site); String added = CampNames.of(player, target.getUniqueId()); player.sendMessage("Added " + added + " to the excavation staff."); @@ -887,9 +843,7 @@ private void openStaff(Player player, Site site, boolean director) { * @param site excavation */ private void openBoard(Player player, Site site) { - if (player.getWorld() != null) { - handPick.ensureJornada(site, player.getWorld()); - } + handPick.ensureJornada(site, player.getWorld()); new CampBoard(site.getId(), staffing(player, site), canCloseCamp(player, site), catalogs) .open(player, site); } @@ -940,20 +894,6 @@ private void handleCloseChat(Player player, String raw) { player.sendMessage("That excavation can no longer be closed."); return; } - closeCamp(player, site); - } - - /** - * Unlocks the camp and gives the closer a field book that still opens this record. - * - * @param player director or server staff - * @param site standing camp - */ - private void closeCamp(Player player, Site site) { - if (!canCloseCamp(player, site)) { - player.sendMessage("Only the director or server staff may close this excavation."); - return; - } campClosure.close(player, site); } @@ -981,6 +921,38 @@ private boolean tryOpenArchive(Player player, ItemStack stack, PlayerInteractEve return true; } + /** + * Cancels every click while a camp window is open, so no item can be moved in or out. + * + * @param event click inside a camp window + * @return the player who clicked the window itself, or {@code null} for their own inventory + * or the gap outside the window + */ + private static Player topClicker(InventoryClickEvent event) { + event.setCancelled(true); + if (event.getClickedInventory() == null || event.getClickedInventory() != event.getView().getTopInventory()) { + return null; + } + // Only players can hold an inventory view open on a Paper server. + return (Player) event.getWhoClicked(); + } + + /** + * Re-reads the record a window was drawn from and closes the window when it is gone. + * + * @param player viewer + * @param siteId excavation the window belongs to + * @return the record if it may still be read, otherwise {@code null} + */ + private Site consultable(Player player, UUID siteId) { + Site site = sites.findById(siteId).orElse(null); + if (site == null || !site.mayConsult()) { + player.closeInventory(); + return null; + } + return site; + } + /** * @param player viewer * @param site excavation @@ -1028,11 +1000,19 @@ private void giveStack(Player player, ItemStack stack) { * @return whether it belongs to a locked camp */ private boolean locked(Block block) { + return lockedCamp(block) != null; + } + + /** + * @param block world block + * @return locked camp owning it, or {@code null} + */ + private Site lockedCamp(Block block) { return sites.findLockedCampBlock( block.getWorld().getName(), block.getX(), block.getY(), - block.getZ()).isPresent(); + block.getZ()).orElse(null); } /** diff --git a/src/main/java/net/tfminecraft/archaeo/establish/CampNames.java b/src/main/java/net/tfminecraft/archaeo/establish/CampNames.java index 01195ae..b6dd381 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/CampNames.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/CampNames.java @@ -48,9 +48,9 @@ public static String of(Player viewer, UUID id) { if (online != null) { return online.getName(); } - OfflinePlayer offline = Bukkit.getOfflinePlayer(id); - String name = offline.getName(); - if (name != null && !name.isBlank()) { + // Paper and Spigot answer a real name or null here, never a blank one. + String name = Bukkit.getOfflinePlayer(id).getName(); + if (name != null) { return name; } return id.toString().substring(0, 8); @@ -59,11 +59,11 @@ public static String of(Player viewer, UUID id) { /** * Matches an online player or someone who has already joined this server. * - * @param raw chat name + * @param raw chat or command name, never {@code null} * @return known player, or {@code null} */ public static OfflinePlayer known(String raw) { - if (raw == null || raw.isBlank()) { + if (raw.isBlank()) { return null; } String name = raw.trim(); diff --git a/src/main/java/net/tfminecraft/archaeo/establish/CampStaffBoard.java b/src/main/java/net/tfminecraft/archaeo/establish/CampStaffBoard.java index 614400a..1ac35d1 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/CampStaffBoard.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/CampStaffBoard.java @@ -54,13 +54,6 @@ public UUID siteId() { return siteId; } - /** - * @return whether this copy includes add/remove - */ - public boolean director() { - return director; - } - /** * {@inheritDoc} */ @@ -137,9 +130,6 @@ private ItemStack addItem() { private ItemStack head(Player viewer, Site site, UUID member) { ItemStack stack = new ItemStack(Material.PLAYER_HEAD); SkullMeta meta = (SkullMeta) stack.getItemMeta(); - if (meta == null) { - return stack; - } OfflinePlayer owner = Bukkit.getOfflinePlayer(member); meta.setOwningPlayer(owner); SiteRole role = site.roleOf(member); @@ -162,13 +152,9 @@ private ItemStack head(Player viewer, Site site, UUID member) { private static ItemStack named(Material material, String name, String... lore) { ItemStack stack = new ItemStack(material); ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.setDisplayName(name); - if (lore.length > 0) { - meta.setLore(List.of(lore)); - } - stack.setItemMeta(meta); - } + meta.setDisplayName(name); + meta.setLore(List.of(lore)); + stack.setItemMeta(meta); return stack; } } diff --git a/src/main/java/net/tfminecraft/archaeo/establish/CampTemplate.java b/src/main/java/net/tfminecraft/archaeo/establish/CampTemplate.java index 8aac26c..c2f8130 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/CampTemplate.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/CampTemplate.java @@ -213,24 +213,23 @@ public static int clampOrigin(int desired, int minOff, int maxOff, int chunkMin, } /** + * The sign is the template's only rotatable piece and the campfire its only lightable one. + * * @param material piece type - * @param facing camp front (used when the block is directional) + * @param facing camp front, one of the four cardinals from {@link #facingFromYaw(float)} * @return block data for {@link org.bukkit.entity.Player#sendBlockChange} */ public static BlockData dataFor(Material material, BlockFace facing) { BlockData data = material.createBlockData(); - if (material == Material.OAK_SIGN && data instanceof Rotatable rotatable) { - BlockFace towardPlayer = facing.getOppositeFace(); - if (towardPlayer.isCartesian() && towardPlayer.getModY() == 0) { - rotatable.setRotation(towardPlayer); - } - } else if (data instanceof Directional directional && facing.isCartesian() && facing.getModY() == 0) { + if (data instanceof Rotatable rotatable) { + rotatable.setRotation(facing.getOppositeFace()); + } else if (data instanceof Directional directional) { directional.setFacing(facing); } if (data instanceof Slab slab) { slab.setType(Slab.Type.BOTTOM); } - if (data instanceof Lightable lightable && material == Material.CAMPFIRE) { + if (data instanceof Lightable lightable) { lightable.setLit(true); } return data; diff --git a/src/main/java/net/tfminecraft/archaeo/establish/CampWoolPicker.java b/src/main/java/net/tfminecraft/archaeo/establish/CampWoolPicker.java index 6d801ec..932afb3 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/CampWoolPicker.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/CampWoolPicker.java @@ -107,13 +107,9 @@ static DyeColor colorAt(int slot) { private static ItemStack named(Material material, String name, String... lore) { ItemStack stack = new ItemStack(material); ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.setDisplayName(name); - if (lore.length > 0) { - meta.setLore(List.of(lore)); - } - stack.setItemMeta(meta); - } + meta.setDisplayName(name); + meta.setLore(List.of(lore)); + stack.setItemMeta(meta); return stack; } } diff --git a/src/main/java/net/tfminecraft/archaeo/establish/CampWools.java b/src/main/java/net/tfminecraft/archaeo/establish/CampWools.java index c529691..281e930 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/CampWools.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/CampWools.java @@ -35,13 +35,8 @@ public static Material woolOf(String name) { * @return wool block for that dye */ public static Material woolOf(String name, DyeColor fallback) { - DyeColor color = parse(name, fallback); - Material material = Material.matchMaterial(color.name() + "_WOOL"); - if (material != null) { - return material; - } - Material fallbackWool = Material.matchMaterial(fallback.name() + "_WOOL"); - return fallbackWool != null ? fallbackWool : Material.RED_WOOL; + // Every dye colour has a matching wool block. + return Material.valueOf(parse(name, fallback).name() + "_WOOL"); } /** diff --git a/src/main/java/net/tfminecraft/archaeo/establish/CampWorkerBoard.java b/src/main/java/net/tfminecraft/archaeo/establish/CampWorkerBoard.java index 6028f28..ae2b39f 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/CampWorkerBoard.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/CampWorkerBoard.java @@ -70,13 +70,6 @@ public UUID member() { return member; } - /** - * @return whether this copy includes role buttons and the dismissal - */ - public boolean director() { - return director; - } - /** * {@inheritDoc} */ @@ -91,11 +84,8 @@ public Inventory getInventory() { */ public void open(Player player, Site site) { String name = CampNames.of(player, member); - String title = name; - if (title.length() > 32) { - title = title.substring(0, 32); - } - inventory = Bukkit.createInventory(this, 27, title); + // Player names and the short-id fallback both fit the 32-character title limit. + inventory = Bukkit.createInventory(this, 27, name); ItemStack filler = named(Material.GRAY_STAINED_GLASS_PANE, " "); for (int slot = 0; slot < 27; slot++) { inventory.setItem(slot, filler); @@ -161,9 +151,6 @@ static SiteRole roleAt(int slot) { private ItemStack headItem(String name, SiteRole role, WorkerRecord record) { ItemStack stack = new ItemStack(Material.PLAYER_HEAD); SkullMeta meta = (SkullMeta) stack.getItemMeta(); - if (meta == null) { - return stack; - } OfflinePlayer owner = Bukkit.getOfflinePlayer(member); meta.setOwningPlayer(owner); meta.setDisplayName(ChatColor.WHITE + name); @@ -346,16 +333,14 @@ private static ItemStack named(Material material, String name, String... lore) { private static ItemStack named(Material material, String name, boolean glint, String... lore) { ItemStack stack = new ItemStack(material); ItemMeta meta = stack.getItemMeta(); - if (meta != null) { - meta.setDisplayName(name); - if (lore.length > 0) { - meta.setLore(List.of(lore)); - } - if (glint) { - meta.setEnchantmentGlintOverride(true); - } - stack.setItemMeta(meta); + meta.setDisplayName(name); + if (lore.length > 0) { + meta.setLore(List.of(lore)); } + if (glint) { + meta.setEnchantmentGlintOverride(true); + } + stack.setItemMeta(meta); return stack; } } diff --git a/src/main/java/net/tfminecraft/archaeo/establish/EstablishListener.java b/src/main/java/net/tfminecraft/archaeo/establish/EstablishListener.java index b5ab579..afd2e6e 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/EstablishListener.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/EstablishListener.java @@ -1,6 +1,8 @@ package net.tfminecraft.archaeo.establish; import net.tfminecraft.archaeo.item.EstablishItem; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; @@ -11,12 +13,18 @@ import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.inventory.EquipmentSlot; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + /** * Routes establishment-kit clicks into {@link EstablishService}. */ public class EstablishListener implements Listener { private final EstablishItem item; private final EstablishService service; + /** Server tick of each player's last main-hand kit click, until the matching off-hand event. */ + private final Map mainHandKitTick = new HashMap<>(); /** * @param item kit recognition @@ -29,17 +37,30 @@ public EstablishListener(EstablishItem item, EstablishService service) { /** * Right-click plants; sneak+left-click cycles wool on first plant; left-click aborts a camp move. + * The server fires one event per hand, so a kit held only in the off hand arrives on the + * off-hand event. When the main hand held a kit for the same click, the main-hand event has + * already acted, even if it spent the last kit there. * * @param event interact event */ @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = false) public void onInteract(PlayerInteractEvent event) { - if (event.getHand() != EquipmentSlot.HAND) { - return; + Player player = event.getPlayer(); + if (event.getHand() == EquipmentSlot.OFF_HAND) { + // Same tick-key as wool cycling. Removing the mark here keeps it from outliving the click. + Integer mainTick = mainHandKitTick.remove(player.getUniqueId()); + if (mainTick != null && mainTick == Bukkit.getCurrentTick()) { + return; + } } if (!item.isEstablish(event.getItem())) { return; } + if (event.getHand() == EquipmentSlot.HAND) { + mainHandKitTick.put(player.getUniqueId(), Bukkit.getCurrentTick()); + } else if (item.isEstablish(player.getInventory().getItemInMainHand())) { + return; + } Action action = event.getAction(); if (action == Action.LEFT_CLICK_AIR || action == Action.LEFT_CLICK_BLOCK) { if (service.tryCancelMove(event.getPlayer())) { @@ -92,6 +113,7 @@ public void onHeld(PlayerItemHeldEvent event) { */ @EventHandler public void onQuit(PlayerQuitEvent event) { + mainHandKitTick.remove(event.getPlayer().getUniqueId()); service.clearSession(event.getPlayer()); service.clearPreview(event.getPlayer()); service.hideHud(event.getPlayer()); diff --git a/src/main/java/net/tfminecraft/archaeo/establish/EstablishService.java b/src/main/java/net/tfminecraft/archaeo/establish/EstablishService.java index 3b84ab7..299a3e0 100644 --- a/src/main/java/net/tfminecraft/archaeo/establish/EstablishService.java +++ b/src/main/java/net/tfminecraft/archaeo/establish/EstablishService.java @@ -9,6 +9,7 @@ import net.tfminecraft.archaeo.model.Site; import net.tfminecraft.archaeo.model.SiteStatus; import net.tfminecraft.archaeo.site.SiteRepository; +import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Chunk; import org.bukkit.DyeColor; @@ -56,10 +57,9 @@ public class EstablishService { private final Map renameForSite = new ConcurrentHashMap<>(); private final Map woolChoicePrimary = new ConcurrentHashMap<>(); private final Map woolChoiceSecondary = new ConcurrentHashMap<>(); - private final Map woolCycleTick = new ConcurrentHashMap<>(); + private final Map woolCycleTick = new ConcurrentHashMap<>(); private final Map moveAimProxies = new ConcurrentHashMap<>(); - private final Map lastPulse = new ConcurrentHashMap<>(); - private final Map lastPulseSite = new ConcurrentHashMap<>(); + private final Map lastPulse = new ConcurrentHashMap<>(); private FindDustService findDust; private SiteLabelRefresh labels; @@ -126,26 +126,13 @@ public void stop() { } previews.clear(); lastPulse.clear(); - lastPulseSite.clear(); for (UUID playerId : new HashSet<>(moveAimProxies.keySet())) { - Player player = plugin.getServer().getPlayer(playerId); - if (player != null) { - removeMoveAimProxy(player); - } else { - removeMoveAimProxy(playerId); - } + removeMoveAimProxy(playerId); } - for (UUID playerId : new HashSet<>(bars.keySet())) { - Player player = plugin.getServer().getPlayer(playerId); - if (player != null) { - hideHud(player); - } else { - BossBar bar = bars.remove(playerId); - if (bar != null) { - bar.removeAll(); - } - } + for (BossBar bar : bars.values()) { + bar.removeAll(); } + bars.clear(); } /** @@ -197,8 +184,7 @@ public void pulse() { */ private void pulsePlacement(Player player, Site site, boolean moving) { CampPlacement placement = assess(player, site, aimBlock(player, site)); - lastPulse.put(player.getUniqueId(), placement); - lastPulseSite.put(player.getUniqueId(), site.getId()); + lastPulse.put(player.getUniqueId(), new Pulse(site.getId(), placement)); applyGhosts(player, site, placement); if (placement.allowed()) { String action = moving @@ -264,7 +250,7 @@ public boolean tryFinishMove(Player player) { */ public boolean tryFinishMoveOnAimProxy(Player player, Entity clicked) { UUID standId = moveAimProxies.get(player.getUniqueId()); - if (standId == null || clicked == null || !standId.equals(clicked.getUniqueId())) { + if (standId == null || !standId.equals(clicked.getUniqueId())) { return false; } tryFinishMove(player); @@ -280,7 +266,7 @@ public boolean tryFinishMoveOnAimProxy(Player player, Entity clicked) { */ public boolean handleMoveAimProxyAttack(Player player, Entity clicked) { UUID standId = moveAimProxies.get(player.getUniqueId()); - if (standId == null || clicked == null || !standId.equals(clicked.getUniqueId())) { + if (standId == null || !standId.equals(clicked.getUniqueId())) { return false; } tryCancelMove(player); @@ -349,7 +335,6 @@ public boolean handleRenameChat(Player player, String raw) { } if (raw.equalsIgnoreCase("cancel")) { player.sendMessage("Rename cancelled."); - renameForSite.remove(player.getUniqueId()); return true; } Site site = sites.findById(siteId).orElse(null); @@ -357,7 +342,7 @@ public boolean handleRenameChat(Player player, String raw) { player.sendMessage("That excavation is no longer active."); return true; } - if (site.getDirector() == null || !site.getDirector().equals(player.getUniqueId())) { + if (!site.isDirector(player.getUniqueId())) { return true; } String name = raw.replace('§', ' ').trim(); @@ -423,12 +408,13 @@ public void cycleWool(Player player) { if (isRelocating(player)) { return; } - long tick = player.getWorld().getFullTime(); - Long previous = woolCycleTick.put(player.getUniqueId(), tick); + // The server tick, not World#getFullTime: the day clock stands still when doDaylightCycle is off. + int tick = Bukkit.getCurrentTick(); + Integer previous = woolCycleTick.put(player.getUniqueId(), tick); if (previous != null && previous == tick) { return; } - Site site = activeSite(player); + Site site = nearestConfirmedHidden(player); String current = secondaryWoolName(player, site); String next = CampWools.next(current).name(); woolChoiceSecondary.put(player.getUniqueId(), next); @@ -446,7 +432,7 @@ public void cycleWool(Player player) { * @param role primary ({@code W}) or secondary ({@code R}) */ public void applyCampWool(Player player, Site site, DyeColor color, CampWoolRole role) { - if (site.getDirector() == null || !site.getDirector().equals(player.getUniqueId())) { + if (!site.isDirector(player.getUniqueId())) { return; } DyeColor fallback = role == CampWoolRole.PRIMARY ? DyeColor.WHITE : DyeColor.RED; @@ -474,11 +460,8 @@ public void applyCampWool(Player player, Site site, DyeColor color, CampWoolRole * @param to new wool */ private void recolorWoolCells(World world, Site site, CampWoolRole role, Material to) { - Integer originX = site.getCampX(); - Integer originY = site.getCampY(); - Integer originZ = site.getCampZ(); BlockFace facing = campFacing(site); - if (originX == null || originY == null || originZ == null || facing == null) { + if (facing == null) { DyeColor fallback = role == CampWoolRole.PRIMARY ? DyeColor.WHITE : DyeColor.RED; String stored = role == CampWoolRole.PRIMARY ? site.getCampWoolPrimary() : site.getCampWoolSecondary(); Material from = CampWools.woolOf(stored, fallback); @@ -499,20 +482,24 @@ private void recolorWoolCells(World world, Site site, CampWoolRole role, Materia } Vector offset = CampTemplate.rotate(piece.dx(), piece.dz(), facing); world.getBlockAt( - originX + offset.getBlockX(), - originY + piece.dy(), - originZ + offset.getBlockZ() + site.getCampX() + offset.getBlockX(), + site.getCampY() + piece.dy(), + site.getCampZ() + offset.getBlockZ() ).setType(to, false); } } /** * Stored facing, or inferred from the sign cell of the tent grid ({@code N} at local −2, −2). + * The dossier stores the three origin coordinates together, so a present X means a full origin. * * @param site excavation - * @return cardinal, or {@code null} + * @return cardinal, or {@code null} when the camp origin or its orientation is unknown */ private BlockFace campFacing(Site site) { + if (site.getCampX() == null) { + return null; + } if (site.getCampFacing() != null) { try { BlockFace stored = BlockFace.valueOf(site.getCampFacing()); @@ -523,7 +510,7 @@ private BlockFace campFacing(Site site) { // fall through to sign inference } } - if (site.getCampX() == null || site.getCampSignX() == null) { + if (site.getCampSignX() == null) { return null; } int dx = site.getCampSignX() - site.getCampX(); @@ -547,9 +534,6 @@ private BlockFace campFacing(Site site) { * @param siteId excavation being erased */ public void abortSessionsFor(UUID siteId) { - if (siteId == null) { - return; - } for (Player player : plugin.getServer().getOnlinePlayers()) { if (siteId.equals(relocating.get(player.getUniqueId()))) { cancelRelocate(player); @@ -618,10 +602,6 @@ private void tryPlace(Player player, Site site, boolean moving) { player.sendMessage(placement.reason()); return; } - if (!moving && atDirectorCap(player)) { - player.sendMessage(directorCapHint()); - return; - } clearPreview(player); World world = player.getWorld(); if (moving) { @@ -667,17 +647,16 @@ private void tryPlace(Player player, Site site, boolean moving) { consumeOne(player); } if (!moving) { - noteMissingTerrain(player, site); + noteMissingTerrain(player, world, site); } sites.save(site); if (findDust != null) { findDust.syncTimer(); } - if (site.getCampSignX() != null) { - plugin.getServer().getScheduler().runTask(plugin, () -> CampSigns.write( - world.getBlockAt(site.getCampSignX(), site.getCampSignY(), site.getCampSignZ()), - site.publicName())); - } + // Every template has one sign piece, and an allowed placement plants all of them. + plugin.getServer().getScheduler().runTask(plugin, () -> CampSigns.write( + world.getBlockAt(site.getCampSignX(), site.getCampSignY(), site.getCampSignZ()), + site.publicName())); player.playSound(player.getLocation(), Sound.BLOCK_WOODEN_BUTTON_CLICK_ON, SoundCategory.PLAYERS, 0.8f, 1.1f); if (moving) { player.sendMessage("Camp moved."); @@ -714,13 +693,10 @@ private String directorCapHint() { * First plant: finds whose cells are already air, water, or builds are damaged. Does not block the claim. * * @param player director + * @param ruinWorld world the camp was just planted in, which is always the ruin's world * @param site excavation just established */ - private void noteMissingTerrain(Player player, Site site) { - World ruinWorld = plugin.getServer().getWorld(site.getWorldName()); - if (ruinWorld == null) { - return; - } + private void noteMissingTerrain(Player player, World ruinWorld, Site site) { ruinWorld.getChunkAt(site.getChunkX(), site.getChunkZ()).load(); PrismWound.Prior prior = PrismWound.markMissingTerrain(ruinWorld, site); site.catalogSettledFinds(null); @@ -745,17 +721,6 @@ private boolean holdingKit(Player player) { || item.isEstablish(player.getInventory().getItemInOffHand()); } - /** - * Site being moved, or the nearest hidden ruin ready to claim. - * - * @param player kit holder - * @return site, or {@code null} - */ - private Site activeSite(Player player) { - Site moving = relocatingSite(player); - return moving != null ? moving : nearestConfirmedHidden(player); - } - /** * @param player kit holder * @return excavation being relocated, or {@code null} @@ -765,45 +730,40 @@ private Site relocatingSite(Player player) { if (siteId == null) { return null; } - return sites.findById(siteId).orElse(null); + Site site = sites.findById(siteId).orElse(null); + if (site == null || !site.isCampLocked() || !site.isDirector(player.getUniqueId())) { + cancelRelocate(player); + return null; + } + return site; } /** + * Outside a move the site is always a hidden ruin, so only the player's own choice applies. + * * @param player kit holder - * @param site current site, or {@code null} + * @param site site being placed; the camp being moved while relocating * @return DyeColor name for {@code W} cells */ private String primaryWoolName(Player player, Site site) { - if (isRelocating(player) && site != null) { + if (isRelocating(player)) { return site.getCampWoolPrimary(); } String chosen = woolChoicePrimary.get(player.getUniqueId()); - if (chosen != null) { - return chosen; - } - if (site != null && site.isCampLocked()) { - return site.getCampWoolPrimary(); - } - return "WHITE"; + return chosen != null ? chosen : "WHITE"; } /** * @param player kit holder - * @param site current site, or {@code null} + * @param site site being placed, the camp being moved, or {@code null} when cycling away from any ruin * @return DyeColor name for {@code R} cells */ private String secondaryWoolName(Player player, Site site) { - if (isRelocating(player) && site != null) { + if (isRelocating(player)) { return site.getCampWoolSecondary(); } String chosen = woolChoiceSecondary.get(player.getUniqueId()); - if (chosen != null) { - return chosen; - } - if (site != null && site.isCampLocked()) { - return site.getCampWoolSecondary(); - } - return "RED"; + return chosen != null ? chosen : "RED"; } /** @@ -900,11 +860,8 @@ private CampPlacement assess(Player player, Site site, Block target) { * @return first real block on the crosshair, or {@code null} */ private Block aimBlock(Player player, Site site) { - if (site == null) { - return player.getTargetBlockExact(64); - } - UUID movingId = relocating.get(player.getUniqueId()); - if (movingId == null || !movingId.equals(site.getId())) { + // While relocating, the only site ever aimed for is the camp being moved. + if (!relocating.containsKey(player.getUniqueId())) { return player.getTargetBlockExact(64); } BlockIterator iterator = new BlockIterator(player, 64); @@ -932,15 +889,20 @@ private Block aimBlock(Player player, Site site) { * @return placement to confirm, or {@code null} to re-aim */ private CampPlacement lastAllowedPulse(Player player, Site site) { - UUID siteId = lastPulseSite.get(player.getUniqueId()); - if (siteId == null || !siteId.equals(site.getId())) { - return null; - } - CampPlacement placement = lastPulse.get(player.getUniqueId()); - if (placement == null || !placement.allowed()) { + Pulse pulse = lastPulse.get(player.getUniqueId()); + if (pulse == null || !pulse.siteId().equals(site.getId()) || !pulse.placement().allowed()) { return null; } - return placement; + return pulse.placement(); + } + + /** + * Ghost drawn on the last preview tick, with the site it was drawn for. + * + * @param siteId ruin or excavation the ghost belongs to + * @param placement assessed ghost + */ + private record Pulse(UUID siteId, CampPlacement placement) { } /** @@ -952,12 +914,10 @@ private void updateMoveAimProxy(Player player) { Location loc = player.getEyeLocation().add(player.getEyeLocation().getDirection().multiply(2.4)); UUID existingId = moveAimProxies.get(player.getUniqueId()); Entity existing = existingId == null ? null : plugin.getServer().getEntity(existingId); + // A world change ends the move and removes the stand, so a live stand shares the viewer's world. if (existing instanceof ArmorStand stand && stand.isValid()) { - if (stand.getWorld().equals(loc.getWorld())) { - stand.teleport(loc); - return; - } - removeMoveAimProxy(player); + stand.teleport(loc); + return; } ArmorStand stand = loc.getWorld().spawn(loc, ArmorStand.class, this::configureMoveAimProxy); moveAimProxies.put(player.getUniqueId(), stand.getUniqueId()); @@ -1030,13 +990,16 @@ private CampPlacement.Issue chunkIssue(Site site, Block target) { String world = chunk.getWorld().getName(); int cx = chunk.getX(); int cz = chunk.getZ(); - if (world.equals(site.getWorldName()) && cx == site.getChunkX() && cz == site.getChunkZ()) { + if (!world.equals(site.getWorldName())) { + return CampPlacement.Issue.NOT_NEIGHBOR; + } + if (cx == site.getChunkX() && cz == site.getChunkZ()) { return CampPlacement.Issue.ON_DIG; } if (occupiedByOther(site, world, cx, cz)) { return CampPlacement.Issue.OCCUPIED; } - if (!isValidCampChunk(site, world, cx, cz)) { + if (!isNeighborChunk(site.getChunkX(), site.getChunkZ(), cx, cz)) { return CampPlacement.Issue.NOT_NEIGHBOR; } return null; @@ -1148,33 +1111,16 @@ private void addSurface(World world, int x, int z, BlockData data, Map