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