From be0b4e53e109825d60947fbf67ae01e9e87399f0 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Fri, 14 Aug 2026 21:10:55 -0400 Subject: [PATCH 01/75] add --atomic on push in release script --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9bb304..ef44386 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -126,7 +126,7 @@ jobs: git add pom.xml README.md .changeset git commit -m "chore(release): $NEXT" git tag -a "v$NEXT" -m "Release $NEXT" - git push origin HEAD:master --follow-tags + git push --atomic origin HEAD:master --follow-tags git ls-remote --exit-code --tags origin "refs/tags/v$NEXT" > /dev/null \ || { echo "Tag v$NEXT did not reach the remote"; exit 1; } From 0f90ebb740d7cd94a22fba88b003a18f162f5218 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Fri, 14 Aug 2026 23:31:16 -0400 Subject: [PATCH 02/75] ci: let a labelled down-merge skip the changeset gate A master to dev down-merge carries files that ship, so the gate demands a changeset for them. Adding one would be wrong: the next promotion of dev consumes it and bumps the version a second time for work already released. Skip the requirement when a pull request into dev carries the downmerge label. The label is ignored on any other base, so a release still cannot reach master without a changeset. The gate now also runs on labeled and unlabeled, so applying it re-runs the check. --- .github/workflows/changeset.yml | 7 +++++- scripts/require-changeset.sh | 16 +++++++++++++ scripts/test-require-changeset.sh | 38 +++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/.github/workflows/changeset.yml b/.github/workflows/changeset.yml index 3afcb3e..31afab6 100644 --- a/.github/workflows/changeset.yml +++ b/.github/workflows/changeset.yml @@ -3,7 +3,10 @@ name: Changeset # Its own workflow rather than a job in Tests: a job skipped by an `if:` still # reports a check run, so the push event would publish a second, skipped # "changeset" result for the same commit. -on: pull_request +on: + pull_request: + # labeled and unlabeled so applying the downmerge label re-runs the gate. + types: [opened, synchronize, reopened, labeled, unlabeled] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -24,6 +27,8 @@ jobs: env: GH_TOKEN: ${{ github.token }} PR: ${{ github.event.pull_request.number }} + PR_BASE: ${{ github.event.pull_request.base.ref }} + PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} run: | set -euo pipefail # Written to a file rather than piped, so a failing gh call cannot be diff --git a/scripts/require-changeset.sh b/scripts/require-changeset.sh index dbeacc1..9795cc4 100755 --- a/scripts/require-changeset.sh +++ b/scripts/require-changeset.sh @@ -9,6 +9,9 @@ CHANGESET_DIR=".changeset" +# Label that marks a master to dev down-merge. +DOWNMERGE_LABEL="downmerge" + # Nothing under these prefixes ships to a user's server. EXEMPT_PREFIXES=( "$CHANGESET_DIR/" @@ -66,6 +69,18 @@ main() { local line path has_changeset=1 local shippable=() + # Everything on master has already been released, so a down-merge must not + # carry a changeset: the next promotion would consume it and bump the version + # a second time for the same work. + if [ "${PR_BASE:-}" = "dev" ]; then + case ",${PR_LABELS:-}," in + *,"$DOWNMERGE_LABEL",*) + printf 'Labelled %s into dev - no changeset needed.\n' "$DOWNMERGE_LABEL" + return 0 + ;; + esac + fi + while IFS= read -r line || [ -n "$line" ]; do # gh writes LF, but a hand-piped list on Windows may not. path="${line%$'\r'}" @@ -94,6 +109,7 @@ main() { printf 'This pull request changes files that ship to users but adds no changeset:\n' printf ' %s\n' "${shippable[@]}" printf '\nAdd one with: bash scripts/changeset.sh\n' + printf 'Down-merging master into dev? Label the pull request %s instead.\n' "$DOWNMERGE_LABEL" } >&2 return 1 } diff --git a/scripts/test-require-changeset.sh b/scripts/test-require-changeset.sh index 5336a7a..fb30821 100755 --- a/scripts/test-require-changeset.sh +++ b/scripts/test-require-changeset.sh @@ -185,6 +185,38 @@ test_blank_lines_are_ignored() { "$(printf 'README.md\n\n' | bash "$REQUIRE_SH" > /dev/null 2>&1; printf '%s' "$?")" } +# -- down-merge bypass -- + +# Runs the script the way the workflow does for a pull request, with the base +# branch and labels supplied. +run_cli_pr() { + local base="$1" labels="$2" + shift 2 + printf '%s +' "$@" | PR_BASE="$base" PR_LABELS="$labels" bash "$REQUIRE_SH" > /dev/null 2>&1 + printf '%s' "$?" +} + +test_a_labelled_downmerge_into_dev_passes() { + assert_status "a downmerge-labelled pull request into dev needs no changeset" 0 "$(run_cli_pr dev downmerge "src/main/java/Foo.java" "pom.xml")" +} + +test_the_label_is_found_among_others() { + assert_status "the label is found alongside other labels" 0 "$(run_cli_pr dev "bug,downmerge,java" "src/main/java/Foo.java")" +} + +test_an_unlabelled_pull_request_into_dev_still_fails() { + assert_status "an unlabelled pull request into dev still needs a changeset" 1 "$(run_cli_pr dev "" "src/main/java/Foo.java")" +} + +test_the_label_does_not_bypass_a_release() { + assert_status "the label does not bypass a pull request into master" 1 "$(run_cli_pr master downmerge "src/main/java/Foo.java")" +} + +test_the_label_is_matched_whole() { + assert_status "a label merely containing the word does not bypass" 1 "$(run_cli_pr dev "not-downmerge" "src/main/java/Foo.java")" +} + test_plugin_source_requires_a_changeset test_the_pom_requires_a_changeset test_an_unrecognised_path_requires_a_changeset @@ -209,6 +241,12 @@ test_the_failure_explains_the_fix test_carriage_returns_are_tolerated test_blank_lines_are_ignored +test_a_labelled_downmerge_into_dev_passes +test_the_label_is_found_among_others +test_an_unlabelled_pull_request_into_dev_still_fails +test_the_label_does_not_bypass_a_release +test_the_label_is_matched_whole + printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" if [ "$FAIL" -gt 0 ]; then exit 1 From 35cddd93f1e72bcdb4acba44bb1e4276d3c68274 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 00:07:25 -0400 Subject: [PATCH 03/75] feat: allow permission defaults to be overridden in config.yml --- .../samleighton/sethomestwo/SetHomesTwo.java | 4 + .../utils/PermissionOverrides.java | 62 ++++++++++++++ src/main/resources/default-config.yml | 17 ++++ .../utils/PermissionOverridesTest.java | 80 +++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java create mode 100644 src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java diff --git a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java index f1e3d30..b7f1d04 100644 --- a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java +++ b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java @@ -19,6 +19,7 @@ import com.samleighton.sethomestwo.tabcompleters.RemoveDimensionTabCompleter; import com.samleighton.sethomestwo.utils.ConfigUtil; import com.samleighton.sethomestwo.utils.DatabaseUtil; +import com.samleighton.sethomestwo.utils.PermissionOverrides; import org.bukkit.Bukkit; import org.bukkit.command.PluginCommand; import org.bukkit.entity.Player; @@ -55,6 +56,9 @@ public void onEnable() { // Create config initConfig(); + // After the config exists, before commands are registered. + PermissionOverrides.apply(); + // Built before the listeners: the join listener is handed this instance. updateChecker = new UpdateChecker( this, diff --git a/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java b/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java new file mode 100644 index 0000000..a527927 --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java @@ -0,0 +1,62 @@ +package com.samleighton.sethomestwo.utils; + +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.PluginManager; + +/** + * Applies the config.yml permissions block over the defaults declared in + * plugin.yml. Only a node's default changes, so an explicit grant or deny in a + * permissions plugin still wins. + */ +public final class PermissionOverrides { + + private PermissionOverrides() { + } + + public static void apply() { + ConfigurationSection section = ConfigUtil.getConfig().getConfigurationSection("permissions"); + if (section == null) return; + + PluginManager pluginManager = Bukkit.getPluginManager(); + + // Deep keys, because Bukkit splits a dotted key such as sh2.import-homes + // into nested sections. The intermediate sections are not nodes. + for (String node : section.getKeys(true)) { + if (section.isConfigurationSection(node)) continue; + + Permission permission = pluginManager.getPermission(node); + if (permission == null) { + Bukkit.getLogger().warning(String.format( + "SetHomesTwo: ignoring unknown permission node '%s' in config.yml.", node)); + continue; + } + + String raw = section.getString(node); + PermissionDefault parsed = raw == null ? null : PermissionDefault.getByName(raw); + if (parsed == null) { + Bukkit.getLogger().warning(String.format( + "SetHomesTwo: ignoring permission '%s', value '%s' is not one of true, false, op, not-op.", + node, raw)); + continue; + } + + PermissionDefault previous = permission.getDefault(); + if (previous == parsed) continue; + + permission.setDefault(parsed); + pluginManager.recalculatePermissionDefaults(permission); + + Bukkit.getLogger().info(String.format( + "SetHomesTwo: permission default changed, %s %s to %s", node, previous, parsed)); + + if ("sh2.import-homes".equals(node) && parsed != PermissionDefault.OP) { + Bukkit.getLogger().warning( + "SetHomesTwo: sh2.import-homes is no longer operator only. " + + "/import-homes confirm writes homes for every player on the server."); + } + } + } +} diff --git a/src/main/resources/default-config.yml b/src/main/resources/default-config.yml index b45c439..03ebb3e 100644 --- a/src/main/resources/default-config.yml +++ b/src/main/resources/default-config.yml @@ -120,3 +120,20 @@ checkForUpdates: true # repeat it. A release nobody has been told about is always announced straight # away, whatever this is set to. Use 0 to announce each release exactly once. updateReminderDays: 7 + +# -- PERMISSIONS -- +# Override the built-in default for any sh2.* permission node, without +# installing a permissions plugin. Accepted values: true | false | op | not-op +# +# This only changes the default. An explicit grant or deny in LuckPerms still +# wins. Unknown node names are ignored with a warning in the server log, and +# every applied override is logged at startup. +# +# There is no wildcard form. List each node you want to change. +# +# Take care with sh2.import-homes: /import-homes confirm writes homes +# for every player on the server, and the command has no second permission check. +# +# permissions: +# sh2.import-homes: op +# sh2.manage-homes: false diff --git a/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java b/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java new file mode 100644 index 0000000..b74a547 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java @@ -0,0 +1,80 @@ +package com.samleighton.sethomestwo.utils; + +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.permissions.PermissionDefault; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PermissionOverridesTest extends ServerTestBase { + + @Test + void aConfiguredDefaultIsApplied() { + plugin.getConfig().set("permissions.sh2.import-homes", "true"); + + PermissionOverrides.apply(); + + assertEquals(PermissionDefault.TRUE, + server.getPluginManager().getPermission("sh2.import-homes").getDefault()); + } + + @Test + void notOpIsAccepted() { + plugin.getConfig().set("permissions.sh2.manage-homes", "not-op"); + + PermissionOverrides.apply(); + + assertEquals(PermissionDefault.NOT_OP, + server.getPluginManager().getPermission("sh2.manage-homes").getDefault()); + } + + @Test + void anUnknownNodeIsIgnored() { + plugin.getConfig().set("permissions.sh2.not-a-real-node", "true"); + + PermissionOverrides.apply(); + + assertEquals(null, server.getPluginManager().getPermission("sh2.not-a-real-node")); + } + + @Test + void anUnparseableValueLeavesTheDefaultAlone() { + plugin.getConfig().set("permissions.sh2.import-homes", "sometimes"); + + PermissionOverrides.apply(); + + assertEquals(PermissionDefault.OP, + server.getPluginManager().getPermission("sh2.import-homes").getDefault()); + } + + @Test + void aWildcardIsNotHonoured() { + plugin.getConfig().set("permissions.sh2.*", "true"); + + PermissionOverrides.apply(); + + assertEquals(PermissionDefault.OP, + server.getPluginManager().getPermission("sh2.import-homes").getDefault()); + } + + @Test + void noPermissionsSectionLeavesStockDefaults() { + PermissionOverrides.apply(); + + assertEquals(PermissionDefault.OP, + server.getPluginManager().getPermission("sh2.import-homes").getDefault()); + assertEquals(PermissionDefault.TRUE, + server.getPluginManager().getPermission("sh2.create-home").getDefault()); + } + + @Test + void anExplicitGrantStillBeatsAConfiguredDefault() { + plugin.getConfig().set("permissions.sh2.import-homes", "false"); + PermissionOverrides.apply(); + + var player = addPlayer(); + player.addAttachment(plugin, "sh2.import-homes", true); + + org.junit.jupiter.api.Assertions.assertTrue(player.hasPermission("sh2.import-homes")); + } +} From 0b8e5fe75e49d73fa9b20f1ab9c63d1db9083e7c Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 00:17:35 -0400 Subject: [PATCH 04/75] test: assert PermissionOverrides warnings for unknown and unparseable nodes anUnknownNodeIsIgnored and anUnparseableValueLeavesTheDefaultAlone previously asserted only conditions that held before apply() ran, so they passed even against a no-op body. Both now also capture the logger and assert the specific warning apply() emits. --- .../utils/PermissionOverridesTest.java | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java b/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java index b74a547..5d544c3 100644 --- a/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java +++ b/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java @@ -1,10 +1,19 @@ package com.samleighton.sethomestwo.utils; import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.Bukkit; import org.bukkit.permissions.PermissionDefault; import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class PermissionOverridesTest extends ServerTestBase { @@ -32,19 +41,25 @@ void notOpIsAccepted() { void anUnknownNodeIsIgnored() { plugin.getConfig().set("permissions.sh2.not-a-real-node", "true"); - PermissionOverrides.apply(); + List logged = captureLog(PermissionOverrides::apply); assertEquals(null, server.getPluginManager().getPermission("sh2.not-a-real-node")); + assertTrue(loggedWarning(logged, + "SetHomesTwo: ignoring unknown permission node 'sh2.not-a-real-node' in config.yml."), + "Expected a warning naming the unknown node"); } @Test void anUnparseableValueLeavesTheDefaultAlone() { plugin.getConfig().set("permissions.sh2.import-homes", "sometimes"); - PermissionOverrides.apply(); + List logged = captureLog(PermissionOverrides::apply); assertEquals(PermissionDefault.OP, server.getPluginManager().getPermission("sh2.import-homes").getDefault()); + assertTrue(loggedWarning(logged, + "SetHomesTwo: ignoring permission 'sh2.import-homes', value 'sometimes' is not one of true, false, op, not-op."), + "Expected a warning naming the node and the rejected value"); } @Test @@ -77,4 +92,41 @@ void anExplicitGrantStillBeatsAConfiguredDefault() { org.junit.jupiter.api.Assertions.assertTrue(player.hasPermission("sh2.import-homes")); } + + /** + * Captures what gets logged during {@code action}. The handler is always + * removed afterward so it cannot leak into other tests. + */ + private List captureLog(Runnable action) { + List captured = new ArrayList<>(); + Handler handler = new Handler() { + @Override + public void publish(LogRecord record) { + captured.add(record); + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + }; + + Logger logger = Bukkit.getLogger(); + logger.addHandler(handler); + try { + action.run(); + } finally { + logger.removeHandler(handler); + } + + return captured; + } + + private boolean loggedWarning(List records, String message) { + return records.stream().anyMatch( + record -> record.getLevel() == Level.WARNING && message.equals(record.getMessage())); + } } From 4007ab6df5ed7c2e21356182fc0f09c37638df60 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 00:21:43 -0400 Subject: [PATCH 05/75] feat: add sh2.player and sh2.admin role bundle permissions --- src/main/resources/plugin.yml | 23 ++++++++++ .../utils/PermissionBundlesTest.java | 42 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/test/java/com/samleighton/sethomestwo/utils/PermissionBundlesTest.java diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index edbb186..9712c82 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -92,3 +92,26 @@ permissions: sh2.update-notify: description: Be told on join when a newer SetHomesTwo release is available. default: op + sh2.player: + description: Everything an ordinary player needs. + default: true + children: + sh2.create-home: true + sh2.go-home: true + sh2.list-homes: true + sh2.delete-home: true + sh2.teleport: true + sh2.give-homes-item: true + sh2.manage-homes: true + sh2.admin: + description: Everything a server administrator needs, including the player nodes. + default: op + children: + sh2.player: true + sh2.add-to-blacklist: true + sh2.remove-from-blacklist: true + sh2.get-blacklisted-dimensions: true + sh2.get-player-homes: true + sh2.set-max-homes: true + sh2.import-homes: true + sh2.update-notify: true diff --git a/src/test/java/com/samleighton/sethomestwo/utils/PermissionBundlesTest.java b/src/test/java/com/samleighton/sethomestwo/utils/PermissionBundlesTest.java new file mode 100644 index 0000000..7998262 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/utils/PermissionBundlesTest.java @@ -0,0 +1,42 @@ +package com.samleighton.sethomestwo.utils; + +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.permissions.PermissionDefault; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PermissionBundlesTest extends ServerTestBase { + + @Test + void theAdminBundleConfersItsChildren() { + assertNotNull(server.getPluginManager().getPermission("sh2.admin")); + + var player = addPlayer(); + player.addAttachment(plugin, "sh2.admin", true); + + assertTrue(player.hasPermission("sh2.get-player-homes")); + assertTrue(player.hasPermission("sh2.set-max-homes")); + assertTrue(player.hasPermission("sh2.import-homes")); + } + + @Test + void thePlayerBundleConfersItsChildren() { + var player = addPlayer(); + player.addAttachment(plugin, "sh2.player", true); + + assertTrue(player.hasPermission("sh2.create-home")); + assertTrue(player.hasPermission("sh2.go-home")); + assertTrue(player.hasPermission("sh2.manage-homes")); + } + + @Test + void theBundleDefaultsMatchTheirMembers() { + assertEquals(PermissionDefault.TRUE, + server.getPluginManager().getPermission("sh2.player").getDefault()); + assertEquals(PermissionDefault.OP, + server.getPluginManager().getPermission("sh2.admin").getDefault()); + } +} From b803990790fa303ad628292da85d88b593df569d Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 00:25:06 -0400 Subject: [PATCH 06/75] fix: add structural assertions to permission bundle tests --- .../sethomestwo/utils/PermissionBundlesTest.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/samleighton/sethomestwo/utils/PermissionBundlesTest.java b/src/test/java/com/samleighton/sethomestwo/utils/PermissionBundlesTest.java index 7998262..a0a0259 100644 --- a/src/test/java/com/samleighton/sethomestwo/utils/PermissionBundlesTest.java +++ b/src/test/java/com/samleighton/sethomestwo/utils/PermissionBundlesTest.java @@ -1,6 +1,7 @@ package com.samleighton.sethomestwo.utils; import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionDefault; import org.junit.jupiter.api.Test; @@ -12,7 +13,12 @@ class PermissionBundlesTest extends ServerTestBase { @Test void theAdminBundleConfersItsChildren() { - assertNotNull(server.getPluginManager().getPermission("sh2.admin")); + Permission adminBundle = server.getPluginManager().getPermission("sh2.admin"); + assertNotNull(adminBundle); + assertTrue(adminBundle.getChildren().containsKey("sh2.player")); + assertTrue(adminBundle.getChildren().containsKey("sh2.get-player-homes")); + assertTrue(adminBundle.getChildren().containsKey("sh2.set-max-homes")); + assertTrue(adminBundle.getChildren().containsKey("sh2.import-homes")); var player = addPlayer(); player.addAttachment(plugin, "sh2.admin", true); @@ -24,6 +30,12 @@ void theAdminBundleConfersItsChildren() { @Test void thePlayerBundleConfersItsChildren() { + Permission playerBundle = server.getPluginManager().getPermission("sh2.player"); + assertNotNull(playerBundle); + assertTrue(playerBundle.getChildren().containsKey("sh2.create-home")); + assertTrue(playerBundle.getChildren().containsKey("sh2.go-home")); + assertTrue(playerBundle.getChildren().containsKey("sh2.manage-homes")); + var player = addPlayer(); player.addAttachment(plugin, "sh2.player", true); From 26c94582b63e6f9748d8abb76e1db493b9ad6610 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 00:29:41 -0400 Subject: [PATCH 07/75] feat: store the owning player name with each home --- .../samleighton/sethomestwo/dao/HomesDao.java | 61 ++++++++++++++++++- .../samleighton/sethomestwo/models/Home.java | 9 +++ .../sethomestwo/utils/DatabaseUtil.java | 20 +++++- .../dao/HomesDaoPlayerNameTest.java | 44 +++++++++++++ 4 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/samleighton/sethomestwo/dao/HomesDaoPlayerNameTest.java diff --git a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java index 0a4a439..0607819 100644 --- a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java +++ b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java @@ -5,7 +5,9 @@ import com.samleighton.sethomestwo.utils.ServerUtil; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.entity.Player; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -64,6 +66,7 @@ public List getAll(Object... keys) { rs.getString("dimension") ); home.setId(rs.getInt("id")); + home.setPlayerName(rs.getString("player_name")); String dimension = home.getDimension(); List blacklistedDimensions = blacklistEntryDao.getAll(); @@ -124,6 +127,7 @@ public Home get(Object... keys) { rs.getString("dimension") ); home.setId(rs.getInt("id")); + home.setPlayerName(rs.getString("player_name")); } } catch (SQLException e) { Bukkit.getLogger().severe("There was an issue reading a home for player " + playerUUID); @@ -138,7 +142,14 @@ public boolean save(Object object) { if(!(object instanceof Home)) return false; Home home = (Home) object; - String sql = "insert into %s (player_uuid, world, material, name, description, x, y, z, pitch, yaw, dimension) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; + + String playerName = home.getPlayerName(); + if (playerName == null) { + Player owner = Bukkit.getPlayer(UUID.fromString(home.getUUIDBelongingTo())); + playerName = owner == null ? null : owner.getName(); + } + + String sql = "insert into %s (player_uuid, world, material, name, description, x, y, z, pitch, yaw, dimension, player_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; return DatabaseUtil.execute( this.conn, String.format(sql, TABLE_NAME), @@ -152,7 +163,8 @@ public boolean save(Object object) { home.getZ(), home.getPitch(), home.getYaw(), - home.getDimension() + home.getDimension(), + playerName ); } @@ -182,7 +194,13 @@ public boolean update(Object object) { return false; } - String sql = "update %s set material = ?, world = ?, name = ?, description = ?, x = ?, y = ?, z = ?, pitch = ?, yaw = ?, dimension = ? where id = ? and player_uuid = ?"; + String playerName = home.getPlayerName(); + if (playerName == null) { + Player owner = Bukkit.getPlayer(UUID.fromString(home.getUUIDBelongingTo())); + playerName = owner == null ? null : owner.getName(); + } + + String sql = "update %s set material = ?, world = ?, name = ?, description = ?, x = ?, y = ?, z = ?, pitch = ?, yaw = ?, dimension = ?, player_name = ? where id = ? and player_uuid = ?"; return DatabaseUtil.executeUpdate( this.conn, String.format(sql, TABLE_NAME), @@ -196,6 +214,7 @@ public boolean update(Object object) { home.getPitch(), home.getYaw(), home.getDimension(), + playerName, home.getId(), home.getUUIDBelongingTo() ) > 0; @@ -234,6 +253,7 @@ public Home getById(UUID playerUUID, int id) { rs.getString("dimension") ); home.setId(rs.getInt("id")); + home.setPlayerName(rs.getString("player_name")); return home; } } catch (SQLException e) { @@ -278,4 +298,39 @@ public boolean nameExists(UUID playerUUID, String name, Integer excludeId) { return false; } + + /** + * The UUID of the player who owns homes stored under this name, or null. + */ + public String uuidForName(String playerName) { + String sql = "select player_uuid from players_homes where player_name = ? limit 1;"; + + try (PreparedStatement statement = this.conn.prepareStatement(sql)) { + statement.setString(1, playerName); + + try (ResultSet rs = statement.executeQuery()) { + return rs.next() ? rs.getString("player_uuid") : null; + } + } catch (SQLException e) { + Bukkit.getLogger().severe("Could not resolve player name " + playerName); + return null; + } + } + + /** + * Point every home this player owns at their current name. + */ + public boolean refreshPlayerName(UUID playerUUID, String playerName) { + String sql = "update players_homes set player_name = ? where player_uuid = ?;"; + + try (PreparedStatement statement = this.conn.prepareStatement(sql)) { + statement.setString(1, playerName); + statement.setString(2, playerUUID.toString()); + statement.executeUpdate(); + return true; + } catch (SQLException e) { + Bukkit.getLogger().severe("Could not refresh player name for " + playerUUID); + return false; + } + } } diff --git a/src/main/java/com/samleighton/sethomestwo/models/Home.java b/src/main/java/com/samleighton/sethomestwo/models/Home.java index 388981d..fb55fe2 100644 --- a/src/main/java/com/samleighton/sethomestwo/models/Home.java +++ b/src/main/java/com/samleighton/sethomestwo/models/Home.java @@ -43,6 +43,7 @@ public class Home implements Serializable { private float pitch; private float yaw; private boolean canTeleport = true; + private String playerName; public Home(String playerUUID, String material, Location location, String name, String description, String dimension) { setUUIDBelongingTo(playerUUID); @@ -184,6 +185,14 @@ public void setCanTeleport(boolean canTeleport) { this.canTeleport = canTeleport; } + public String getPlayerName() { + return playerName; + } + + public void setPlayerName(String playerName) { + this.playerName = playerName; + } + public void teleport(Player player) { // Home is blacklisted guard if(!this.getCanTeleport()) { diff --git a/src/main/java/com/samleighton/sethomestwo/utils/DatabaseUtil.java b/src/main/java/com/samleighton/sethomestwo/utils/DatabaseUtil.java index 3fda6ff..b13624c 100644 --- a/src/main/java/com/samleighton/sethomestwo/utils/DatabaseUtil.java +++ b/src/main/java/com/samleighton/sethomestwo/utils/DatabaseUtil.java @@ -29,6 +29,8 @@ public static boolean initTables(Connection connection){ ");"; boolean createPlayersHomes = execute(connection, String.format(createPlayersHomesSQL, "players_homes")); + boolean playerNameColumn = ensureColumn(connection, "players_homes", "player_name", "TEXT"); + // Create blacklist table String createBlacklistSQL = "create table if not exists %s (\n" + "id integer PRIMARY KEY, \n" + @@ -47,7 +49,23 @@ public static boolean initTables(Connection connection){ ");"; boolean createPlayerTeleportAttempts = execute(connection, String.format(createSQL, "player_teleport_attempts")); - return createPlayerTeleportAttempts && createBlacklist && createPlayersHomes; + return createPlayerTeleportAttempts && createBlacklist && createPlayersHomes && playerNameColumn; + } + + /** + * SQLite has no ADD COLUMN IF NOT EXISTS, so the column list is read first. + */ + private static boolean ensureColumn(Connection connection, String table, String column, String type) { + try (ResultSet rs = connection.createStatement().executeQuery("pragma table_info(" + table + ");")) { + while (rs.next()) { + if (column.equalsIgnoreCase(rs.getString("name"))) return true; + } + } catch (SQLException e) { + Bukkit.getLogger().severe("Could not read columns for " + table + ": " + e.getMessage()); + return false; + } + + return execute(connection, String.format("alter table %s add column %s %s;", table, column, type)); } /** diff --git a/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoPlayerNameTest.java b/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoPlayerNameTest.java new file mode 100644 index 0000000..ee643c3 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoPlayerNameTest.java @@ -0,0 +1,44 @@ +package com.samleighton.sethomestwo.dao; + +import com.samleighton.sethomestwo.support.HomeFixtures; +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class HomesDaoPlayerNameTest extends ServerTestBase { + + @Test + void theOwnerNameIsStoredWhenAHomeIsSaved() { + PlayerMock player = addPlayer("Steve"); + HomeFixtures.persist(player, "base"); + + assertEquals("Steve", new HomesDao().getAll(player.getUniqueId()).get(0).getPlayerName()); + } + + @Test + void aNameResolvesToItsOwnersUuid() { + PlayerMock player = addPlayer("Steve"); + HomeFixtures.persist(player, "base"); + + assertEquals(player.getUniqueId().toString(), new HomesDao().uuidForName("Steve")); + } + + @Test + void anUnknownNameResolvesToNull() { + assertNull(new HomesDao().uuidForName("Nobody")); + } + + @Test + void aRenameIsPickedUpByRefresh() { + PlayerMock player = addPlayer("Steve"); + HomeFixtures.persist(player, "base"); + + new HomesDao().refreshPlayerName(player.getUniqueId(), "Steven"); + + assertEquals(player.getUniqueId().toString(), new HomesDao().uuidForName("Steven")); + assertNull(new HomesDao().uuidForName("Steve")); + } +} From daa5d0f3856f73750a2d2e044c0ece3040d8b99b Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 00:36:13 -0400 Subject: [PATCH 08/75] test: cover the idempotent player_name migration and fix a statement leak Adds coverage for the ensureColumn early-return path, which every real server restart takes after the first upgrade, and closes the Statement that pragma table_info opens in ensureColumn. --- .../sethomestwo/utils/DatabaseUtil.java | 3 +- .../dao/HomesDaoPlayerNameTest.java | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/samleighton/sethomestwo/utils/DatabaseUtil.java b/src/main/java/com/samleighton/sethomestwo/utils/DatabaseUtil.java index b13624c..2648e81 100644 --- a/src/main/java/com/samleighton/sethomestwo/utils/DatabaseUtil.java +++ b/src/main/java/com/samleighton/sethomestwo/utils/DatabaseUtil.java @@ -56,7 +56,8 @@ public static boolean initTables(Connection connection){ * SQLite has no ADD COLUMN IF NOT EXISTS, so the column list is read first. */ private static boolean ensureColumn(Connection connection, String table, String column, String type) { - try (ResultSet rs = connection.createStatement().executeQuery("pragma table_info(" + table + ");")) { + try (Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery("pragma table_info(" + table + ");")) { while (rs.next()) { if (column.equalsIgnoreCase(rs.getString("name"))) return true; } diff --git a/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoPlayerNameTest.java b/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoPlayerNameTest.java index ee643c3..939f9e6 100644 --- a/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoPlayerNameTest.java +++ b/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoPlayerNameTest.java @@ -2,11 +2,18 @@ import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; +import com.samleighton.sethomestwo.utils.DatabaseUtil; import org.junit.jupiter.api.Test; import org.mockbukkit.mockbukkit.entity.PlayerMock; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class HomesDaoPlayerNameTest extends ServerTestBase { @@ -41,4 +48,27 @@ void aRenameIsPickedUpByRefresh() { assertEquals(player.getUniqueId().toString(), new HomesDao().uuidForName("Steven")); assertNull(new HomesDao().uuidForName("Steve")); } + + @Test + void initTablesIsSafeToRunAgainAfterTheColumnAlreadyExists() { + Connection connection = plugin.getConnectionManager().getConnection("homes"); + + assertTrue(DatabaseUtil.initTables(connection)); + assertEquals(1, countPlayerNameColumns(connection)); + } + + private int countPlayerNameColumns(Connection connection) { + int count = 0; + + try (Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery("pragma table_info(players_homes);")) { + while (rs.next()) { + if ("player_name".equalsIgnoreCase(rs.getString("name"))) count++; + } + } catch (SQLException e) { + throw new IllegalStateException("Could not read players_homes columns", e); + } + + return count; + } } From cd669080dcb5dc8e8eec9ec2c8096fb94247d0c4 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 00:41:56 -0400 Subject: [PATCH 09/75] feat: resolve offline players from stored home owner names --- .../sethomestwo/commands/GetPlayerHomes.java | 8 ++--- .../sethomestwo/events/PlayerJoin.java | 3 ++ .../sethomestwo/utils/ServerUtil.java | 14 +++++--- .../utils/ServerUtilOfflineLookupTest.java | 35 +++++++++++++++++++ 4 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 src/test/java/com/samleighton/sethomestwo/utils/ServerUtilOfflineLookupTest.java diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java b/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java index 3122792..1f7f2e3 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java @@ -63,10 +63,10 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command Dao homesDao = new HomesDao(true); List playersHomes = homesDao.getAll(UUID.fromString(uuidString)); - Player player = Bukkit.getPlayer(UUID.fromString(uuidString)); - if (player == null) return true; + Player target = Bukkit.getPlayer(UUID.fromString(uuidString)); + String targetName = target == null ? args[0] : target.getDisplayName(); - HomesGui adminGui = new HomesGui(requester, "Homes of " + player.getDisplayName()); + HomesGui adminGui = new HomesGui(requester, "Homes of " + targetName); adminGui.setHomes(playersHomes); GuiSession session = plugin.getGuiSessionMap().computeIfAbsent(requester.getUniqueId(), uuid -> new GuiSession(new HomesGui(requester))); @@ -74,7 +74,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command adminGui.displayInventory(requester); if (ConfigUtil.getDebugLevel().equals(DebugLevel.INFO)) - Bukkit.getLogger().info(String.format("%s is viewing homes of player %s", requester.getDisplayName(), player.getDisplayName())); + Bukkit.getLogger().info(String.format("%s is viewing homes of player %s", requester.getDisplayName(), targetName)); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/events/PlayerJoin.java b/src/main/java/com/samleighton/sethomestwo/events/PlayerJoin.java index 2f936f9..4c0bbae 100644 --- a/src/main/java/com/samleighton/sethomestwo/events/PlayerJoin.java +++ b/src/main/java/com/samleighton/sethomestwo/events/PlayerJoin.java @@ -1,6 +1,7 @@ package com.samleighton.sethomestwo.events; import com.samleighton.sethomestwo.SetHomesTwo; +import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.gui.GuiSession; import com.samleighton.sethomestwo.gui.HomesGui; import com.samleighton.sethomestwo.updates.UpdateChecker; @@ -24,6 +25,8 @@ public void onPlayerJoin(PlayerJoinEvent event){ Player player = event.getPlayer(); plugin.getGuiSessionMap().put(player.getUniqueId(), new GuiSession(new HomesGui(player))); + new HomesDao().refreshPlayerName(player.getUniqueId(), player.getName()); + updateChecker.notifyIfUpdateAvailable(player); } } diff --git a/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java b/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java index e362ad9..1ff656e 100644 --- a/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java +++ b/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java @@ -2,6 +2,7 @@ import com.samleighton.sethomestwo.dao.BlacklistDao; import com.samleighton.sethomestwo.dao.Dao; +import com.samleighton.sethomestwo.dao.HomesDao; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -61,14 +62,17 @@ public static boolean isDimensionBlacklisted(String dimension) { return blacklistedDimensions.contains(getDimensionsMap().get(dimension)); } - public static String getPlayerUUID(String playerName){ - for(Player player : Bukkit.getOnlinePlayers()) { - String name = player.getDisplayName(); - if(name.equals(playerName)) { + /** + * Resolve a player name to a UUID, falling back to the names stored against + * saved homes so offline players can be addressed. + */ + public static String getPlayerUUID(String playerName) { + for (Player player : Bukkit.getOnlinePlayers()) { + if (player.getName().equals(playerName)) { return player.getUniqueId().toString(); } } - return null; + return new HomesDao().uuidForName(playerName); } } diff --git a/src/test/java/com/samleighton/sethomestwo/utils/ServerUtilOfflineLookupTest.java b/src/test/java/com/samleighton/sethomestwo/utils/ServerUtilOfflineLookupTest.java new file mode 100644 index 0000000..e27b6ee --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/utils/ServerUtilOfflineLookupTest.java @@ -0,0 +1,35 @@ +package com.samleighton.sethomestwo.utils; + +import com.samleighton.sethomestwo.support.HomeFixtures; +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class ServerUtilOfflineLookupTest extends ServerTestBase { + + @Test + void anOnlinePlayerStillResolves() { + PlayerMock player = addPlayer("Steve"); + + assertEquals(player.getUniqueId().toString(), ServerUtil.getPlayerUUID("Steve")); + } + + @Test + void anOfflinePlayerWithStoredHomesResolves() { + PlayerMock player = addPlayer("Steve"); + HomeFixtures.persist(player, "base"); + String expected = player.getUniqueId().toString(); + + player.disconnect(); + + assertEquals(expected, ServerUtil.getPlayerUUID("Steve")); + } + + @Test + void aPlayerWithNoHomesAndNoSessionDoesNotResolve() { + assertNull(ServerUtil.getPlayerUUID("Nobody")); + } +} From 889f18bf83dedc79c3ab260bbbf5c7012a23db76 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 00:51:09 -0400 Subject: [PATCH 10/75] fix: prevent stale player names from resolving to the wrong owner Refresh now claims a name exclusively by clearing it off any other UUID's rows before pointing this player's rows at it. uuidForName returns null instead of guessing when a name still resolves to more than one distinct UUID. --- .../sethomestwo/commands/GetPlayerHomes.java | 2 +- .../samleighton/sethomestwo/dao/HomesDao.java | 32 ++++++++++++----- .../dao/HomesDaoPlayerNameTest.java | 35 +++++++++++++++++++ .../sethomestwo/events/PlayerJoinTest.java | 19 ++++++++++ 4 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java b/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java index 1f7f2e3..92647e5 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java @@ -54,7 +54,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command String uuidString = ServerUtil.getPlayerUUID(args[0]); - // Add a check for if player is online/exists + // null means the name matched no online player and no stored home owner if (uuidString == null) { ChatUtils.sendError(requester, UserError.PLAYER_NOT_ONLINE.getValue()); return true; diff --git a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java index 0607819..7db034c 100644 --- a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java +++ b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java @@ -301,15 +301,21 @@ public boolean nameExists(UUID playerUUID, String name, Integer excludeId) { /** * The UUID of the player who owns homes stored under this name, or null. + * A stale name can collide across two accounts (an old owner who renamed + * away and a new owner who took the name); returns null rather than + * guessing when more than one distinct UUID claims the name. */ public String uuidForName(String playerName) { - String sql = "select player_uuid from players_homes where player_name = ? limit 1;"; + String sql = "select distinct player_uuid from players_homes where player_name = ?;"; try (PreparedStatement statement = this.conn.prepareStatement(sql)) { statement.setString(1, playerName); try (ResultSet rs = statement.executeQuery()) { - return rs.next() ? rs.getString("player_uuid") : null; + if (!rs.next()) return null; + + String uuid = rs.getString("player_uuid"); + return rs.next() ? null : uuid; } } catch (SQLException e) { Bukkit.getLogger().severe("Could not resolve player name " + playerName); @@ -318,15 +324,23 @@ public String uuidForName(String playerName) { } /** - * Point every home this player owns at their current name. + * Point every home this player owns at their current name, first stripping + * that name from any other UUID's rows so a stale prior owner can never + * make the name resolve ambiguously. The joining player takes precedence. */ public boolean refreshPlayerName(UUID playerUUID, String playerName) { - String sql = "update players_homes set player_name = ? where player_uuid = ?;"; - - try (PreparedStatement statement = this.conn.prepareStatement(sql)) { - statement.setString(1, playerName); - statement.setString(2, playerUUID.toString()); - statement.executeUpdate(); + String clearSql = "update players_homes set player_name = null where player_name = ? and player_uuid <> ?;"; + String claimSql = "update players_homes set player_name = ? where player_uuid = ?;"; + + try (PreparedStatement clear = this.conn.prepareStatement(clearSql); + PreparedStatement claim = this.conn.prepareStatement(claimSql)) { + clear.setString(1, playerName); + clear.setString(2, playerUUID.toString()); + clear.executeUpdate(); + + claim.setString(1, playerName); + claim.setString(2, playerUUID.toString()); + claim.executeUpdate(); return true; } catch (SQLException e) { Bukkit.getLogger().severe("Could not refresh player name for " + playerUUID); diff --git a/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoPlayerNameTest.java b/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoPlayerNameTest.java index 939f9e6..c49e198 100644 --- a/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoPlayerNameTest.java +++ b/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoPlayerNameTest.java @@ -1,5 +1,6 @@ package com.samleighton.sethomestwo.dao; +import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; import com.samleighton.sethomestwo.utils.DatabaseUtil; @@ -49,6 +50,40 @@ void aRenameIsPickedUpByRefresh() { assertNull(new HomesDao().uuidForName("Steve")); } + @Test + void aNameSharedByTwoDistinctOwnersResolvesToNull() { + PlayerMock playerA = addPlayer("PlayerA"); + Home homeA = HomeFixtures.home(playerA, "base"); + homeA.setPlayerName("Steve"); + HomeFixtures.persist(homeA); + + PlayerMock playerB = addPlayer("PlayerB"); + Home homeB = HomeFixtures.home(playerB, "base"); + homeB.setPlayerName("Steve"); + HomeFixtures.persist(homeB); + + assertNull(new HomesDao().uuidForName("Steve")); + } + + @Test + void theMostRecentJoinerClaimsANameFromAStalePriorOwner() { + PlayerMock playerA = addPlayer("PlayerA"); + Home homeA = HomeFixtures.home(playerA, "base"); + homeA.setPlayerName("Steve"); + HomeFixtures.persist(homeA); + + PlayerMock playerB = addPlayer("PlayerB"); + Home homeB = HomeFixtures.home(playerB, "base"); + homeB.setPlayerName("Steve"); + HomeFixtures.persist(homeB); + + // PlayerB actually joins under the name "Steve", so it must win the + // name and PlayerA's stale rows must stop carrying it. + new HomesDao().refreshPlayerName(playerB.getUniqueId(), "Steve"); + + assertEquals(playerB.getUniqueId().toString(), new HomesDao().uuidForName("Steve")); + } + @Test void initTablesIsSafeToRunAgainAfterTheColumnAlreadyExists() { Connection connection = plugin.getConnectionManager().getConnection("homes"); diff --git a/src/test/java/com/samleighton/sethomestwo/events/PlayerJoinTest.java b/src/test/java/com/samleighton/sethomestwo/events/PlayerJoinTest.java index 36429fb..3d83d8e 100644 --- a/src/test/java/com/samleighton/sethomestwo/events/PlayerJoinTest.java +++ b/src/test/java/com/samleighton/sethomestwo/events/PlayerJoinTest.java @@ -1,11 +1,14 @@ package com.samleighton.sethomestwo.events; +import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; import com.samleighton.sethomestwo.support.TestPlayer; import com.samleighton.sethomestwo.updates.UpdateChecker; import org.bukkit.event.player.PlayerJoinEvent; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -25,4 +28,20 @@ void joiningPlayerWhoMaySeeNoticesIsToldAboutAnAvailableUpdate() { assertNotNull(message, "expected the join listener to deliver the update notice"); assertTrue(message.contains("v1.3.0")); } + + @Test + void joiningRefreshesTheStoredNameOnExistingHomes() { + UpdateChecker checker = new UpdateChecker(plugin, "1.2.0", () -> null); + server.getPluginManager().registerEvents(new PlayerJoin(plugin, checker), plugin); + + TestPlayer player = addPlayer("Steve"); + HomeFixtures.persist(player, "base"); + // Simulate a stale name left over from before this join, as if the + // account had been seen under a different name previously. + new HomesDao().refreshPlayerName(player.getUniqueId(), "OldSteve"); + + server.getPluginManager().callEvent(new PlayerJoinEvent(player, "")); + + assertEquals(player.getUniqueId().toString(), new HomesDao().uuidForName("Steve")); + } } From 9b491582dda80c04f17fe6c0338971e981d568cd Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 00:58:13 -0400 Subject: [PATCH 11/75] fix: enforce the blacklist against the actual world, not a positional map --- .../sethomestwo/commands/CreateHome.java | 2 +- .../samleighton/sethomestwo/dao/HomesDao.java | 15 ++-- .../sethomestwo/gui/HomeActionsGui.java | 2 +- .../sethomestwo/utils/ServerUtil.java | 17 ++--- .../sethomestwo/support/HomeFixtures.java | 3 +- .../utils/BlacklistEnforcementTest.java | 73 +++++++++++++++++++ 6 files changed, 90 insertions(+), 22 deletions(-) create mode 100644 src/test/java/com/samleighton/sethomestwo/utils/BlacklistEnforcementTest.java diff --git a/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java b/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java index 304a115..6f1aad3 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java @@ -62,7 +62,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command String playerDimension = player.getWorld().getEnvironment().toString(); // Check if player is in a blacklisted dimension before creating home - if (ServerUtil.isDimensionBlacklisted(playerDimension)) { + if (ServerUtil.isWorldBlacklisted(player.getWorld())) { String errorMessage = ConfigUtil.getConfig().getString("dimensionBlacklisted", UserError.DIMENSION_IS_BLACKLISTED.getValue()); ChatUtils.sendError(player, errorMessage); return true; diff --git a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java index 7db034c..13267f1 100644 --- a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java +++ b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java @@ -2,9 +2,9 @@ import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.utils.DatabaseUtil; -import com.samleighton.sethomestwo.utils.ServerUtil; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.World; import org.bukkit.entity.Player; import java.sql.PreparedStatement; @@ -12,7 +12,6 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.UUID; public class HomesDao extends SQLiteDao implements Dao { @@ -45,7 +44,8 @@ public List getAll(Object... keys) { if (rs == null) return new ArrayList<>(); // Build list of homes - Dao blacklistEntryDao = new BlacklistDao(); + // Read once, not once per row. + List blacklistedWorlds = new BlacklistDao().getAll(); List playerHomes = new ArrayList<>(); try { while (rs.next()) { @@ -68,12 +68,9 @@ public List getAll(Object... keys) { home.setId(rs.getInt("id")); home.setPlayerName(rs.getString("player_name")); - String dimension = home.getDimension(); - List blacklistedDimensions = blacklistEntryDao.getAll(); - Map blacklistedMap = ServerUtil.getDimensionsMap(); - - if (blacklistedDimensions.contains(blacklistedMap.get(dimension))) { - if(!this.isAdmin) home.setDescription("Cannot teleport here: dimension blacklisted"); + World homeWorld = Bukkit.getWorld(UUID.fromString(home.getWorld())); + if (homeWorld != null && blacklistedWorlds.contains(homeWorld.getName().toLowerCase())) { + if (!this.isAdmin) home.setDescription("Cannot teleport here: dimension blacklisted"); home.setCanTeleport(this.isAdmin); } diff --git a/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java b/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java index 98f1653..35ccfba 100644 --- a/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java +++ b/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java @@ -166,7 +166,7 @@ public void onClick(InventoryClickEvent event, GuiSession session) { String destinationDimension = Objects.requireNonNull(destination.getWorld()).getEnvironment().toString(); // Blacklisted dimension guard, sharing the rule create-home applies. - if (ServerUtil.isDimensionBlacklisted(destinationDimension)) { + if (ServerUtil.isWorldBlacklisted(destination.getWorld())) { ChatUtils.sendError(player, ConfigUtil.getConfig().getString("cannotMoveToBlacklistedDimension", UserError.CANNOT_MOVE_TO_BLACKLISTED_DIMENSION.getValue())); return; } diff --git a/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java b/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java index 1ff656e..a4e675f 100644 --- a/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java +++ b/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java @@ -4,6 +4,7 @@ import com.samleighton.sethomestwo.dao.Dao; import com.samleighton.sethomestwo.dao.HomesDao; import org.bukkit.Bukkit; +import org.bukkit.World; import org.bukkit.entity.Player; import java.util.ArrayList; @@ -49,17 +50,15 @@ public static Map getDimensionsMap() { } /** - * Whether homes are barred from a dimension. - * - * @param dimension The environment name, as produced by - * world.getEnvironment().toString() - * @return true when the dimension is blacklisted + * Whether homes are barred from a world. Blacklist rows hold lowercased + * world names, so the world is compared directly rather than through an + * environment mapping, which could only ever address the first three worlds. */ - public static boolean isDimensionBlacklisted(String dimension) { - Dao blacklistDao = new BlacklistDao(); - List blacklistedDimensions = blacklistDao.getAll(); + public static boolean isWorldBlacklisted(World world) { + if (world == null) return false; - return blacklistedDimensions.contains(getDimensionsMap().get(dimension)); + Dao blacklistDao = new BlacklistDao(); + return blacklistDao.getAll().contains(world.getName().toLowerCase()); } /** diff --git a/src/test/java/com/samleighton/sethomestwo/support/HomeFixtures.java b/src/test/java/com/samleighton/sethomestwo/support/HomeFixtures.java index b004940..c4ce719 100644 --- a/src/test/java/com/samleighton/sethomestwo/support/HomeFixtures.java +++ b/src/test/java/com/samleighton/sethomestwo/support/HomeFixtures.java @@ -68,8 +68,7 @@ public static Home persist(Player owner, String name) { } /** - * Blacklist a world. ServerUtil maps an environment onto the lowercased - * world name, which is what the blacklist table stores. + * Blacklist a world. The blacklist table stores lowercased world names. */ public static void blacklist(String worldName) { new BlacklistDao().save(worldName.toLowerCase()); diff --git a/src/test/java/com/samleighton/sethomestwo/utils/BlacklistEnforcementTest.java b/src/test/java/com/samleighton/sethomestwo/utils/BlacklistEnforcementTest.java new file mode 100644 index 0000000..d903fec --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/utils/BlacklistEnforcementTest.java @@ -0,0 +1,73 @@ +package com.samleighton.sethomestwo.utils; + +import com.samleighton.sethomestwo.support.HomeFixtures; +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.Location; +import org.bukkit.World; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; +import org.mockbukkit.mockbukkit.world.WorldMock; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BlacklistEnforcementTest extends ServerTestBase { + + @Test + void aConventionalWorldStillEnforces() { + HomeFixtures.blacklist("world_nether"); + + assertTrue(ServerUtil.isWorldBlacklisted(nether)); + assertFalse(ServerUtil.isWorldBlacklisted(overworld)); + } + + @Test + void aFourthWorldEnforces() { + WorldMock creative = server.addSimpleWorld("creative"); + creative.setEnvironment(World.Environment.NORMAL); + HomeFixtures.blacklist("creative"); + + // The positional map could never address a fourth world, so this is the + // regression test for the silent no-op. + assertTrue(ServerUtil.isWorldBlacklisted(creative)); + assertFalse(ServerUtil.isWorldBlacklisted(overworld)); + } + + @Test + void aSecondOverworldIsBlacklistedIndependently() { + WorldMock resource = server.addSimpleWorld("resource"); + resource.setEnvironment(World.Environment.NORMAL); + HomeFixtures.blacklist("resource"); + + assertTrue(ServerUtil.isWorldBlacklisted(resource)); + assertFalse(ServerUtil.isWorldBlacklisted(overworld)); + } + + @Test + void creatingAHomeInABlacklistedFourthWorldIsRefused() { + WorldMock creative = server.addSimpleWorld("creative"); + creative.setEnvironment(World.Environment.NORMAL); + HomeFixtures.blacklist("creative"); + + PlayerMock player = addPlayer(); + player.teleport(new Location(creative, 0, 64, 0)); + + server.execute("create-home", player, "base").assertSucceeded(); + + assertTrue(player.nextMessage().contains("blacklisted")); + assertTrue(new com.samleighton.sethomestwo.dao.HomesDao().getAll(player.getUniqueId()).isEmpty()); + } + + @Test + void aHomeInABlacklistedFourthWorldCannotBeTeleportedTo() { + WorldMock creative = server.addSimpleWorld("creative"); + creative.setEnvironment(World.Environment.NORMAL); + + PlayerMock player = addPlayer(); + HomeFixtures.persist(HomeFixtures.home(player, "far", new Location(creative, 0, 64, 0))); + HomeFixtures.blacklist("creative"); + + assertFalse(new com.samleighton.sethomestwo.dao.HomesDao() + .getAll(player.getUniqueId()).get(0).getCanTeleport()); + } +} From 19497c0ba0a509187d2420938277ca4b34bed3a1 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 01:07:21 -0400 Subject: [PATCH 12/75] fix: deduplicate blacklist enforcement rule into a single implementation HomesDao.getAll inlined the same lowercase-compare rule as ServerUtil.isWorldBlacklisted, risking silent divergence between the two enforcement paths. Add a two-argument overload that takes an already-fetched blacklist, have the single-argument form delegate to it, and have HomesDao.getAll call the overload instead of inlining. --- .../samleighton/sethomestwo/dao/HomesDao.java | 3 ++- .../sethomestwo/utils/ServerUtil.java | 21 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java index 13267f1..85f23bc 100644 --- a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java +++ b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java @@ -2,6 +2,7 @@ import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.utils.DatabaseUtil; +import com.samleighton.sethomestwo.utils.ServerUtil; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; @@ -69,7 +70,7 @@ public List getAll(Object... keys) { home.setPlayerName(rs.getString("player_name")); World homeWorld = Bukkit.getWorld(UUID.fromString(home.getWorld())); - if (homeWorld != null && blacklistedWorlds.contains(homeWorld.getName().toLowerCase())) { + if (ServerUtil.isWorldBlacklisted(homeWorld, blacklistedWorlds)) { if (!this.isAdmin) home.setDescription("Cannot teleport here: dimension blacklisted"); home.setCanTeleport(this.isAdmin); diff --git a/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java b/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java index a4e675f..94b01c4 100644 --- a/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java +++ b/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java @@ -53,12 +53,29 @@ public static Map getDimensionsMap() { * Whether homes are barred from a world. Blacklist rows hold lowercased * world names, so the world is compared directly rather than through an * environment mapping, which could only ever address the first three worlds. + * + * @param world The world to check + * @return true when the world is blacklisted */ public static boolean isWorldBlacklisted(World world) { + Dao blacklistDao = new BlacklistDao(); + return isWorldBlacklisted(world, blacklistDao.getAll()); + } + + /** + * Same rule as {@link #isWorldBlacklisted(World)}, against an + * already-fetched blacklist so a caller checking many worlds does not + * query once per check. + * + * @param world The world to check + * @param blacklistedWorlds Lowercased world names, as returned by + * {@code new BlacklistDao().getAll()} + * @return true when the world is blacklisted + */ + public static boolean isWorldBlacklisted(World world, List blacklistedWorlds) { if (world == null) return false; - Dao blacklistDao = new BlacklistDao(); - return blacklistDao.getAll().contains(world.getName().toLowerCase()); + return blacklistedWorlds.contains(world.getName().toLowerCase()); } /** From edf3612deffc64519ce88e11241c1ca1c41ac3b3 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 01:24:41 -0400 Subject: [PATCH 13/75] refactor: fold the three blacklist commands into one Consolidate add-to-blacklist, remove-from-blacklist, and get-blacklisted-dimensions into a single blacklist command with the old names kept as aliases. The three permission nodes stay declared and are enforced per subcommand inside the executor, but the command-level plugin.yml permission has to OR across all three (Bukkit's own Command.testPermission supports this via a semicolon-separated list) so a holder of any single node can still reach the executor - a single node there would silently block the other two actions before onCommand ever runs. The executor infers the subcommand from the label Bukkit actually invoked it with, not just args[0], so the old bare form ("/add-to-blacklist world_nether", with no subcommand) keeps working exactly as it did before. --- README.md | 6 +- .../samleighton/sethomestwo/SetHomesTwo.java | 16 +- .../commands/AddDimensionToBlacklist.java | 69 ------ .../sethomestwo/commands/Blacklist.java | 205 ++++++++++++++++++ .../commands/GetBlacklistedDimensions.java | 50 ----- .../RemoveDimensionFromBlacklist.java | 75 ------- .../sethomestwo/enums/UserInfo.java | 4 +- .../tabcompleters/BlacklistTabCompleter.java | 40 ++++ .../RemoveDimensionTabCompleter.java | 26 --- src/main/resources/plugin.yml | 19 +- .../sethomestwo/commands/BlacklistTest.java | 155 +++++++++++++ 11 files changed, 418 insertions(+), 247 deletions(-) delete mode 100644 src/main/java/com/samleighton/sethomestwo/commands/AddDimensionToBlacklist.java create mode 100644 src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java delete mode 100644 src/main/java/com/samleighton/sethomestwo/commands/GetBlacklistedDimensions.java delete mode 100644 src/main/java/com/samleighton/sethomestwo/commands/RemoveDimensionFromBlacklist.java create mode 100644 src/main/java/com/samleighton/sethomestwo/tabcompleters/BlacklistTabCompleter.java delete mode 100644 src/main/java/com/samleighton/sethomestwo/tabcompleters/RemoveDimensionTabCompleter.java create mode 100644 src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java diff --git a/README.md b/README.md index 1cf30b6..606df93 100644 --- a/README.md +++ b/README.md @@ -84,9 +84,9 @@ Afterwards, remove the old jar. Set Homes Two provides `/sethome`, `/home` and ` | --- | --- | | `/set-max-homes [group] ` | Sets the home limit, per LuckPerms group or server-wide. | | `/get-player-homes ` | Lists another player's homes. | -| `/add-to-blacklist ` | Stops homes being set in a dimension. | -| `/remove-from-blacklist ` | Lifts the restriction again. | -| `/get-blacklisted-dimensions` | Shows which dimensions are blacklisted. | +| `/blacklist add ` (alias `/add-to-blacklist`) | Stops homes being set in a dimension. | +| `/blacklist remove ` (alias `/remove-from-blacklist`) | Lifts the restriction again. | +| `/blacklist list` (alias `/get-blacklisted-dimensions`) | Shows which dimensions are blacklisted. | | `/import-homes [confirm]` | Imports homes from another plugin. Dry-run unless `confirm` is given. | diff --git a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java index b7f1d04..6bbb53e 100644 --- a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java +++ b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java @@ -13,10 +13,9 @@ import com.samleighton.sethomestwo.models.TeleportAttempt; import com.samleighton.sethomestwo.updates.GitHubReleaseSource; import com.samleighton.sethomestwo.updates.UpdateChecker; -import com.samleighton.sethomestwo.tabcompleters.DimensionTabCompleter; +import com.samleighton.sethomestwo.tabcompleters.BlacklistTabCompleter; import com.samleighton.sethomestwo.tabcompleters.HomesTabCompleter; import com.samleighton.sethomestwo.tabcompleters.MaterialsTabCompleter; -import com.samleighton.sethomestwo.tabcompleters.RemoveDimensionTabCompleter; import com.samleighton.sethomestwo.utils.ConfigUtil; import com.samleighton.sethomestwo.utils.DatabaseUtil; import com.samleighton.sethomestwo.utils.PermissionOverrides; @@ -177,16 +176,9 @@ public void registerCommands() { deleteHome.setExecutor(new DeleteHome()); deleteHome.setTabCompleter(new HomesTabCompleter()); - PluginCommand addToBlacklist = Objects.requireNonNull(this.getCommand("add-to-blacklist")); - addToBlacklist.setExecutor(new AddDimensionToBlacklist()); - addToBlacklist.setTabCompleter(new DimensionTabCompleter()); - - PluginCommand removeFromBlacklist = Objects.requireNonNull(this.getCommand("remove-from-blacklist")); - removeFromBlacklist.setExecutor(new RemoveDimensionFromBlacklist()); - removeFromBlacklist.setTabCompleter(new RemoveDimensionTabCompleter()); - - PluginCommand getBlacklistedDimensions = Objects.requireNonNull(this.getCommand("get-blacklisted-dimensions")); - getBlacklistedDimensions.setExecutor(new GetBlacklistedDimensions()); + PluginCommand blacklist = Objects.requireNonNull(this.getCommand("blacklist")); + blacklist.setExecutor(new Blacklist()); + blacklist.setTabCompleter(new BlacklistTabCompleter()); PluginCommand getPlayerHomes = Objects.requireNonNull(this.getCommand("get-player-homes")); getPlayerHomes.setExecutor(new GetPlayerHomes(this)); diff --git a/src/main/java/com/samleighton/sethomestwo/commands/AddDimensionToBlacklist.java b/src/main/java/com/samleighton/sethomestwo/commands/AddDimensionToBlacklist.java deleted file mode 100644 index 9a0a86a..0000000 --- a/src/main/java/com/samleighton/sethomestwo/commands/AddDimensionToBlacklist.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.samleighton.sethomestwo.commands; - -import com.samleighton.sethomestwo.dao.BlacklistDao; -import com.samleighton.sethomestwo.dao.Dao; -import com.samleighton.sethomestwo.enums.DebugLevel; -import com.samleighton.sethomestwo.enums.UserError; -import com.samleighton.sethomestwo.enums.UserInfo; -import com.samleighton.sethomestwo.enums.UserSuccess; -import com.samleighton.sethomestwo.utils.ChatUtils; -import com.samleighton.sethomestwo.utils.ConfigUtil; -import com.samleighton.sethomestwo.utils.ServerUtil; -import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -public class AddDimensionToBlacklist implements CommandExecutor { - - public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, String[] args) { - if (!(commandSender instanceof Player)) { - commandSender.sendMessage(UserError.PLAYERS_ONLY.getValue()); - return false; - } - - Player player = (Player) commandSender; - - // Permission guard - if(!player.hasPermission("sh2.add-to-blacklist")){ - ChatUtils.invalidPermissions(player); - return true; - } - - // Args length guard - if (args.length < 1) { - ChatUtils.incorrectNumArguments(player); - ChatUtils.sendInfo(player, UserInfo.ADD_TO_BLACKLIST_USAGE.getValue()); - return true; - } - - Dao blacklistDao = new BlacklistDao(); - List blacklistedDimensions = blacklistDao.getAll(); - - for (String dimension : args) { - if (!ServerUtil.getValidDimensions().contains(dimension)) { - ChatUtils.sendError(player, String.format(UserError.INVALID_DIMENSION.getValue(), dimension)); - continue; - } - - if(blacklistedDimensions.contains(dimension)) { - ChatUtils.sendError(player, String.format(UserError.DIMENSION_ALREADY_BLACKLISTED.getValue(), dimension)); - continue; - } - - boolean success = blacklistDao.save(dimension); - // Successful addition of blacklist guard - if (!success && ConfigUtil.getDebugLevel().equals(DebugLevel.INFO)) { - Bukkit.getLogger().info(String.format("Failed to add dimension to blacklist. %s", dimension)); - } - - ChatUtils.sendSuccess(player, String.format(UserSuccess.DIMENSION_ADDED_TO_BLACKLIST.getValue(), dimension)); - } - - return true; - } -} diff --git a/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java b/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java new file mode 100644 index 0000000..bd5c6ab --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java @@ -0,0 +1,205 @@ +package com.samleighton.sethomestwo.commands; + +import com.samleighton.sethomestwo.dao.BlacklistDao; +import com.samleighton.sethomestwo.dao.Dao; +import com.samleighton.sethomestwo.enums.DebugLevel; +import com.samleighton.sethomestwo.enums.PluginError; +import com.samleighton.sethomestwo.enums.UserError; +import com.samleighton.sethomestwo.enums.UserInfo; +import com.samleighton.sethomestwo.enums.UserSuccess; +import com.samleighton.sethomestwo.utils.ChatUtils; +import com.samleighton.sethomestwo.utils.ConfigUtil; +import com.samleighton.sethomestwo.utils.ServerUtil; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; + +/** + * Folds add-to-blacklist, remove-from-blacklist, and get-blacklisted-dimensions + * into one executor. Bukkit only hands onCommand the label the player actually + * typed (add-to-blacklist, remove-from-blacklist, get-blacklisted-dimensions, or + * blacklist), so the subcommand is inferred from that label rather than always + * reading args[0] - otherwise "/add-to-blacklist world_nether", the exact + * pre-existing usage this command replaces, would silently fail. + */ +public class Blacklist implements CommandExecutor { + + private static final List SUBCOMMANDS = Arrays.asList("add", "remove", "list"); + + @Override + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String label, String[] args) { + if (!(commandSender instanceof Player)) { + commandSender.sendMessage(UserError.PLAYERS_ONLY.getValue()); + return true; + } + + Player player = (Player) commandSender; + + String subcommand; + String[] names; + + switch (label.toLowerCase()) { + case "add-to-blacklist": + if (isExplicitSubcommand(args)) { + subcommand = args[0].toLowerCase(); + names = Arrays.copyOfRange(args, 1, args.length); + } else { + subcommand = "add"; + names = args; + } + break; + case "remove-from-blacklist": + if (isExplicitSubcommand(args)) { + subcommand = args[0].toLowerCase(); + names = Arrays.copyOfRange(args, 1, args.length); + } else { + subcommand = "remove"; + names = args; + } + break; + case "get-blacklisted-dimensions": + subcommand = "list"; + names = args; + break; + default: + if (args.length < 1) { + ChatUtils.incorrectNumArguments(player); + ChatUtils.sendInfo(player, UserInfo.BLACKLIST_USAGE.getValue()); + return true; + } + subcommand = args[0].toLowerCase(); + names = Arrays.copyOfRange(args, 1, args.length); + break; + } + + switch (subcommand) { + case "add": + return add(player, names); + case "remove": + return remove(player, names); + case "list": + return list(player, names); + default: + ChatUtils.sendInfo(player, UserInfo.BLACKLIST_USAGE.getValue()); + return true; + } + } + + /** + * True when the alias's first argument is itself one of add/remove/list, so + * "/add-to-blacklist add world_nether" is honoured instead of blacklisting a + * world named "add". + */ + private boolean isExplicitSubcommand(String[] args) { + return args.length >= 1 && SUBCOMMANDS.contains(args[0].toLowerCase()); + } + + private boolean add(Player player, String[] dimensions) { + if (!player.hasPermission("sh2.add-to-blacklist")) { + ChatUtils.invalidPermissions(player); + return true; + } + + if (dimensions.length < 1) { + ChatUtils.incorrectNumArguments(player); + ChatUtils.sendInfo(player, UserInfo.BLACKLIST_USAGE.getValue()); + return true; + } + + Dao blacklistDao = new BlacklistDao(); + List blacklistedDimensions = blacklistDao.getAll(); + + for (String dimension : dimensions) { + if (!ServerUtil.getValidDimensions().contains(dimension)) { + ChatUtils.sendError(player, String.format(UserError.INVALID_DIMENSION.getValue(), dimension)); + continue; + } + + if (blacklistedDimensions.contains(dimension)) { + ChatUtils.sendError(player, String.format(UserError.DIMENSION_ALREADY_BLACKLISTED.getValue(), dimension)); + continue; + } + + boolean success = blacklistDao.save(dimension); + if (!success && ConfigUtil.getDebugLevel().equals(DebugLevel.INFO)) { + Bukkit.getLogger().info(String.format("Failed to add dimension to blacklist. %s", dimension)); + } + + ChatUtils.sendSuccess(player, String.format(UserSuccess.DIMENSION_ADDED_TO_BLACKLIST.getValue(), dimension)); + } + + return true; + } + + private boolean remove(Player player, String[] dimensions) { + if (!player.hasPermission("sh2.remove-from-blacklist")) { + ChatUtils.invalidPermissions(player); + return true; + } + + if (dimensions.length < 1) { + ChatUtils.incorrectNumArguments(player); + ChatUtils.sendInfo(player, UserInfo.BLACKLIST_USAGE.getValue()); + return true; + } + + Dao blacklistDao = new BlacklistDao(); + List blacklistedDimensions = blacklistDao.getAll(); + + for (String dimension : dimensions) { + if (!ServerUtil.getValidDimensions().contains(dimension)) { + ChatUtils.sendError(player, String.format(UserError.INVALID_DIMENSION.getValue(), dimension)); + continue; + } + + if (!blacklistedDimensions.contains(dimension)) { + ChatUtils.sendError(player, String.format(UserError.DIMENSION_IS_NOT_BLACKLISTED.getValue(), dimension)); + continue; + } + + boolean success = blacklistDao.delete(dimension); + if (!success && ConfigUtil.getDebugLevel().equals(DebugLevel.ERROR)) { + Bukkit.getLogger().info(String.format("Failed to remove dimension from blacklist. %s", dimension)); + ChatUtils.sendError(player, PluginError.REMOVE_DIMENSION_FAILED.getValue()); + } + + ChatUtils.sendSuccess(player, String.format( + ConfigUtil.getConfig().getString("dimensionRemovedFromBlacklist", UserSuccess.DIMENSION_REMOVED_FROM_BLACKLIST.getValue()), + dimension + )); + } + + return true; + } + + private boolean list(Player player, String[] extraArgs) { + if (!player.hasPermission("sh2.get-blacklisted-dimensions")) { + ChatUtils.invalidPermissions(player); + return true; + } + + if (extraArgs.length > 0) { + ChatUtils.incorrectNumArguments(player); + ChatUtils.sendInfo(player, UserInfo.BLACKLIST_USAGE.getValue()); + return true; + } + + Dao blacklistDao = new BlacklistDao(); + List blacklistedDimensions = blacklistDao.getAll(); + + if (blacklistedDimensions.isEmpty()) { + ChatUtils.sendInfo(player, UserInfo.NO_BLACKLISTED_DIMENSIONS.getValue()); + return true; + } + + String blacklist = "Blacklisted Dimensions: " + blacklistedDimensions; + ChatUtils.sendInfo(player, blacklist); + return true; + } +} diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GetBlacklistedDimensions.java b/src/main/java/com/samleighton/sethomestwo/commands/GetBlacklistedDimensions.java deleted file mode 100644 index 0b0048c..0000000 --- a/src/main/java/com/samleighton/sethomestwo/commands/GetBlacklistedDimensions.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.samleighton.sethomestwo.commands; - -import com.samleighton.sethomestwo.dao.BlacklistDao; -import com.samleighton.sethomestwo.dao.Dao; -import com.samleighton.sethomestwo.enums.UserError; -import com.samleighton.sethomestwo.enums.UserInfo; -import com.samleighton.sethomestwo.utils.ChatUtils; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -public class GetBlacklistedDimensions implements CommandExecutor { - public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, String[] args) { - if (!(commandSender instanceof Player)) { - commandSender.sendMessage(UserError.PLAYERS_ONLY.getValue()); - return true; - } - - Player player = (Player) commandSender; - - // Permission guard - if(!player.hasPermission("sh2.get-blacklisted-dimensions")){ - ChatUtils.invalidPermissions(player); - return true; - } - - // Args length guard - if (args.length > 0) { - ChatUtils.incorrectNumArguments(player); - ChatUtils.sendInfo(player, UserInfo.GET_BLACKLIST_USAGE.getValue()); - return true; - } - - Dao blacklistDao = new BlacklistDao(); - List blacklistedDimensions = blacklistDao.getAll(); - - if (blacklistedDimensions.isEmpty()) { - ChatUtils.sendInfo(player, UserInfo.NO_BLACKLISTED_DIMENSIONS.getValue()); - return true; - } - - String blacklist = "Blacklisted Dimensions: " + blacklistedDimensions; - ChatUtils.sendInfo(player, blacklist); - return true; - } -} diff --git a/src/main/java/com/samleighton/sethomestwo/commands/RemoveDimensionFromBlacklist.java b/src/main/java/com/samleighton/sethomestwo/commands/RemoveDimensionFromBlacklist.java deleted file mode 100644 index 3a81597..0000000 --- a/src/main/java/com/samleighton/sethomestwo/commands/RemoveDimensionFromBlacklist.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.samleighton.sethomestwo.commands; - -import com.samleighton.sethomestwo.dao.BlacklistDao; -import com.samleighton.sethomestwo.dao.Dao; -import com.samleighton.sethomestwo.enums.*; -import com.samleighton.sethomestwo.utils.ChatUtils; -import com.samleighton.sethomestwo.utils.ConfigUtil; -import com.samleighton.sethomestwo.utils.ServerUtil; -import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -public class RemoveDimensionFromBlacklist implements CommandExecutor { - public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, String[] args) { - // Sender must be player guard - if (!(commandSender instanceof Player)) { - commandSender.sendMessage(UserError.PLAYERS_ONLY.getValue()); - return true; - } - - Player player = (Player) commandSender; - - // Permission guard - if(!player.hasPermission("sh2.remove-from-blacklist")){ - ChatUtils.invalidPermissions(player); - return true; - } - - // Args length guard - if (args.length < 1) { - ChatUtils.incorrectNumArguments(player); - ChatUtils.sendInfo(player, UserInfo.REMOVE_FROM_BLACKLIST_USAGE.getValue()); - return true; - } - - Dao blacklistDao = new BlacklistDao(); - List blacklistedDimensions = blacklistDao.getAll(); - - // Loop over inputs assuming each is a dimension - for (String dimension : args) { - // Valid dimension guard - if (!ServerUtil.getValidDimensions().contains(dimension)) { - ChatUtils.sendError(player, String.format(UserError.INVALID_DIMENSION.getValue(), dimension)); - continue; - } - - // Currently in blacklist guard - if(!blacklistedDimensions.contains(dimension)) { - ChatUtils.sendError(player, String.format(UserError.DIMENSION_IS_NOT_BLACKLISTED.getValue(), dimension)); - continue; - } - - // Perform delete action and obtain result - boolean success = blacklistDao.delete(dimension); - - // Guard for successful addition of blacklist - if (!success && ConfigUtil.getDebugLevel().equals(DebugLevel.ERROR)) { - Bukkit.getLogger().info(String.format("Failed to remove dimension from blacklist. %s", dimension)); - ChatUtils.sendError(player, PluginError.REMOVE_DIMENSION_FAILED.getValue()); - } - - ChatUtils.sendSuccess(player, String.format( - ConfigUtil.getConfig().getString("dimensionRemovedFromBlacklist", UserSuccess.DIMENSION_REMOVED_FROM_BLACKLIST.getValue()), - dimension - )); - } - - return true; - } -} diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java b/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java index 9b5705c..ba7b4ed 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java @@ -2,9 +2,7 @@ public enum UserInfo { GET_PLAYER_HOMES_USAGE("Usage: /get-player-homes [playerName]"), - GET_BLACKLIST_USAGE("Usage: /get-blacklisted-dimensions"), - REMOVE_FROM_BLACKLIST_USAGE("Usage: /remove-from-blacklist [dimension names]"), - ADD_TO_BLACKLIST_USAGE("Usage: /add-to-blacklist [dimension names]"), + BLACKLIST_USAGE("Usage: /blacklist [world]"), CREATE_HOME_USAGE("Usage: /create-home [name] [display_material | d | default] [description]"), NO_HOMES("You have not setup any homes yet, you can use the /create-home command to create one."), NO_MAX_HOMES("There is no max number of homes."), diff --git a/src/main/java/com/samleighton/sethomestwo/tabcompleters/BlacklistTabCompleter.java b/src/main/java/com/samleighton/sethomestwo/tabcompleters/BlacklistTabCompleter.java new file mode 100644 index 0000000..f01f6a8 --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/tabcompleters/BlacklistTabCompleter.java @@ -0,0 +1,40 @@ +package com.samleighton.sethomestwo.tabcompleters; + +import com.samleighton.sethomestwo.dao.BlacklistDao; +import com.samleighton.sethomestwo.utils.ServerUtil; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.util.StringUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class BlacklistTabCompleter implements TabCompleter { + + @Nullable + @Override + public List onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + List completions = new ArrayList<>(); + + // Only the canonical name takes a subcommand argument; the old names go + // straight to world names, matching how the executor reads them. + boolean canonical = "blacklist".equalsIgnoreCase(label); + + if (canonical && args.length == 1) { + StringUtil.copyPartialMatches(args[0], Arrays.asList("add", "remove", "list"), completions); + return completions; + } + + boolean removing = "remove-from-blacklist".equalsIgnoreCase(label) + || (canonical && args.length > 0 && "remove".equalsIgnoreCase(args[0])); + List source = removing ? new BlacklistDao().getAll() : ServerUtil.getValidDimensions(); + + String lastArg = args.length == 0 ? "" : args[args.length - 1]; + StringUtil.copyPartialMatches(lastArg, source, completions); + return completions; + } +} diff --git a/src/main/java/com/samleighton/sethomestwo/tabcompleters/RemoveDimensionTabCompleter.java b/src/main/java/com/samleighton/sethomestwo/tabcompleters/RemoveDimensionTabCompleter.java deleted file mode 100644 index 9019905..0000000 --- a/src/main/java/com/samleighton/sethomestwo/tabcompleters/RemoveDimensionTabCompleter.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.samleighton.sethomestwo.tabcompleters; - -import com.samleighton.sethomestwo.dao.BlacklistDao; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; -import org.bukkit.util.StringUtil; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; - -public class RemoveDimensionTabCompleter implements TabCompleter { - @Nullable - @Override - public List onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] args) { - List completions = new ArrayList<>(); - - for(String arg : args){ - StringUtil.copyPartialMatches(arg, new BlacklistDao().getAll(), completions); - } - - return completions; - } -} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 9712c82..67cdbd5 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -20,15 +20,16 @@ commands: permission: sh2.delete-home aliases: - delhome - add-to-blacklist: - description: Add dimensions to be blacklisted from setting homes. - permission: sh2.add-to-blacklist - remove-from-blacklist: - description: Remove dimensions from blacklist table. - permission: sh2.remove-from-blacklist - get-blacklisted-dimensions: - description: Retrieves a list of the blacklisted dimensions. - permission: sh2.get-blacklisted-dimensions + blacklist: + description: Manage the worlds in which homes are disallowed. + # Semicolon-separated permissions are OR'd by Bukkit's own Command.testPermission, + # so the command is reachable by a holder of any one of the three nodes; add and + # remove are still gated on their own node inside the executor. + permission: sh2.add-to-blacklist;sh2.remove-from-blacklist;sh2.get-blacklisted-dimensions + aliases: + - add-to-blacklist + - remove-from-blacklist + - get-blacklisted-dimensions get-player-homes: description: Retrieves a list of a given player's homes. permission: sh2.get-player-homes diff --git a/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java b/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java new file mode 100644 index 0000000..843940c --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java @@ -0,0 +1,155 @@ +package com.samleighton.sethomestwo.commands; + +import com.samleighton.sethomestwo.dao.BlacklistDao; +import com.samleighton.sethomestwo.support.HomeFixtures; +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BlacklistTest extends ServerTestBase { + + @Test + void addStoresTheWorld() { + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.add-to-blacklist", true); + + server.execute("blacklist", player, "add", "world_nether").assertSucceeded(); + + assertTrue(new BlacklistDao().getAll().contains("world_nether")); + } + + @Test + void removeDropsTheWorld() { + HomeFixtures.blacklist("world_nether"); + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.remove-from-blacklist", true); + + server.execute("blacklist", player, "remove", "world_nether").assertSucceeded(); + + assertFalse(new BlacklistDao().getAll().contains("world_nether")); + } + + @Test + void listPrintsTheEntries() { + HomeFixtures.blacklist("world_nether"); + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.get-blacklisted-dimensions", true); + + server.execute("blacklist", player, "list").assertSucceeded(); + + assertTrue(player.nextMessage().contains("world_nether")); + } + + @Test + void addIsRefusedWithoutItsOwnNode() { + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.get-blacklisted-dimensions", true); + player.addAttachment(plugin, "sh2.add-to-blacklist", false); + + server.execute("blacklist", player, "add", "world_nether").assertSucceeded(); + + assertTrue(player.nextMessage().contains("permission")); + assertFalse(new BlacklistDao().getAll().contains("world_nether")); + } + + @Test + void removeIsRefusedWithoutItsOwnNode() { + HomeFixtures.blacklist("world_nether"); + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.get-blacklisted-dimensions", true); + player.addAttachment(plugin, "sh2.remove-from-blacklist", false); + + server.execute("blacklist", player, "remove", "world_nether").assertSucceeded(); + + assertTrue(player.nextMessage().contains("permission")); + assertTrue(new BlacklistDao().getAll().contains("world_nether")); + } + + @Test + void listIsRefusedWithoutItsOwnNode() { + HomeFixtures.blacklist("world_nether"); + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.add-to-blacklist", true); + player.addAttachment(plugin, "sh2.remove-from-blacklist", true); + player.addAttachment(plugin, "sh2.get-blacklisted-dimensions", false); + + server.execute("blacklist", player, "list").assertSucceeded(); + + assertTrue(player.nextMessage().contains("permission")); + } + + @Test + void theOldCommandNameStillWorks() { + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.add-to-blacklist", true); + + server.execute("add-to-blacklist", player, "add", "world_nether").assertSucceeded(); + + assertTrue(new BlacklistDao().getAll().contains("world_nether")); + } + + @Test + void anUnknownWorldIsRejected() { + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.add-to-blacklist", true); + + server.execute("blacklist", player, "add", "not_a_world").assertSucceeded(); + + assertTrue(player.nextMessage().contains("not a valid")); + assertTrue(new BlacklistDao().getAll().isEmpty()); + } + + // The old command names arrive at onCommand with no subcommand token at + // all (e.g. "/add-to-blacklist world_nether"), unlike the new "blacklist" + // name, which always expects one. Bukkit hands onCommand the exact label + // the player typed only when the command is dispatched through the real + // command line, so these use server.dispatchCommand rather than + // server.execute, which always reports the canonical command name as the + // label regardless of which alias was used to look it up. + + @Test + void bareAddToBlacklistAliasWithNoSubcommandStillAdds() { + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.add-to-blacklist", true); + + server.dispatchCommand(player, "add-to-blacklist world_nether"); + + assertTrue(new BlacklistDao().getAll().contains("world_nether")); + } + + @Test + void bareRemoveFromBlacklistAliasWithNoSubcommandStillRemoves() { + HomeFixtures.blacklist("world_nether"); + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.remove-from-blacklist", true); + + server.dispatchCommand(player, "remove-from-blacklist world_nether"); + + assertFalse(new BlacklistDao().getAll().contains("world_nether")); + } + + @Test + void bareGetBlacklistedDimensionsAliasListsEntries() { + HomeFixtures.blacklist("world_nether"); + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.get-blacklisted-dimensions", true); + + server.dispatchCommand(player, "get-blacklisted-dimensions"); + + assertTrue(player.nextMessage().contains("world_nether")); + } + + @Test + void explicitSubcommandViaOldAliasIsNotTreatedAsAWorldName() { + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.add-to-blacklist", true); + + server.dispatchCommand(player, "add-to-blacklist add world_nether"); + + assertTrue(new BlacklistDao().getAll().contains("world_nether")); + assertFalse(new BlacklistDao().getAll().contains("add")); + } +} From 9bd7b8e9df411be7037ee3a3156165d07db16b5f Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 01:39:53 -0400 Subject: [PATCH 14/75] feat: add the move-home command Extract HomeActionsGui's inline move-home mutation into a public applyMove/MoveOutcome pair, mirroring the applyRename/RenameOutcome pattern already used for the anvil rename prompt. The GUI keeps its own navigation and messaging; the new /move-home command (alias /uhome) reuses the same config keys and enum defaults so a customised message shows identically from both paths. --- .../samleighton/sethomestwo/SetHomesTwo.java | 4 + .../sethomestwo/commands/MoveHome.java | 61 +++++++++++++++ .../sethomestwo/enums/UserInfo.java | 3 +- .../sethomestwo/gui/HomeActionsGui.java | 72 +++++++++++------- src/main/resources/plugin.yml | 9 +++ .../sethomestwo/commands/MoveHomeTest.java | 74 +++++++++++++++++++ 6 files changed, 196 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/samleighton/sethomestwo/commands/MoveHome.java create mode 100644 src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java diff --git a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java index 6bbb53e..52c227c 100644 --- a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java +++ b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java @@ -188,6 +188,10 @@ public void registerCommands() { PluginCommand importHomes = Objects.requireNonNull(this.getCommand("import-homes")); importHomes.setExecutor(new ImportHomes()); + + PluginCommand moveHome = Objects.requireNonNull(this.getCommand("move-home")); + moveHome.setExecutor(new MoveHome()); + moveHome.setTabCompleter(new HomesTabCompleter()); } /** diff --git a/src/main/java/com/samleighton/sethomestwo/commands/MoveHome.java b/src/main/java/com/samleighton/sethomestwo/commands/MoveHome.java new file mode 100644 index 0000000..aaea642 --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/commands/MoveHome.java @@ -0,0 +1,61 @@ +package com.samleighton.sethomestwo.commands; + +import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.enums.UserError; +import com.samleighton.sethomestwo.enums.UserInfo; +import com.samleighton.sethomestwo.enums.UserSuccess; +import com.samleighton.sethomestwo.gui.HomeActionsGui; +import com.samleighton.sethomestwo.models.Home; +import com.samleighton.sethomestwo.utils.ChatUtils; +import com.samleighton.sethomestwo.utils.ConfigUtil; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class MoveHome implements CommandExecutor { + + @Override + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String label, String[] args) { + if (!(commandSender instanceof Player)) { + commandSender.sendMessage(UserError.PLAYERS_ONLY.getValue()); + return true; + } + + Player player = (Player) commandSender; + + if (!player.hasPermission("sh2.move-home")) { + ChatUtils.invalidPermissions(player); + return true; + } + + if (args.length != 1) { + ChatUtils.incorrectNumArguments(player); + ChatUtils.sendInfo(player, UserInfo.MOVE_HOME_USAGE.getValue()); + return true; + } + + Home home = new HomesDao().get(player.getUniqueId(), args[0]); + + switch (HomeActionsGui.applyMove(player, home)) { + case GONE: + ChatUtils.sendError(player, ConfigUtil.getConfig().getString( + "homeNoLongerExists", UserError.HOME_NO_LONGER_EXISTS.getValue())); + break; + case BLACKLISTED: + ChatUtils.sendError(player, ConfigUtil.getConfig().getString( + "cannotMoveToBlacklistedDimension", UserError.CANNOT_MOVE_TO_BLACKLISTED_DIMENSION.getValue())); + break; + case UPDATE_FAILED: + ChatUtils.pluginError(player); + break; + case MOVED: + String moved = ConfigUtil.getConfig().getString("homeMoved", UserSuccess.HOME_MOVED.getValue()); + ChatUtils.sendSuccess(player, String.format(moved, home.getName())); + break; + } + + return true; + } +} diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java b/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java index ba7b4ed..352caab 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java @@ -7,7 +7,8 @@ public enum UserInfo { NO_HOMES("You have not setup any homes yet, you can use the /create-home command to create one."), NO_MAX_HOMES("There is no max number of homes."), NO_BLACKLISTED_DIMENSIONS("No dimensions are blacklisted"), - MOVED_TO_SAFE_SPOT("Your home was not safe to stand in, so you were moved to the nearest safe spot."); + MOVED_TO_SAFE_SPOT("Your home was not safe to stand in, so you were moved to the nearest safe spot."), + MOVE_HOME_USAGE("Usage: /move-home "); private final String value; diff --git a/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java b/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java index 35ccfba..96faaca 100644 --- a/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java +++ b/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java @@ -63,6 +63,17 @@ enum RenameOutcome { RENAMED } + /** + * Why a move ended the way it did. Public so the move-home command, which + * lives in a different package, can share the rule the GUI button applies. + */ + public enum MoveOutcome { + GONE, + BLACKLISTED, + UPDATE_FAILED, + MOVED + } + private final Inventory inv; private final int homeId; private boolean confirmingDelete = false; @@ -162,33 +173,21 @@ public void onClick(InventoryClickEvent event, GuiSession session) { Home fresh = reloadHome(player, session); if (fresh == null) return; - Location destination = player.getLocation(); - String destinationDimension = Objects.requireNonNull(destination.getWorld()).getEnvironment().toString(); - - // Blacklisted dimension guard, sharing the rule create-home applies. - if (ServerUtil.isWorldBlacklisted(destination.getWorld())) { - ChatUtils.sendError(player, ConfigUtil.getConfig().getString("cannotMoveToBlacklistedDimension", UserError.CANNOT_MOVE_TO_BLACKLISTED_DIMENSION.getValue())); - return; - } - - fresh.setWorld(destination.getWorld().getUID().toString()); - fresh.setX(destination.getX()); - fresh.setY(destination.getY()); - fresh.setZ(destination.getZ()); - fresh.setPitch(destination.getPitch()); - fresh.setYaw(destination.getYaw()); - fresh.setDimension(destinationDimension); - - HomesDao homesDao = new HomesDao(); - if (!homesDao.update(fresh)) { - ChatUtils.pluginError(player); - return; + switch (applyMove(player, fresh)) { + case BLACKLISTED: + ChatUtils.sendError(player, ConfigUtil.getConfig().getString("cannotMoveToBlacklistedDimension", UserError.CANNOT_MOVE_TO_BLACKLISTED_DIMENSION.getValue())); + return; + case UPDATE_FAILED: + ChatUtils.pluginError(player); + return; + case MOVED: + String moved = ConfigUtil.getConfig().getString("homeMoved", UserSuccess.HOME_MOVED.getValue()); + ChatUtils.sendSuccess(player, String.format(moved, fresh.getName())); + returnToRefreshedList(player, session); + return; + default: + return; } - - String moved = ConfigUtil.getConfig().getString("homeMoved", UserSuccess.HOME_MOVED.getValue()); - ChatUtils.sendSuccess(player, String.format(moved, fresh.getName())); - returnToRefreshedList(player, session); - return; } if (ACTION_ICON.equals(action)) { @@ -298,6 +297,27 @@ RenameOutcome applyRename(Player player, Home home, String rawName, int maxLengt return RenameOutcome.RENAMED; } + /** + * Moves a home to the player's current location, holding only the mutation: + * no messaging, no navigation. Those stay with each caller. + */ + public static MoveOutcome applyMove(Player player, Home home) { + if (home == null) return MoveOutcome.GONE; + + Location destination = player.getLocation(); + if (ServerUtil.isWorldBlacklisted(destination.getWorld())) return MoveOutcome.BLACKLISTED; + + home.setWorld(Objects.requireNonNull(destination.getWorld()).getUID().toString()); + home.setX(destination.getX()); + home.setY(destination.getY()); + home.setZ(destination.getZ()); + home.setPitch(destination.getPitch()); + home.setYaw(destination.getYaw()); + home.setDimension(destination.getWorld().getEnvironment().toString()); + + return new HomesDao().update(home) ? MoveOutcome.MOVED : MoveOutcome.UPDATE_FAILED; + } + /** * Open an anvil prompt for the new home name. Validation failures re-prompt * with the reason rather than closing. diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 67cdbd5..2a1375d 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -50,6 +50,11 @@ commands: import-homes: description: Import homes from another homes plugin (dry-run unless confirmed). permission: sh2.import-homes + move-home: + description: Move one of your homes to where you are standing. + permission: sh2.move-home + aliases: + - uhome permissions: sh2.create-home: description: Allow player to create homes. @@ -72,6 +77,9 @@ permissions: sh2.manage-homes: description: Allow player to rename, move, re-icon, and delete their homes from the GUI. default: true + sh2.move-home: + description: Allow a player to move their own homes by command. + default: true sh2.add-to-blacklist: description: Add dimensions to the blacklist. default: op @@ -104,6 +112,7 @@ permissions: sh2.teleport: true sh2.give-homes-item: true sh2.manage-homes: true + sh2.move-home: true sh2.admin: description: Everything a server administrator needs, including the player nodes. default: op diff --git a/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java new file mode 100644 index 0000000..2d1264e --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java @@ -0,0 +1,74 @@ +package com.samleighton.sethomestwo.commands; + +import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.models.Home; +import com.samleighton.sethomestwo.support.HomeFixtures; +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.Location; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MoveHomeTest extends ServerTestBase { + + @Test + void theHomeMovesToThePlayersLocation() { + PlayerMock player = addPlayer(); + HomeFixtures.persist(player, "base"); + player.teleport(new Location(overworld, 100, 70, -40)); + + server.execute("move-home", player, "base").assertSucceeded(); + + Home moved = new HomesDao().getAll(player.getUniqueId()).get(0); + assertEquals(100.0, moved.getX()); + assertEquals(-40.0, moved.getZ()); + } + + @Test + void anUnknownHomeIsReported() { + PlayerMock player = addPlayer(); + + server.execute("move-home", player, "nope").assertSucceeded(); + + assertTrue(player.nextMessage().contains("no longer exists")); + } + + @Test + void withoutPermissionTheCommandIsRefused() { + PlayerMock player = addPlayer(); + HomeFixtures.persist(player, "base"); + player.addAttachment(plugin, "sh2.move-home", false); + Location before = player.getLocation(); + player.teleport(new Location(overworld, 100, 70, -40)); + + server.execute("move-home", player, "base").assertSucceeded(); + + assertTrue(player.nextMessage().contains("permission")); + assertEquals(before.getX(), new HomesDao().getAll(player.getUniqueId()).get(0).getX()); + } + + @Test + void movingIntoABlacklistedWorldIsRefused() { + PlayerMock player = addPlayer(); + HomeFixtures.persist(player, "base"); + HomeFixtures.blacklist("world_nether"); + player.teleport(new Location(nether, 10, 70, 10)); + + server.execute("move-home", player, "base").assertSucceeded(); + + assertTrue(player.nextMessage().contains("blacklisted")); + } + + @Test + void theAliasWorks() { + PlayerMock player = addPlayer(); + HomeFixtures.persist(player, "base"); + player.teleport(new Location(overworld, 5, 70, 5)); + + server.execute("uhome", player, "base").assertSucceeded(); + + assertEquals(5.0, new HomesDao().getAll(player.getUniqueId()).get(0).getX()); + } +} From 800284fa185fa6fdc07122b19925c76542554bb2 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 10:20:25 -0400 Subject: [PATCH 15/75] test: cover cross-world moves and the move-home usage message The move rewrites world and dimension, but nothing asserted it. Deleting both writes left the whole suite green, so a cross-dimension move could have silently kept the old world. --- .../sethomestwo/commands/MoveHomeTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java index 2d1264e..3f42fb9 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java @@ -71,4 +71,27 @@ void theAliasWorks() { assertEquals(5.0, new HomesDao().getAll(player.getUniqueId()).get(0).getX()); } + + @Test + void movingAcrossWorldsRewritesTheWorldAndDimension() { + PlayerMock player = addPlayer(); + HomeFixtures.persist(player, "base"); + player.teleport(new Location(nether, 8, 70, 8)); + + server.execute("move-home", player, "base").assertSucceeded(); + + Home moved = new HomesDao().getAll(player.getUniqueId()).get(0); + assertEquals(nether.getUID().toString(), moved.getWorld()); + assertEquals("NETHER", moved.getDimension()); + } + + @Test + void theWrongNumberOfArgumentsShowsTheUsage() { + PlayerMock player = addPlayer(); + + server.execute("move-home", player).assertSucceeded(); + + assertTrue(player.nextMessage().contains("Incorrect number of arguments")); + assertTrue(player.nextMessage().contains("Usage: /move-home ")); + } } From 448612128d24b7f429ed2e4ec48cca10e6a33d2e Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 10:23:21 -0400 Subject: [PATCH 16/75] fix: match home names without regard to case when looking one up /go-home already ignored case but /move-home and /delete-home did not, so /go-home Base found a home named base and the other two reported it missing. Creating a home already rejects a name that differs only by case, so at most one home can match. --- .../com/samleighton/sethomestwo/dao/HomesDao.java | 7 ++++++- .../com/samleighton/sethomestwo/dao/HomesDaoTest.java | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java index 85f23bc..9d8a5fe 100644 --- a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java +++ b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java @@ -86,6 +86,11 @@ public List getAll(Object... keys) { return playerHomes; } + /** + * Look a home up by owner and name. The name match ignores case, which is + * safe because {@link #nameExists} makes names unique per player ignoring + * case, so at most one home can ever match. + */ @Override public Home get(Object... keys) { UUID playerUUID = null; @@ -99,7 +104,7 @@ public Home get(Object... keys) { // Key guard if(homeName == null || playerUUID == null) return null; - String sql = "select * from %s where player_uuid = ? and name = ?"; + String sql = "select * from %s where player_uuid = ? and lower(name) = lower(?)"; ResultSet rs = DatabaseUtil.fetch(this.conn, String.format(sql, TABLE_NAME), playerUUID.toString(), homeName); if(rs == null) return null; diff --git a/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoTest.java b/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoTest.java index 57286ba..e29f205 100644 --- a/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoTest.java +++ b/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoTest.java @@ -36,6 +36,17 @@ void savedHomeComesBackFromGetAll() { assertNotNull(homes.get(0).getId()); } + @Test + void getIsCaseInsensitive() { + PlayerMock player = addPlayer(); + HomeFixtures.persist(player, "base"); + + Home found = new HomesDao().get(player.getUniqueId(), "BaSe"); + + assertNotNull(found); + assertEquals("base", found.getName()); + } + @Test void getReturnsNullForUnknownName() { PlayerMock player = addPlayer(); From 11946b7ca982090753bf75ef0f53342300245ad8 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 10:30:38 -0400 Subject: [PATCH 17/75] feat: add go-player-home, delete-player-home and move-player-home Three operator commands for working on another player's homes, with the v1 aliases home-of, delhome-of and uhome-of. Each resolves the target through ServerUtil.getPlayerUUID, so an offline player who has saved homes can be addressed by name. Renames UserError.PLAYER_NOT_ONLINE to PLAYER_NOT_FOUND and rewords it, because the name now only fails to resolve when it matches nobody at all. It is overridable as the playerNotFound config key. --- .../samleighton/sethomestwo/SetHomesTwo.java | 13 ++ .../commands/DeletePlayerHome.java | 67 +++++++ .../sethomestwo/commands/GetPlayerHomes.java | 3 +- .../sethomestwo/commands/GoPlayerHome.java | 61 +++++++ .../sethomestwo/commands/MovePlayerHome.java | 72 ++++++++ .../sethomestwo/enums/UserError.java | 2 +- .../sethomestwo/enums/UserInfo.java | 5 +- .../sethomestwo/enums/UserSuccess.java | 4 +- .../PlayerHomesTabCompleter.java | 48 +++++ src/main/resources/plugin.yml | 27 +++ .../commands/GetPlayerHomesTest.java | 2 +- .../commands/PlayerHomeAdminCommandsTest.java | 172 ++++++++++++++++++ 12 files changed, 471 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java create mode 100644 src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java create mode 100644 src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java create mode 100644 src/main/java/com/samleighton/sethomestwo/tabcompleters/PlayerHomesTabCompleter.java create mode 100644 src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java diff --git a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java index 52c227c..1cda590 100644 --- a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java +++ b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java @@ -16,6 +16,7 @@ import com.samleighton.sethomestwo.tabcompleters.BlacklistTabCompleter; import com.samleighton.sethomestwo.tabcompleters.HomesTabCompleter; import com.samleighton.sethomestwo.tabcompleters.MaterialsTabCompleter; +import com.samleighton.sethomestwo.tabcompleters.PlayerHomesTabCompleter; import com.samleighton.sethomestwo.utils.ConfigUtil; import com.samleighton.sethomestwo.utils.DatabaseUtil; import com.samleighton.sethomestwo.utils.PermissionOverrides; @@ -192,6 +193,18 @@ public void registerCommands() { PluginCommand moveHome = Objects.requireNonNull(this.getCommand("move-home")); moveHome.setExecutor(new MoveHome()); moveHome.setTabCompleter(new HomesTabCompleter()); + + PluginCommand goPlayerHome = Objects.requireNonNull(this.getCommand("go-player-home")); + goPlayerHome.setExecutor(new GoPlayerHome()); + goPlayerHome.setTabCompleter(new PlayerHomesTabCompleter()); + + PluginCommand deletePlayerHome = Objects.requireNonNull(this.getCommand("delete-player-home")); + deletePlayerHome.setExecutor(new DeletePlayerHome()); + deletePlayerHome.setTabCompleter(new PlayerHomesTabCompleter()); + + PluginCommand movePlayerHome = Objects.requireNonNull(this.getCommand("move-player-home")); + movePlayerHome.setExecutor(new MovePlayerHome()); + movePlayerHome.setTabCompleter(new PlayerHomesTabCompleter()); } /** diff --git a/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java new file mode 100644 index 0000000..bc37fc6 --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java @@ -0,0 +1,67 @@ +package com.samleighton.sethomestwo.commands; + +import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.enums.UserError; +import com.samleighton.sethomestwo.enums.UserInfo; +import com.samleighton.sethomestwo.enums.UserSuccess; +import com.samleighton.sethomestwo.models.Home; +import com.samleighton.sethomestwo.utils.ChatUtils; +import com.samleighton.sethomestwo.utils.ConfigUtil; +import com.samleighton.sethomestwo.utils.ServerUtil; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class DeletePlayerHome implements CommandExecutor { + + @Override + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String label, String[] args) { + if (!(commandSender instanceof Player)) { + commandSender.sendMessage(UserError.PLAYERS_ONLY.getValue()); + return true; + } + + Player admin = (Player) commandSender; + + if (!admin.hasPermission("sh2.delete-player-home")) { + ChatUtils.invalidPermissions(admin); + return true; + } + + if (args.length != 2) { + ChatUtils.incorrectNumArguments(admin); + ChatUtils.sendInfo(admin, UserInfo.DELETE_PLAYER_HOME_USAGE.getValue()); + return true; + } + + String uuid = ServerUtil.getPlayerUUID(args[0]); + + if (uuid == null) { + ChatUtils.sendError(admin, ConfigUtil.getConfig().getString( + "playerNotFound", UserError.PLAYER_NOT_FOUND.getValue())); + return true; + } + + HomesDao homesDao = new HomesDao(true); + Home home = homesDao.get(UUID.fromString(uuid), args[1]); + + if (home == null) { + ChatUtils.sendError(admin, ConfigUtil.getConfig().getString( + "homeNoLongerExists", UserError.HOME_NO_LONGER_EXISTS.getValue())); + return true; + } + + if (!homesDao.delete(home)) { + ChatUtils.pluginError(admin); + return true; + } + + String deleted = ConfigUtil.getConfig().getString("playerHomeDeleted", UserSuccess.PLAYER_HOME_DELETED.getValue()); + ChatUtils.sendSuccess(admin, String.format(deleted, args[0], home.getName())); + return true; + } +} diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java b/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java index 92647e5..36c601d 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java @@ -56,7 +56,8 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command // null means the name matched no online player and no stored home owner if (uuidString == null) { - ChatUtils.sendError(requester, UserError.PLAYER_NOT_ONLINE.getValue()); + ChatUtils.sendError(requester, ConfigUtil.getConfig().getString( + "playerNotFound", UserError.PLAYER_NOT_FOUND.getValue())); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java new file mode 100644 index 0000000..ab275eb --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java @@ -0,0 +1,61 @@ +package com.samleighton.sethomestwo.commands; + +import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.enums.UserError; +import com.samleighton.sethomestwo.enums.UserInfo; +import com.samleighton.sethomestwo.models.Home; +import com.samleighton.sethomestwo.utils.ChatUtils; +import com.samleighton.sethomestwo.utils.ConfigUtil; +import com.samleighton.sethomestwo.utils.ServerUtil; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class GoPlayerHome implements CommandExecutor { + + @Override + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String label, String[] args) { + if (!(commandSender instanceof Player)) { + commandSender.sendMessage(UserError.PLAYERS_ONLY.getValue()); + return true; + } + + Player admin = (Player) commandSender; + + if (!admin.hasPermission("sh2.go-player-home")) { + ChatUtils.invalidPermissions(admin); + return true; + } + + if (args.length != 2) { + ChatUtils.incorrectNumArguments(admin); + ChatUtils.sendInfo(admin, UserInfo.GO_PLAYER_HOME_USAGE.getValue()); + return true; + } + + String uuid = ServerUtil.getPlayerUUID(args[0]); + + if (uuid == null) { + ChatUtils.sendError(admin, ConfigUtil.getConfig().getString( + "playerNotFound", UserError.PLAYER_NOT_FOUND.getValue())); + return true; + } + + // The admin dao leaves canTeleport set, so a blacklisted world does not + // bar an admin from reaching the home. + Home home = new HomesDao(true).get(UUID.fromString(uuid), args[1]); + + if (home == null) { + ChatUtils.sendError(admin, ConfigUtil.getConfig().getString( + "homeNoLongerExists", UserError.HOME_NO_LONGER_EXISTS.getValue())); + return true; + } + + home.teleport(admin); + return true; + } +} diff --git a/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java new file mode 100644 index 0000000..adede6b --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java @@ -0,0 +1,72 @@ +package com.samleighton.sethomestwo.commands; + +import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.enums.UserError; +import com.samleighton.sethomestwo.enums.UserInfo; +import com.samleighton.sethomestwo.enums.UserSuccess; +import com.samleighton.sethomestwo.gui.HomeActionsGui; +import com.samleighton.sethomestwo.models.Home; +import com.samleighton.sethomestwo.utils.ChatUtils; +import com.samleighton.sethomestwo.utils.ConfigUtil; +import com.samleighton.sethomestwo.utils.ServerUtil; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class MovePlayerHome implements CommandExecutor { + + @Override + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String label, String[] args) { + if (!(commandSender instanceof Player)) { + commandSender.sendMessage(UserError.PLAYERS_ONLY.getValue()); + return true; + } + + Player admin = (Player) commandSender; + + if (!admin.hasPermission("sh2.move-player-home")) { + ChatUtils.invalidPermissions(admin); + return true; + } + + if (args.length != 2) { + ChatUtils.incorrectNumArguments(admin); + ChatUtils.sendInfo(admin, UserInfo.MOVE_PLAYER_HOME_USAGE.getValue()); + return true; + } + + String uuid = ServerUtil.getPlayerUUID(args[0]); + + if (uuid == null) { + ChatUtils.sendError(admin, ConfigUtil.getConfig().getString( + "playerNotFound", UserError.PLAYER_NOT_FOUND.getValue())); + return true; + } + + Home home = new HomesDao(true).get(UUID.fromString(uuid), args[1]); + + switch (HomeActionsGui.applyMove(admin, home)) { + case GONE: + ChatUtils.sendError(admin, ConfigUtil.getConfig().getString( + "homeNoLongerExists", UserError.HOME_NO_LONGER_EXISTS.getValue())); + break; + case BLACKLISTED: + ChatUtils.sendError(admin, ConfigUtil.getConfig().getString( + "cannotMoveToBlacklistedDimension", UserError.CANNOT_MOVE_TO_BLACKLISTED_DIMENSION.getValue())); + break; + case UPDATE_FAILED: + ChatUtils.pluginError(admin); + break; + case MOVED: + String moved = ConfigUtil.getConfig().getString("playerHomeMoved", UserSuccess.PLAYER_HOME_MOVED.getValue()); + ChatUtils.sendSuccess(admin, String.format(moved, args[0], home.getName())); + break; + } + + return true; + } +} diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserError.java b/src/main/java/com/samleighton/sethomestwo/enums/UserError.java index a696e52..35c55a3 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserError.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserError.java @@ -21,7 +21,7 @@ public enum UserError { INVALID_DIMENSION("%s is not a valid dimension. Valid dimensions are (nether, overworld, end)."), DELETE_HOME_USAGE("Usage: /delete-home [name]"), INVALID_MATERIAL("The material you entered is not valid, please try a different one."), - PLAYER_NOT_ONLINE("The player supplied is either not online or does not exist."), + PLAYER_NOT_FOUND("No player by that name is online or has any saved homes."), NO_HOMES("You have not created any homes yet. Use /create-home."), PLAYERS_ONLY("Only players may execute this command."), DIMENSION_ALREADY_BLACKLISTED("The %s dimension has already been blacklisted. You cannot add it again."), diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java b/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java index 352caab..faae905 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java @@ -8,7 +8,10 @@ public enum UserInfo { NO_MAX_HOMES("There is no max number of homes."), NO_BLACKLISTED_DIMENSIONS("No dimensions are blacklisted"), MOVED_TO_SAFE_SPOT("Your home was not safe to stand in, so you were moved to the nearest safe spot."), - MOVE_HOME_USAGE("Usage: /move-home "); + MOVE_HOME_USAGE("Usage: /move-home "), + GO_PLAYER_HOME_USAGE("Usage: /go-player-home "), + DELETE_PLAYER_HOME_USAGE("Usage: /delete-player-home "), + MOVE_PLAYER_HOME_USAGE("Usage: /move-player-home "); private final String value; diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserSuccess.java b/src/main/java/com/samleighton/sethomestwo/enums/UserSuccess.java index 796b2ca..249dd49 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserSuccess.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserSuccess.java @@ -9,7 +9,9 @@ public enum UserSuccess { TELEPORTED("Teleported to %s"), DIMENSION_ADDED_TO_BLACKLIST("%s has been added to the blacklist"), DIMENSION_REMOVED_FROM_BLACKLIST("%s has been removed from the blacklist"), - MAX_HOMES_UPDATED_SUCCESSFULLY("Max homes updated successfully."); + MAX_HOMES_UPDATED_SUCCESSFULLY("Max homes updated successfully."), + PLAYER_HOME_DELETED("%s's home '%s' has been deleted."), + PLAYER_HOME_MOVED("%s's home '%s' has been moved to your location."); private final String value; diff --git a/src/main/java/com/samleighton/sethomestwo/tabcompleters/PlayerHomesTabCompleter.java b/src/main/java/com/samleighton/sethomestwo/tabcompleters/PlayerHomesTabCompleter.java new file mode 100644 index 0000000..19b9dcc --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/tabcompleters/PlayerHomesTabCompleter.java @@ -0,0 +1,48 @@ +package com.samleighton.sethomestwo.tabcompleters; + +import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.models.Home; +import com.samleighton.sethomestwo.utils.ServerUtil; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.util.StringUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class PlayerHomesTabCompleter implements TabCompleter { + + @Nullable + @Override + public List onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] args) { + List completions = new ArrayList<>(); + + // Only online players are offered, though the commands themselves also + // accept an offline player who has saved homes. + if (args.length == 1) { + List names = new ArrayList<>(); + Bukkit.getOnlinePlayers().forEach(player -> names.add(player.getName())); + StringUtil.copyPartialMatches(args[0], names, completions); + return completions; + } + + if (args.length == 2) { + String uuid = ServerUtil.getPlayerUUID(args[0]); + if (uuid == null) return completions; + + List homeNames = new ArrayList<>(); + for (Home home : new HomesDao(true).getAll(UUID.fromString(uuid))) { + homeNames.add(home.getName()); + } + + StringUtil.copyPartialMatches(args[1], homeNames, completions); + } + + return completions; + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 2a1375d..f85ad3e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -55,6 +55,21 @@ commands: permission: sh2.move-home aliases: - uhome + go-player-home: + description: Teleport to another player's home. + permission: sh2.go-player-home + aliases: + - home-of + delete-player-home: + description: Delete another player's home. + permission: sh2.delete-player-home + aliases: + - delhome-of + move-player-home: + description: Move another player's home to your location. + permission: sh2.move-player-home + aliases: + - uhome-of permissions: sh2.create-home: description: Allow player to create homes. @@ -101,6 +116,15 @@ permissions: sh2.update-notify: description: Be told on join when a newer SetHomesTwo release is available. default: op + sh2.go-player-home: + description: Teleport to another player's home. + default: op + sh2.delete-player-home: + description: Delete another player's home. + default: op + sh2.move-player-home: + description: Move another player's home. + default: op sh2.player: description: Everything an ordinary player needs. default: true @@ -125,3 +149,6 @@ permissions: sh2.set-max-homes: true sh2.import-homes: true sh2.update-notify: true + sh2.go-player-home: true + sh2.delete-player-home: true + sh2.move-player-home: true diff --git a/src/test/java/com/samleighton/sethomestwo/commands/GetPlayerHomesTest.java b/src/test/java/com/samleighton/sethomestwo/commands/GetPlayerHomesTest.java index 76e0558..efde2d8 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/GetPlayerHomesTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/GetPlayerHomesTest.java @@ -31,7 +31,7 @@ void anOfflineOrUnknownPlayerIsReported() { server.execute("get-player-homes", admin, "nobody").assertSucceeded(); - assertTrue(admin.nextMessage().contains("not online")); + assertTrue(admin.nextMessage().contains("No player by that name")); } @Test diff --git a/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java b/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java new file mode 100644 index 0000000..ac8858a --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java @@ -0,0 +1,172 @@ +package com.samleighton.sethomestwo.commands; + +import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.support.HomeFixtures; +import com.samleighton.sethomestwo.support.ServerTestBase; +import com.samleighton.sethomestwo.tabcompleters.PlayerHomesTabCompleter; +import org.bukkit.Location; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PlayerHomeAdminCommandsTest extends ServerTestBase { + + @Test + void deletingAnOfflinePlayersHomeWorks() { + PlayerMock target = addPlayer("Steve"); + HomeFixtures.persist(target, "base"); + UUID targetId = target.getUniqueId(); + target.disconnect(); + + PlayerMock admin = addPlayer("Admin"); + admin.addAttachment(plugin, "sh2.delete-player-home", true); + + server.execute("delete-player-home", admin, "Steve", "base").assertSucceeded(); + + assertTrue(new HomesDao(true).getAll(targetId).isEmpty()); + } + + @Test + void movingAnOfflinePlayersHomeUsesTheAdminLocation() { + PlayerMock target = addPlayer("Steve"); + HomeFixtures.persist(target, "base"); + UUID targetId = target.getUniqueId(); + target.disconnect(); + + PlayerMock admin = addPlayer("Admin"); + admin.addAttachment(plugin, "sh2.move-player-home", true); + admin.teleport(new Location(overworld, 250, 70, 250)); + + server.execute("move-player-home", admin, "Steve", "base").assertSucceeded(); + + assertEquals(250.0, new HomesDao(true).getAll(targetId).get(0).getX()); + } + + @Test + void teleportingToAnOfflinePlayersHomeIsAccepted() { + PlayerMock target = addPlayer("Steve"); + HomeFixtures.persist(HomeFixtures.home(target, "base", new Location(overworld, 30, 70, 30))); + target.disconnect(); + + PlayerMock admin = addPlayer("Admin"); + admin.addAttachment(plugin, "sh2.go-player-home", true); + admin.addAttachment(plugin, "sh2.teleport", true); + admin.teleport(new Location(overworld, 0, 64, 0)); + plugin.getConfig().set("delay", 0); + // TeleportSafetyUtil.prefetchChunks reaches an unimplemented MockBukkit call. + plugin.getConfig().set("teleportSafety", false); + + server.execute("go-player-home", admin, "Steve", "base").assertSucceeded(); + server.getScheduler().performTicks(100L); + + assertEquals(30.0, admin.getLocation().getX()); + } + + @Test + void anUnknownPlayerIsReported() { + PlayerMock admin = addPlayer("Admin"); + admin.addAttachment(plugin, "sh2.delete-player-home", true); + + server.execute("delete-player-home", admin, "Nobody", "base").assertSucceeded(); + + assertTrue(admin.nextMessage().contains("No player by that name")); + } + + @Test + void eachCommandIsRefusedWithoutItsNodeAtTheCommandGate() { + PlayerMock target = addPlayer("Steve"); + HomeFixtures.persist(target, "base"); + + PlayerMock admin = addPlayer("Admin"); + admin.addAttachment(plugin, "sh2.delete-player-home", false); + + server.execute("delete-player-home", admin, "Steve", "base").assertSucceeded(); + + assertTrue(admin.nextMessage().contains("permission")); + assertEquals(1, new HomesDao(true).getAll(target.getUniqueId()).size()); + } + + @Test + void theInCodeGuardAlsoRefuses() { + PlayerMock target = addPlayer("Steve"); + HomeFixtures.persist(target, "base"); + + PlayerMock admin = addPlayer("Admin"); + admin.addAttachment(plugin, "sh2.delete-player-home", false); + + // Call the executor directly. Going through server.execute would be + // refused by plugin.yml's permission before onCommand is reached. + new DeletePlayerHome().onCommand( + admin, + Objects.requireNonNull(plugin.getCommand("delete-player-home")), + "delete-player-home", + new String[]{"Steve", "base"}); + + assertTrue(admin.nextMessage().contains("permission")); + assertEquals(1, new HomesDao(true).getAll(target.getUniqueId()).size()); + } + + @Test + void theV1AliasesWork() { + PlayerMock target = addPlayer("Steve"); + HomeFixtures.persist(target, "base"); + + PlayerMock admin = addPlayer("Admin"); + admin.addAttachment(plugin, "sh2.delete-player-home", true); + + server.execute("delhome-of", admin, "Steve", "base").assertSucceeded(); + + assertTrue(new HomesDao(true).getAll(target.getUniqueId()).isEmpty()); + } + + @Test + void theWrongNumberOfArgumentsShowsTheUsage() { + PlayerMock admin = addPlayer("Admin"); + admin.addAttachment(plugin, "sh2.move-player-home", true); + + server.execute("move-player-home", admin, "Steve").assertSucceeded(); + + assertTrue(admin.nextMessage().contains("Incorrect number of arguments")); + assertTrue(admin.nextMessage().contains("Usage: /move-player-home ")); + } + + @Test + void theTabCompleterOffersOnlinePlayerNamesFirst() { + addPlayer("Steve"); + PlayerMock admin = addPlayer("Admin"); + + List completions = new PlayerHomesTabCompleter().onTabComplete( + admin, + Objects.requireNonNull(plugin.getCommand("delete-player-home")), + "delete-player-home", + new String[]{"St"}); + + assertTrue(completions.contains("Steve")); + assertFalse(completions.contains("Admin")); + } + + @Test + void theTabCompleterOffersTheTargetsHomeNamesSecond() { + PlayerMock target = addPlayer("Steve"); + HomeFixtures.persist(target, "base"); + HomeFixtures.persist(target, "mine"); + target.disconnect(); + + PlayerMock admin = addPlayer("Admin"); + + List completions = new PlayerHomesTabCompleter().onTabComplete( + admin, + Objects.requireNonNull(plugin.getCommand("delete-player-home")), + "delete-player-home", + new String[]{"Steve", "b"}); + + assertEquals(List.of("base"), completions); + } +} From 1c1deebd47c8355eef4abadcac8127fbf5f54177 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 10:33:06 -0400 Subject: [PATCH 18/75] fix: match player names without regard to case in admin commands /delete-player-home steve base failed where Steve worked, which sat badly next to home names now being matched without regard to case. Minecraft names are unique ignoring case, so at most one player can match. --- .../samleighton/sethomestwo/dao/HomesDao.java | 4 ++-- .../sethomestwo/utils/ServerUtil.java | 5 +++-- .../utils/ServerUtilOfflineLookupTest.java | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java index 9d8a5fe..9ded32f 100644 --- a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java +++ b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java @@ -309,7 +309,7 @@ public boolean nameExists(UUID playerUUID, String name, Integer excludeId) { * guessing when more than one distinct UUID claims the name. */ public String uuidForName(String playerName) { - String sql = "select distinct player_uuid from players_homes where player_name = ?;"; + String sql = "select distinct player_uuid from players_homes where lower(player_name) = lower(?);"; try (PreparedStatement statement = this.conn.prepareStatement(sql)) { statement.setString(1, playerName); @@ -332,7 +332,7 @@ public String uuidForName(String playerName) { * make the name resolve ambiguously. The joining player takes precedence. */ public boolean refreshPlayerName(UUID playerUUID, String playerName) { - String clearSql = "update players_homes set player_name = null where player_name = ? and player_uuid <> ?;"; + String clearSql = "update players_homes set player_name = null where lower(player_name) = lower(?) and player_uuid <> ?;"; String claimSql = "update players_homes set player_name = ? where player_uuid = ?;"; try (PreparedStatement clear = this.conn.prepareStatement(clearSql); diff --git a/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java b/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java index 94b01c4..352bb65 100644 --- a/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java +++ b/src/main/java/com/samleighton/sethomestwo/utils/ServerUtil.java @@ -80,11 +80,12 @@ public static boolean isWorldBlacklisted(World world, List blacklistedWo /** * Resolve a player name to a UUID, falling back to the names stored against - * saved homes so offline players can be addressed. + * saved homes so offline players can be addressed. The match ignores case, + * which is safe because Minecraft names are unique ignoring case. */ public static String getPlayerUUID(String playerName) { for (Player player : Bukkit.getOnlinePlayers()) { - if (player.getName().equals(playerName)) { + if (player.getName().equalsIgnoreCase(playerName)) { return player.getUniqueId().toString(); } } diff --git a/src/test/java/com/samleighton/sethomestwo/utils/ServerUtilOfflineLookupTest.java b/src/test/java/com/samleighton/sethomestwo/utils/ServerUtilOfflineLookupTest.java index e27b6ee..02dc394 100644 --- a/src/test/java/com/samleighton/sethomestwo/utils/ServerUtilOfflineLookupTest.java +++ b/src/test/java/com/samleighton/sethomestwo/utils/ServerUtilOfflineLookupTest.java @@ -28,6 +28,24 @@ void anOfflinePlayerWithStoredHomesResolves() { assertEquals(expected, ServerUtil.getPlayerUUID("Steve")); } + @Test + void anOnlinePlayerResolvesRegardlessOfCase() { + PlayerMock player = addPlayer("Steve"); + + assertEquals(player.getUniqueId().toString(), ServerUtil.getPlayerUUID("steve")); + } + + @Test + void anOfflinePlayerResolvesRegardlessOfCase() { + PlayerMock player = addPlayer("Steve"); + HomeFixtures.persist(player, "base"); + String expected = player.getUniqueId().toString(); + + player.disconnect(); + + assertEquals(expected, ServerUtil.getPlayerUUID("sTeVe")); + } + @Test void aPlayerWithNoHomesAndNoSessionDoesNotResolve() { assertNull(ServerUtil.getPlayerUUID("Nobody")); From 3e30d1b63b41ed893272e98f160ff035de1d9ec2 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 10:42:41 -0400 Subject: [PATCH 19/75] fix: correct a false comment and echo stored names in admin replies The comment on GoPlayerHome claimed the admin dao keeps canTeleport set. It does not: only getAll applies the blacklist rule, so the flag stays at its default whichever dao is used, and the admin reached the home by accident rather than by design. Also cover the two commands that had no permission test, the not-found path on all three, and echo the stored owner name rather than what was typed, so a reply cannot say sTeVe's home 'base'. --- .../commands/DeletePlayerHome.java | 11 +++- .../sethomestwo/commands/GoPlayerHome.java | 4 +- .../sethomestwo/commands/MovePlayerHome.java | 11 +++- .../commands/PlayerHomeAdminCommandsTest.java | 66 +++++++++++++++++++ 4 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java index bc37fc6..4180bca 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java @@ -61,7 +61,16 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command } String deleted = ConfigUtil.getConfig().getString("playerHomeDeleted", UserSuccess.PLAYER_HOME_DELETED.getValue()); - ChatUtils.sendSuccess(admin, String.format(deleted, args[0], home.getName())); + ChatUtils.sendSuccess(admin, String.format(deleted, ownerName(home, args[0]), home.getName())); return true; } + + /** + * The owner's name as stored against the home, which is canonically cased. + * Falls back to what the admin typed for a home saved before names were + * recorded. + */ + private static String ownerName(Home home, String typed) { + return home.getPlayerName() == null ? typed : home.getPlayerName(); + } } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java index ab275eb..ec43c05 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java @@ -45,8 +45,8 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command return true; } - // The admin dao leaves canTeleport set, so a blacklisted world does not - // bar an admin from reaching the home. + // Unlike getAll, get applies no blacklist rule, so canTeleport stays at + // its default and an admin reaches the home whatever world it is in. Home home = new HomesDao(true).get(UUID.fromString(uuid), args[1]); if (home == null) { diff --git a/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java index adede6b..c72ad55 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java @@ -63,10 +63,19 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command break; case MOVED: String moved = ConfigUtil.getConfig().getString("playerHomeMoved", UserSuccess.PLAYER_HOME_MOVED.getValue()); - ChatUtils.sendSuccess(admin, String.format(moved, args[0], home.getName())); + ChatUtils.sendSuccess(admin, String.format(moved, ownerName(home, args[0]), home.getName())); break; } return true; } + + /** + * The owner's name as stored against the home, which is canonically cased. + * Falls back to what the admin typed for a home saved before names were + * recorded. + */ + private static String ownerName(Home home, String typed) { + return home.getPlayerName() == null ? typed : home.getPlayerName(); + } } diff --git a/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java b/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java index ac8858a..7154720 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java @@ -169,4 +169,70 @@ void theTabCompleterOffersTheTargetsHomeNamesSecond() { assertEquals(List.of("base"), completions); } + + @Test + void theInCodeGuardAlsoRefusesTheOtherTwoCommands() { + PlayerMock target = addPlayer("Steve"); + HomeFixtures.persist(target, "base"); + double beforeX = new HomesDao(true).getAll(target.getUniqueId()).get(0).getX(); + + PlayerMock admin = addPlayer("Admin"); + admin.addAttachment(plugin, "sh2.go-player-home", false); + admin.addAttachment(plugin, "sh2.move-player-home", false); + admin.teleport(new Location(overworld, 900, 70, 900)); + // So a regression that lets the teleport through fails on the assertion + // below rather than aborting on an unimplemented mock call. + plugin.getConfig().set("teleportSafety", false); + + new GoPlayerHome().onCommand(admin, + Objects.requireNonNull(plugin.getCommand("go-player-home")), + "go-player-home", new String[]{"Steve", "base"}); + assertTrue(admin.nextMessage().contains("permission")); + assertEquals(900.0, admin.getLocation().getX()); + + new MovePlayerHome().onCommand(admin, + Objects.requireNonNull(plugin.getCommand("move-player-home")), + "move-player-home", new String[]{"Steve", "base"}); + assertTrue(admin.nextMessage().contains("permission")); + assertEquals(beforeX, new HomesDao(true).getAll(target.getUniqueId()).get(0).getX()); + } + + @Test + void anUnknownHomeIsReportedByEachCommand() { + PlayerMock target = addPlayer("Steve"); + HomeFixtures.persist(target, "base"); + + PlayerMock admin = addPlayer("Admin"); + admin.addAttachment(plugin, "sh2.delete-player-home", true); + admin.addAttachment(plugin, "sh2.move-player-home", true); + admin.addAttachment(plugin, "sh2.go-player-home", true); + + server.execute("delete-player-home", admin, "Steve", "nope").assertSucceeded(); + assertTrue(admin.nextMessage().contains("no longer exists")); + + server.execute("move-player-home", admin, "Steve", "nope").assertSucceeded(); + assertTrue(admin.nextMessage().contains("no longer exists")); + + server.execute("go-player-home", admin, "Steve", "nope").assertSucceeded(); + assertTrue(admin.nextMessage().contains("no longer exists")); + + assertEquals(1, new HomesDao(true).getAll(target.getUniqueId()).size()); + } + + @Test + void theSuccessMessageNamesTheOwnerAndHomeCanonically() { + PlayerMock target = addPlayer("Steve"); + HomeFixtures.persist(target, "base"); + + PlayerMock admin = addPlayer("Admin"); + admin.addAttachment(plugin, "sh2.delete-player-home", true); + + // Both names typed in the wrong case. The reply must echo the stored + // spelling, not what was typed. + server.execute("delete-player-home", admin, "sTeVe", "BaSe").assertSucceeded(); + + String reply = admin.nextMessage(); + assertTrue(reply.contains("Steve"), reply); + assertTrue(reply.contains("base"), reply); + } } From 298ff6e41cd0dfd7fa8b0c090faa2f32eda1adf7 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 11:03:42 -0400 Subject: [PATCH 20/75] feat: treat a non-material second argument as description text --- .../sethomestwo/commands/CreateHome.java | 58 ++++++++----------- .../sethomestwo/enums/UserSuccess.java | 2 +- src/main/resources/default-config.yml | 6 +- .../sethomestwo/commands/CreateHomeTest.java | 42 ++++++++++++-- 4 files changed, 69 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java b/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java index 6f1aad3..9837a22 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java @@ -71,41 +71,33 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command // Extract parameters from command arguments String homeName = args[0]; - String material = ""; - if (args.length > 1) - material = args[1]; - - // Guard to ensure material entered is a valid material - boolean isMaterialBlankOrDefault = material.equalsIgnoreCase("d") || material.equalsIgnoreCase("default") || material.equalsIgnoreCase(""); - Material mat = isMaterialBlankOrDefault ? Material.WHITE_WOOL : Material.matchMaterial(material); - if (mat == null) { - String errorMessage = ConfigUtil.getConfig().getString("invalidHomeItem", UserError.INVALID_MATERIAL.getValue()); - ChatUtils.sendError(player, errorMessage); - return true; - } - if (!mat.isItem()) { - String errorMessage = ConfigUtil.getConfig().getString("invalidHomeItem", UserError.INVALID_MATERIAL.getValue()); - ChatUtils.sendError(player, errorMessage); - return true; - } - - material = mat.name(); - String description = null; - StringBuilder stringBuilder = new StringBuilder(); - - // Build description from leftover arguments - if (args.length > 2) { - String[] remainingArgs = Arrays.copyOfRange(args, 2, args.length); - for (int i = 0; i < remainingArgs.length; i++) { - String arg = remainingArgs[i]; - if (i == remainingArgs.length - 1) { - stringBuilder.append(arg); - } else { - stringBuilder.append(arg).append(" "); + Material mat = null; + int descriptionStart = 1; + + if (args.length > 1) { + String candidate = args[1]; + + if (candidate.isEmpty() || candidate.equalsIgnoreCase("d") || candidate.equalsIgnoreCase("default")) { + mat = Material.WHITE_WOOL; + descriptionStart = 2; + } else { + Material matched = Material.matchMaterial(candidate); + if (matched != null && matched.isItem()) { + mat = matched; + descriptionStart = 2; } } + } - description = stringBuilder.toString(); + // An argument 2 that names no item is description text, not an error, so + // that the v1 form "/sethome base my main base" still works. + if (mat == null) mat = Material.WHITE_WOOL; + + String material = mat.name(); + + String description = null; + if (args.length > descriptionStart) { + description = String.join(" ", Arrays.copyOfRange(args, descriptionStart, args.length)); } // Duplicate name guard @@ -133,7 +125,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command } String message = ConfigUtil.getConfig().getString("homeCreated", UserSuccess.HOME_CREATED.getValue()); - ChatUtils.sendSuccess(player, String.format(message, homeName)); + ChatUtils.sendSuccess(player, String.format(message, homeName, material)); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserSuccess.java b/src/main/java/com/samleighton/sethomestwo/enums/UserSuccess.java index 249dd49..50b5f16 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserSuccess.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserSuccess.java @@ -1,7 +1,7 @@ package com.samleighton.sethomestwo.enums; public enum UserSuccess { - HOME_CREATED("%s has been created successfully."), + HOME_CREATED("%s has been created successfully. Icon: %s"), HOME_DELETED("%s has been deleted successfully."), HOME_MOVED("%s has been moved to your current location."), HOME_ICON_CHANGED("The icon for %s is now %s."), diff --git a/src/main/resources/default-config.yml b/src/main/resources/default-config.yml index 03ebb3e..86086fc 100644 --- a/src/main/resources/default-config.yml +++ b/src/main/resources/default-config.yml @@ -42,7 +42,11 @@ delay: 3 # (seconds) 0 is no delay. teleportSafety: true # Relocate to the nearest safe spot (or cancel) when a home would teleport you into blocks, lava, or a fall. # -- MESSAGES -- -homeCreated: "%s has been created successfully." # You can use %s here as a placeholder for the players home name. +# The first %s is the home name, the second is the icon material. The icon is +# named so that a description starting with a material word, for example +# "/create-home base stone house", makes clear which word was taken as the icon. +# Dropping the second %s is allowed and simply omits the icon. +homeCreated: "%s has been created successfully. Icon: %s" homeDeleted: "%s has been deleted successfully." # You can use %s here as a placeholder for the players home name. dimensionAddedToBlacklist: "%s has been added to the blacklist." # You can use %s here as a placeholder for the dimension names. diff --git a/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java index f43a3c2..4d99abf 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java @@ -1,6 +1,7 @@ package com.samleighton.sethomestwo.commands; import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; import org.bukkit.Bukkit; @@ -56,13 +57,46 @@ void aDuplicateNameIsRejected() { } @Test - void anInvalidMaterialIsRejected() { + void aNonMaterialSecondArgumentBecomesTheDescription() { PlayerMock player = addPlayer(); - server.execute("create-home", player, "base", "not_a_material").assertSucceeded(); + server.execute("create-home", player, "base", "my", "main", "base").assertSucceeded(); - assertTrue(player.nextMessage().contains("not valid")); - assertTrue(new HomesDao().getAll(player.getUniqueId()).isEmpty()); + var homes = new HomesDao().getAll(player.getUniqueId()); + assertEquals(1, homes.size()); + assertEquals("my main base", homes.get(0).getDescription()); + assertEquals(Material.WHITE_WOOL.name(), homes.get(0).getMaterial()); + } + + @Test + void aMaterialSecondArgumentStillSetsTheIcon() { + PlayerMock player = addPlayer(); + + server.execute("create-home", player, "base", "diamond_block", "my", "base").assertSucceeded(); + + var homes = new HomesDao().getAll(player.getUniqueId()); + assertEquals(Material.DIAMOND_BLOCK.name(), homes.get(0).getMaterial()); + assertEquals("my base", homes.get(0).getDescription()); + } + + @Test + void theChosenIconIsNamedInTheSuccessMessage() { + PlayerMock player = addPlayer(); + + server.execute("create-home", player, "base", "diamond_block").assertSucceeded(); + + assertTrue(player.nextMessage().contains("DIAMOND_BLOCK")); + } + + @Test + void aDescriptionBeginningWithAMaterialWordLosesThatWordToTheIcon() { + PlayerMock player = addPlayer(); + + server.execute("create-home", player, "base", "stone", "house").assertSucceeded(); + + Home home = new HomesDao().getAll(player.getUniqueId()).get(0); + assertEquals(Material.STONE.name(), home.getMaterial()); + assertEquals("house", home.getDescription()); } @Test From 84473a34f06685231a4591c18b844397a4376adf Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 11:20:08 -0400 Subject: [PATCH 21/75] fix: honour defaultHomeItem and cover the icon guards The isItem check was untested, and losing it stored a non-item material such as WATER as a home icon, which made HomesGui throw when it built the ItemStack and stopped the homes menu opening at all. defaultHomeItem was documented as the icon used when a home is created without one but only the importer read it, so a server that set it saw it apply to imported homes and not to new ones. --- .../sethomestwo/commands/CreateHome.java | 16 +++- src/main/resources/default-config.yml | 3 +- .../sethomestwo/commands/CreateHomeTest.java | 80 +++++++++++++++++++ 3 files changed, 96 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java b/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java index 9837a22..1138e76 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java @@ -78,7 +78,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command String candidate = args[1]; if (candidate.isEmpty() || candidate.equalsIgnoreCase("d") || candidate.equalsIgnoreCase("default")) { - mat = Material.WHITE_WOOL; + mat = defaultHomeItem(); descriptionStart = 2; } else { Material matched = Material.matchMaterial(candidate); @@ -91,7 +91,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command // An argument 2 that names no item is description text, not an error, so // that the v1 form "/sethome base my main base" still works. - if (mat == null) mat = Material.WHITE_WOOL; + if (mat == null) mat = defaultHomeItem(); String material = mat.name(); @@ -168,4 +168,16 @@ private boolean maxHomesReached(Player player, Dao homesDao){ int playersHomeCount = HomesUtil.getPlayerHomesCount(homesDao, player.getUniqueId()); return playersHomeCount >= maxHomesAllowed; } + + /** + * The icon a home takes when none is given. A configured value that names no + * item falls back to white wool, because a non-item material stored as an + * icon makes HomesGui throw when it builds the ItemStack. + */ + private static Material defaultHomeItem() { + Material configured = Material.matchMaterial( + ConfigUtil.getConfig().getString("defaultHomeItem", "white_wool")); + + return configured != null && configured.isItem() ? configured : Material.WHITE_WOOL; + } } diff --git a/src/main/resources/default-config.yml b/src/main/resources/default-config.yml index 86086fc..c71e029 100644 --- a/src/main/resources/default-config.yml +++ b/src/main/resources/default-config.yml @@ -45,7 +45,8 @@ teleportSafety: true # Relocate to the nearest safe spot (or cancel) when a home # The first %s is the home name, the second is the icon material. The icon is # named so that a description starting with a material word, for example # "/create-home base stone house", makes clear which word was taken as the icon. -# Dropping the second %s is allowed and simply omits the icon. +# Dropping the second %s is allowed and simply omits the icon. Adding a third +# is not: the command will fail with a formatting error every time. homeCreated: "%s has been created successfully. Icon: %s" homeDeleted: "%s has been deleted successfully." # You can use %s here as a placeholder for the players home name. dimensionAddedToBlacklist: "%s has been added to the blacklist." # You can use %s here as a placeholder for the dimension names. diff --git a/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java index 4d99abf..924a43e 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java @@ -17,7 +17,9 @@ import java.util.logging.LogRecord; import java.util.logging.Logger; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; class CreateHomeTest extends ServerTestBase { @@ -88,6 +90,11 @@ void theChosenIconIsNamedInTheSuccessMessage() { assertTrue(player.nextMessage().contains("DIAMOND_BLOCK")); } + /** + * Pins the trade-off the forgiving parsing introduces, so it stays a + * documented behaviour rather than a surprise. Looks like a duplicate of + * aMaterialSecondArgumentStillSetsTheIcon; it is not, keep both. + */ @Test void aDescriptionBeginningWithAMaterialWordLosesThatWordToTheIcon() { PlayerMock player = addPlayer(); @@ -189,4 +196,77 @@ private boolean loggedWarning(List records, String message) { return records.stream().anyMatch( record -> record.getLevel() == Level.WARNING && message.equals(record.getMessage())); } + + @Test + void aValidButNonItemMaterialWordIsDescriptionText() { + PlayerMock player = addPlayer(); + + // water names a real Material but not an item. Storing it as the icon + // would make HomesGui throw on new ItemStack and the menu stop opening. + server.execute("create-home", player, "base", "water", "front").assertSucceeded(); + + Home home = new HomesDao().getAll(player.getUniqueId()).get(0); + assertEquals(Material.WHITE_WOOL.name(), home.getMaterial()); + assertEquals("water front", home.getDescription()); + } + + @Test + void theHomesMenuStillOpensAfterAHomeNamedAfterANonItem() { + PlayerMock player = addPlayer(); + server.execute("create-home", player, "base", "water", "front").assertSucceeded(); + + assertDoesNotThrow(() -> server.execute("homes", player).assertSucceeded()); + } + + /** + * Reachable by typing a double space, which Bukkit turns into an empty + * argument rather than dropping it. + */ + @Test + void anEmptySecondArgumentDoesNotLeakIntoTheDescription() { + PlayerMock player = addPlayer(); + + server.dispatchCommand(player, "create-home base hi"); + + Home home = new HomesDao().getAll(player.getUniqueId()).get(0); + assertEquals(Material.WHITE_WOOL.name(), home.getMaterial()); + assertEquals("hi", home.getDescription()); + } + + @Test + void theDefaultIconSentinelIsNotDescriptionText() { + PlayerMock player = addPlayer(); + + server.execute("create-home", player, "base", "d").assertSucceeded(); + server.execute("create-home", player, "camp", "default", "my", "spot").assertSucceeded(); + + List homes = new HomesDao().getAll(player.getUniqueId()); + Home base = homes.stream().filter(h -> h.getName().equals("base")).findFirst().orElseThrow(); + Home camp = homes.stream().filter(h -> h.getName().equals("camp")).findFirst().orElseThrow(); + + assertEquals(Material.WHITE_WOOL.name(), base.getMaterial()); + assertNull(base.getDescription()); + assertEquals(Material.WHITE_WOOL.name(), camp.getMaterial()); + assertEquals("my spot", camp.getDescription()); + } + + @Test + void aDefaultIconThatNamesNoItemFallsBackToWhiteWool() { + PlayerMock player = addPlayer(); + plugin.getConfig().set("defaultHomeItem", "water"); + + server.execute("create-home", player, "base").assertSucceeded(); + + assertEquals(Material.WHITE_WOOL.name(), new HomesDao().getAll(player.getUniqueId()).get(0).getMaterial()); + } + + @Test + void theConfiguredDefaultIconIsUsedWhenNoneIsGiven() { + PlayerMock player = addPlayer(); + plugin.getConfig().set("defaultHomeItem", "chest"); + + server.execute("create-home", player, "base").assertSucceeded(); + + assertEquals(Material.CHEST.name(), new HomesDao().getAll(player.getUniqueId()).get(0).getMaterial()); + } } From 5db7ddbac3cb427cbb6e79138a7169a76b4c853d Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 11:29:19 -0400 Subject: [PATCH 22/75] feat: bare create-home and go-home use a home named default A bare /create-home creates a home called default and a bare /go-home teleports to it, restoring the v1 unnamed home. The name matches what SetHomesV1Importer gives an imported v1 unnamed home. Also fixes the unknown-home error, which was sent unformatted and so showed players the literal %s placeholder. It now names the home that was looked for and takes a homeDoesNotExist config override. --- .../sethomestwo/commands/CreateHome.java | 13 ++--- .../sethomestwo/commands/GoHome.java | 12 +++-- .../sethomestwo/utils/HomesUtil.java | 7 +++ src/main/resources/default-config.yml | 1 + .../sethomestwo/commands/CreateHomeTest.java | 42 +++++++++++++-- .../sethomestwo/commands/GoHomeTest.java | 53 ++++++++++++++++++- 6 files changed, 109 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java b/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java index 1138e76..299ee96 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java @@ -3,7 +3,6 @@ import com.samleighton.sethomestwo.dao.Dao; import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.enums.UserError; -import com.samleighton.sethomestwo.enums.UserInfo; import com.samleighton.sethomestwo.enums.UserSuccess; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.utils.ChatUtils; @@ -45,13 +44,6 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command return true; } - // Guard to ensure we have minimum number of args - if (args.length < 1) { - ChatUtils.incorrectNumArguments(player); - ChatUtils.sendInfo(player, UserInfo.CREATE_HOME_USAGE.getValue()); - return true; - } - // Guard to check if player has exceeded the max number of homes if (this.maxHomesReached(player, homesDao)){ String errorMessage = ConfigUtil.getConfig().getString("maxHomesReached", UserError.MAX_HOMES.getValue()); @@ -68,8 +60,9 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command return true; } - // Extract parameters from command arguments - String homeName = args[0]; + // Extract parameters from command arguments. A bare command is the v1 + // form, naming the home rather than erroring. + String homeName = args.length < 1 ? HomesUtil.DEFAULT_HOME_NAME : args[0]; Material mat = null; int descriptionStart = 1; diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java b/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java index c038c9d..94fb557 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java @@ -5,6 +5,8 @@ import com.samleighton.sethomestwo.enums.UserError; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.utils.ChatUtils; +import com.samleighton.sethomestwo.utils.ConfigUtil; +import com.samleighton.sethomestwo.utils.HomesUtil; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -24,8 +26,9 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command Player player = (Player) commandSender; - // Args length guard - if(args.length != 1){ + // Args length guard. A bare command is the v1 form, so only too many + // arguments is an error. + if(args.length > 1){ ChatUtils.incorrectNumArguments(player); return true; } @@ -37,7 +40,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command } // Get players home dao instance - String desiredHomeName = args[0]; + String desiredHomeName = args.length < 1 ? HomesUtil.DEFAULT_HOME_NAME : args[0]; Dao homesDao = new HomesDao(); ArrayList playerHomes = (ArrayList) homesDao.getAll(player.getUniqueId()); Home homeToTeleportTo = null; @@ -51,7 +54,8 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command // Home does not exist guard if(homeToTeleportTo == null){ - ChatUtils.sendError(player, UserError.HOME_DOES_NOT_EXIST.getValue()); + String message = ConfigUtil.getConfig().getString("homeDoesNotExist", UserError.HOME_DOES_NOT_EXIST.getValue()); + ChatUtils.sendError(player, String.format(message, desiredHomeName)); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/utils/HomesUtil.java b/src/main/java/com/samleighton/sethomestwo/utils/HomesUtil.java index 4d7adcc..1d1eca1 100644 --- a/src/main/java/com/samleighton/sethomestwo/utils/HomesUtil.java +++ b/src/main/java/com/samleighton/sethomestwo/utils/HomesUtil.java @@ -9,6 +9,13 @@ public class HomesUtil { + /** + * The name a home takes when created or requested without one. Matches the + * name SetHomesV1Importer gives an imported v1 unnamed home, so an importing + * server's homes line up with what a bare command reaches for. + */ + public static final String DEFAULT_HOME_NAME = "default"; + public static List getPlayerHomesNameOnly(Dao homesDao, UUID playerUUID){ List playerHomes = homesDao.getAll(playerUUID); return Lists.transform(playerHomes, Home::getName); diff --git a/src/main/resources/default-config.yml b/src/main/resources/default-config.yml index c71e029..b3ff08e 100644 --- a/src/main/resources/default-config.yml +++ b/src/main/resources/default-config.yml @@ -60,6 +60,7 @@ noHomes: "You have not created any homes yet. Use /create-home to make your firs teleportToBlacklistedDimension: "You cannot teleport to this home because the dimension it is in has been blacklisted." maxHomesReached: "You have reached the maximum number of homes allowed." dimensionBlacklisted: "You cannot set a home in this dimension because it has been blacklisted." +homeDoesNotExist: "The home '%s' does not exist." # %s is the home name that was looked for. A bare /go-home looks for 'default'. unsafeHome: "Teleport cancelled: this home is not safe to stand in and no safe spot was found nearby." movedToSafeSpot: "Your home was not safe to stand in, so you were moved to the nearest safe spot." diff --git a/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java index 924a43e..3193e6b 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java @@ -38,13 +38,49 @@ void aHomeIsCreatedAtThePlayersLocation() { } @Test - void missingNameIsRejected() { + void aBareCommandCreatesTheDefaultHome() { PlayerMock player = addPlayer(); + player.teleport(new Location(overworld, 7, 65, 7)); server.execute("create-home", player).assertSucceeded(); - assertTrue(player.nextMessage().contains("Incorrect number of arguments")); - assertTrue(new HomesDao().getAll(player.getUniqueId()).isEmpty()); + List homes = new HomesDao().getAll(player.getUniqueId()); + assertEquals(1, homes.size()); + assertEquals("default", homes.get(0).getName()); + assertEquals(7.0, homes.get(0).getX()); + assertEquals(Material.WHITE_WOOL.name(), homes.get(0).getMaterial()); + assertNull(homes.get(0).getDescription()); + } + + @Test + void theNameDefaultBehavesLikeTheBareCommand() { + PlayerMock player = addPlayer(); + + server.execute("create-home", player, "default").assertSucceeded(); + player.nextMessage(); + server.execute("create-home", player).assertSucceeded(); + + assertTrue(player.nextMessage().contains("You already have a home called")); + List homes = new HomesDao().getAll(player.getUniqueId()); + assertEquals(1, homes.size()); + assertEquals("default", homes.get(0).getName()); + } + + /** + * Argument 2 'default' is the icon sentinel, not the default home name. The + * two meanings live in different argument positions and must not be merged. + */ + @Test + void theIconSentinelNeverBecomesTheHomeName() { + PlayerMock player = addPlayer(); + + server.execute("create-home", player, "base", "default").assertSucceeded(); + + List homes = new HomesDao().getAll(player.getUniqueId()); + assertEquals(1, homes.size()); + assertEquals("base", homes.get(0).getName()); + assertEquals(Material.WHITE_WOOL.name(), homes.get(0).getMaterial()); + assertNull(homes.get(0).getDescription()); } @Test diff --git a/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java index eebbfe3..d0a5f05 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -31,14 +32,62 @@ void consoleIsTurnedAway() { } @Test - void wrongArgumentCountIsRejected() { + void tooManyArgumentsAreRejected() { TestPlayer player = addTestPlayer("traveller"); - server.execute("go-home", player).assertSucceeded(); + server.execute("go-home", player, "base", "camp").assertSucceeded(); assertTrue(player.nextMessage().contains("Incorrect number of arguments")); } + @Test + void aBareCommandTeleportsToTheDefaultHome() { + TestPlayer player = addTestPlayer("traveller"); + player.teleport(new Location(overworld, 0, 64, 0)); + HomeFixtures.persist(HomeFixtures.home(player, "default", new Location(overworld, 44, 70, 44))); + plugin.getConfig().set("delay", 0); + + server.execute("go-home", player).assertSucceeded(); + server.getScheduler().performTicks(100L); + + assertEquals(44, player.getLocation().getBlockX()); + assertEquals(44, player.getLocation().getBlockZ()); + } + + @Test + void aBareCommandWithNoDefaultHomeIsReported() { + TestPlayer player = addTestPlayer("traveller"); + HomeFixtures.persist(player, "base"); + + server.execute("go-home", player).assertSucceeded(); + + String message = player.nextMessage(); + assertTrue(message.contains("default"), message); + assertNull(new TeleportAttemptsDao().get(player)); + } + + @Test + void anUnknownHomeIsNamedInTheError() { + TestPlayer player = addTestPlayer("traveller"); + + server.execute("go-home", player, "nope").assertSucceeded(); + + String message = player.nextMessage(); + assertTrue(message.contains("nope"), message); + assertFalse(message.contains("%s"), message); + } + + @Test + void theUnknownHomeMessageIsOverridableInConfig() { + TestPlayer player = addTestPlayer("traveller"); + plugin.getConfig().set("homeDoesNotExist", "No home called %s here."); + + server.execute("go-home", player, "nope").assertSucceeded(); + + String message = player.nextMessage(); + assertTrue(message.contains("No home called nope here."), message); + } + @Test void withoutPermissionTheCommandIsRefused() { TestPlayer player = addTestPlayer("traveller"); From 4c4b3fd086fdcb8796011b4a7e071c17bff025bc Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 11:43:30 -0400 Subject: [PATCH 23/75] fix: validate the home name when creating one by command The GUI rename applied HomeNameValidator but create-home never did, so a double space, which Bukkit turns into an empty first argument, saved a home with an empty name. No command can address such a home, so no command can delete it either, and on a server that denies sh2.manage-homes it was unreachable in game and kept occupying a max-homes slot. maxHomeNameLength was enforced on rename and ignored on create for the same reason. --- .../sethomestwo/commands/CreateHome.java | 23 ++++++++++++- .../sethomestwo/commands/CreateHomeTest.java | 34 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java b/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java index 299ee96..02d69a2 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java @@ -7,6 +7,7 @@ import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.utils.ChatUtils; import com.samleighton.sethomestwo.utils.ConfigUtil; +import com.samleighton.sethomestwo.utils.HomeNameValidator; import com.samleighton.sethomestwo.utils.HomesUtil; import com.samleighton.sethomestwo.utils.ServerUtil; import net.luckperms.api.LuckPerms; @@ -62,7 +63,27 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command // Extract parameters from command arguments. A bare command is the v1 // form, naming the home rather than erroring. - String homeName = args.length < 1 ? HomesUtil.DEFAULT_HOME_NAME : args[0]; + String homeName = HomeNameValidator.normalise( + args.length < 1 ? HomesUtil.DEFAULT_HOME_NAME : args[0]); + + // The same shape rules the GUI rename applies. A double space yields an + // empty first argument, and an empty name leaves a home that no command + // can address, so none can delete it either. + int maxNameLength = ConfigUtil.getConfig().getInt("maxHomeNameLength", 32); + + switch (HomeNameValidator.validate(homeName, maxNameLength)) { + case EMPTY: + ChatUtils.sendError(player, ConfigUtil.getConfig().getString( + "invalidHomeName", UserError.INVALID_HOME_NAME.getValue())); + return true; + case TOO_LONG: + String tooLong = ConfigUtil.getConfig().getString( + "homeNameTooLong", UserError.HOME_NAME_TOO_LONG.getValue()); + ChatUtils.sendError(player, String.format(tooLong, maxNameLength)); + return true; + default: + break; + } Material mat = null; int descriptionStart = 1; diff --git a/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java index 3193e6b..6711f75 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java @@ -305,4 +305,38 @@ void theConfiguredDefaultIconIsUsedWhenNoneIsGiven() { assertEquals(Material.CHEST.name(), new HomesDao().getAll(player.getUniqueId()).get(0).getMaterial()); } + + @Test + void anEmptyHomeNameIsRejected() { + PlayerMock player = addPlayer(); + + // A double space makes Bukkit hand over an empty first argument. Saved + // as-is it produces a home no command can name, so no command can + // delete it either. + server.dispatchCommand(player, "create-home base"); + + assertTrue(player.nextMessage().contains("must not be blank")); + assertTrue(new HomesDao().getAll(player.getUniqueId()).isEmpty()); + } + + @Test + void aHomeNameOverTheConfiguredLimitIsRejected() { + PlayerMock player = addPlayer(); + plugin.getConfig().set("maxHomeNameLength", 8); + + server.execute("create-home", player, "waaaaaaaaaaaytoolong").assertSucceeded(); + + assertTrue(player.nextMessage().contains("too long")); + assertTrue(new HomesDao().getAll(player.getUniqueId()).isEmpty()); + } + + @Test + void aNameWithinTheConfiguredLimitIsAccepted() { + PlayerMock player = addPlayer(); + plugin.getConfig().set("maxHomeNameLength", 8); + + server.execute("create-home", player, "base").assertSucceeded(); + + assertEquals(1, new HomesDao().getAll(player.getUniqueId()).size()); + } } From 29a02bbc2ac53cfbe432b399addad6c7c9b7754d Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 11:58:55 -0400 Subject: [PATCH 24/75] feat: add granular bypass nodes for max homes, the blacklist and teleport delay Version 1 had a single homes.config_bypass node covering all three. Splitting it lets an admin grant a delay skip without also granting blacklist bypass. sh2.bypass-blacklist is enforced on create, on move, and on the reads that decide whether a home is teleportable, which is passed to HomesDao by its callers rather than threading a Player into the DAO. --- .../sethomestwo/commands/CreateHome.java | 4 +- .../sethomestwo/commands/GoHome.java | 2 +- .../sethomestwo/commands/ListHomes.java | 2 +- .../sethomestwo/commands/OpenHomesGui.java | 2 +- .../samleighton/sethomestwo/dao/HomesDao.java | 15 +- .../sethomestwo/dao/TeleportAttemptsDao.java | 1 + .../events/RightClickHomeItem.java | 2 +- .../sethomestwo/gui/HomeActionsGui.java | 5 +- .../samleighton/sethomestwo/models/Home.java | 6 +- src/main/resources/plugin.yml | 12 + .../sethomestwo/gui/HomeActionsGuiTest.java | 55 ++++ .../sethomestwo/utils/BypassNodesTest.java | 239 ++++++++++++++++++ 12 files changed, 331 insertions(+), 14 deletions(-) create mode 100644 src/test/java/com/samleighton/sethomestwo/utils/BypassNodesTest.java diff --git a/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java b/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java index 02d69a2..51b3e66 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/CreateHome.java @@ -55,7 +55,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command String playerDimension = player.getWorld().getEnvironment().toString(); // Check if player is in a blacklisted dimension before creating home - if (ServerUtil.isWorldBlacklisted(player.getWorld())) { + if (!player.hasPermission("sh2.bypass-blacklist") && ServerUtil.isWorldBlacklisted(player.getWorld())) { String errorMessage = ConfigUtil.getConfig().getString("dimensionBlacklisted", UserError.DIMENSION_IS_BLACKLISTED.getValue()); ChatUtils.sendError(player, errorMessage); return true; @@ -144,6 +144,8 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command } private boolean maxHomesReached(Player player, Dao homesDao){ + if (player.hasPermission("sh2.bypass-max-homes")) return false; + boolean isMaxHomesEnabled = ConfigUtil.getConfig().getBoolean("maxHomeEnabled", false); if (!isMaxHomesEnabled) return false; diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java b/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java index 94fb557..fd0aca0 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java @@ -41,7 +41,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command // Get players home dao instance String desiredHomeName = args.length < 1 ? HomesUtil.DEFAULT_HOME_NAME : args[0]; - Dao homesDao = new HomesDao(); + Dao homesDao = new HomesDao(player.hasPermission("sh2.bypass-blacklist")); ArrayList playerHomes = (ArrayList) homesDao.getAll(player.getUniqueId()); Home homeToTeleportTo = null; diff --git a/src/main/java/com/samleighton/sethomestwo/commands/ListHomes.java b/src/main/java/com/samleighton/sethomestwo/commands/ListHomes.java index cee44ef..eab1f49 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/ListHomes.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/ListHomes.java @@ -39,7 +39,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } - Dao homesDao = new HomesDao(); + Dao homesDao = new HomesDao(player.hasPermission("sh2.bypass-blacklist")); List playersHomes = homesDao.getAll(player.getUniqueId()); // Player has no homes guard diff --git a/src/main/java/com/samleighton/sethomestwo/commands/OpenHomesGui.java b/src/main/java/com/samleighton/sethomestwo/commands/OpenHomesGui.java index c8fe3af..3aaaf6e 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/OpenHomesGui.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/OpenHomesGui.java @@ -41,7 +41,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command return true; } - Dao homesDao = new HomesDao(); + Dao homesDao = new HomesDao(player.hasPermission("sh2.bypass-blacklist")); List playersHomes = homesDao.getAll(player.getUniqueId()); // Guard for no homes yet diff --git a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java index 9ded32f..a8154af 100644 --- a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java +++ b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java @@ -17,14 +17,19 @@ public class HomesDao extends SQLiteDao implements Dao { private final String TABLE_NAME = "players_homes"; - private boolean isAdmin = false; + private boolean bypassBlacklist = false; public HomesDao(){ super(); } - public HomesDao(boolean isAdmin){ + /** + * @param bypassBlacklist true to read homes without the blacklist marking them + * unreachable. Both the admin views of another player's + * homes and a holder of sh2.bypass-blacklist pass true. + */ + public HomesDao(boolean bypassBlacklist){ super(); - this.isAdmin = isAdmin; + this.bypassBlacklist = bypassBlacklist; } @Override @@ -71,9 +76,9 @@ public List getAll(Object... keys) { World homeWorld = Bukkit.getWorld(UUID.fromString(home.getWorld())); if (ServerUtil.isWorldBlacklisted(homeWorld, blacklistedWorlds)) { - if (!this.isAdmin) home.setDescription("Cannot teleport here: dimension blacklisted"); + if (!this.bypassBlacklist) home.setDescription("Cannot teleport here: dimension blacklisted"); - home.setCanTeleport(this.isAdmin); + home.setCanTeleport(this.bypassBlacklist); } playerHomes.add(home); diff --git a/src/main/java/com/samleighton/sethomestwo/dao/TeleportAttemptsDao.java b/src/main/java/com/samleighton/sethomestwo/dao/TeleportAttemptsDao.java index 4d85030..89bd528 100644 --- a/src/main/java/com/samleighton/sethomestwo/dao/TeleportAttemptsDao.java +++ b/src/main/java/com/samleighton/sethomestwo/dao/TeleportAttemptsDao.java @@ -97,6 +97,7 @@ public TeleportAttempt get(Object... keys) { // Skip cancel on move check if (!ConfigUtil.getConfig().getBoolean("cancelOnMove", true)) return ta; + if (player.hasPermission("sh2.bypass-teleport-delay")) return ta; // Check if player has moved if (teleportStart.getX() != currLocation.getX() || teleportStart.getY() != currLocation.getY() || teleportStart.getZ() != currLocation.getZ()) diff --git a/src/main/java/com/samleighton/sethomestwo/events/RightClickHomeItem.java b/src/main/java/com/samleighton/sethomestwo/events/RightClickHomeItem.java index 99b0903..5cd67f9 100644 --- a/src/main/java/com/samleighton/sethomestwo/events/RightClickHomeItem.java +++ b/src/main/java/com/samleighton/sethomestwo/events/RightClickHomeItem.java @@ -71,7 +71,7 @@ public void onPlayerRightClickHomeItem(PlayerInteractEvent event) { return; } - Dao homesDao = new HomesDao(); + Dao homesDao = new HomesDao(player.hasPermission("sh2.bypass-blacklist")); List playersHomes = homesDao.getAll(player.getUniqueId()); GuiSession session = plugin.getGuiSessionMap().get(player.getUniqueId()); diff --git a/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java b/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java index 96faaca..bbc9b63 100644 --- a/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java +++ b/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java @@ -305,7 +305,8 @@ public static MoveOutcome applyMove(Player player, Home home) { if (home == null) return MoveOutcome.GONE; Location destination = player.getLocation(); - if (ServerUtil.isWorldBlacklisted(destination.getWorld())) return MoveOutcome.BLACKLISTED; + if (!player.hasPermission("sh2.bypass-blacklist") && ServerUtil.isWorldBlacklisted(destination.getWorld())) + return MoveOutcome.BLACKLISTED; home.setWorld(Objects.requireNonNull(destination.getWorld()).getUID().toString()); home.setX(destination.getX()); @@ -388,7 +389,7 @@ private Home reloadHome(Player player, GuiSession session) { */ private void returnToRefreshedList(Player player, GuiSession session) { player.closeInventory(); - HomesDao homesDao = new HomesDao(); + HomesDao homesDao = new HomesDao(player.hasPermission("sh2.bypass-blacklist")); session.getHomesGui().setHomes(homesDao.getAll(player.getUniqueId())); session.openHomeList(player); } diff --git a/src/main/java/com/samleighton/sethomestwo/models/Home.java b/src/main/java/com/samleighton/sethomestwo/models/Home.java index fb55fe2..af0626f 100644 --- a/src/main/java/com/samleighton/sethomestwo/models/Home.java +++ b/src/main/java/com/samleighton/sethomestwo/models/Home.java @@ -222,8 +222,10 @@ public void teleport(Player player) { TeleportSafetyUtil.prefetchChunks(prefetchDestination, plugin); } - // Send player countdown title. - AtomicInteger seconds = new AtomicInteger(ConfigUtil.getConfig().getInt("delay")); + // Send player countdown title. A zero delay runs the existing loop straight + // through to the teleport on its first pass. + AtomicInteger seconds = new AtomicInteger( + player.hasPermission("sh2.bypass-teleport-delay") ? 0 : ConfigUtil.getConfig().getInt("delay")); // Schedule repeating task for every second plugin.getServer().getScheduler().runTaskTimer(plugin, bukkitTask -> { diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index f85ad3e..5f166f5 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -125,6 +125,15 @@ permissions: sh2.move-player-home: description: Move another player's home. default: op + sh2.bypass-max-homes: + description: Create homes beyond the configured maximum. + default: op + sh2.bypass-blacklist: + description: Create, move and teleport to homes in blacklisted worlds. + default: op + sh2.bypass-teleport-delay: + description: Teleport instantly, without the countdown or the cancel on move. + default: op sh2.player: description: Everything an ordinary player needs. default: true @@ -152,3 +161,6 @@ permissions: sh2.go-player-home: true sh2.delete-player-home: true sh2.move-player-home: true + sh2.bypass-max-homes: true + sh2.bypass-blacklist: true + sh2.bypass-teleport-delay: true diff --git a/src/test/java/com/samleighton/sethomestwo/gui/HomeActionsGuiTest.java b/src/test/java/com/samleighton/sethomestwo/gui/HomeActionsGuiTest.java index 68d42ec..11b5a8b 100644 --- a/src/test/java/com/samleighton/sethomestwo/gui/HomeActionsGuiTest.java +++ b/src/test/java/com/samleighton/sethomestwo/gui/HomeActionsGuiTest.java @@ -1,11 +1,14 @@ package com.samleighton.sethomestwo.gui; +import com.samleighton.sethomestwo.SetHomesTwo; import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.datatypes.PersistentHome; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.NamespacedKey; import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryAction; import org.bukkit.event.inventory.InventoryClickEvent; @@ -15,6 +18,7 @@ import org.mockbukkit.mockbukkit.entity.PlayerMock; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -41,6 +45,22 @@ private void click(HomeActionsGui gui, GuiSession session, PlayerMock player, in gui.onClick(event, session); } + /** + * The home carried by the item in the given slot of the session's homes list. + * Its canTeleport flag is what gates {@link Home#teleport}. + */ + private Home listedHome(GuiSession session, int slot) { + ItemStack item = session.getHomesGui().getInventory().getItem(slot); + assertNotNull(item); + assertNotNull(item.getItemMeta()); + + Home listed = item.getItemMeta().getPersistentDataContainer() + .get(new NamespacedKey(SetHomesTwo.instance(), "home"), new PersistentHome()); + assertNotNull(listed); + + return listed; + } + private HomeActionsGui openSubmenu(PlayerMock player, Home home, GuiSession session) { HomeActionsGui gui = new HomeActionsGui(player, home); session.setActiveScreen(gui); @@ -141,6 +161,41 @@ void moveWritesThePlayersCurrentLocation() { assertEquals(-150.0, reloaded.getZ()); } + @Test + void theRefreshedListKeepsABlacklistBypassHoldersHomeTeleportable() { + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.bypass-blacklist", true); + Home home = HomeFixtures.persist(player, "base"); + HomeFixtures.blacklist(overworld.getName()); + + HomesGui homesGui = new HomesGui(player); + homesGui.setHomes(new HomesDao(true).getAll(player.getUniqueId())); + GuiSession session = new GuiSession(homesGui); + + HomeActionsGui gui = openSubmenu(player, home, session); + player.getInventory().setItemInMainHand(new ItemStack(Material.DIAMOND)); + click(gui, session, player, SLOT_ICON); + + assertTrue(listedHome(session, 0).getCanTeleport()); + } + + @Test + void theRefreshedListMarksABlacklistedHomeUnreachableWithoutTheNode() { + PlayerMock player = addPlayer(); + Home home = HomeFixtures.persist(player, "base"); + HomeFixtures.blacklist(overworld.getName()); + + HomesGui homesGui = new HomesGui(player); + homesGui.setHomes(new HomesDao().getAll(player.getUniqueId())); + GuiSession session = new GuiSession(homesGui); + + HomeActionsGui gui = openSubmenu(player, home, session); + player.getInventory().setItemInMainHand(new ItemStack(Material.DIAMOND)); + click(gui, session, player, SLOT_ICON); + + assertFalse(listedHome(session, 0).getCanTeleport()); + } + @Test void moveIntoABlacklistedDimensionIsRefusedAndWritesNothing() { PlayerMock player = addPlayer(); diff --git a/src/test/java/com/samleighton/sethomestwo/utils/BypassNodesTest.java b/src/test/java/com/samleighton/sethomestwo/utils/BypassNodesTest.java new file mode 100644 index 0000000..afc1f18 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/utils/BypassNodesTest.java @@ -0,0 +1,239 @@ +package com.samleighton.sethomestwo.utils; + +import com.samleighton.sethomestwo.dao.Dao; +import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.dao.TeleportAttemptsDao; +import com.samleighton.sethomestwo.models.Home; +import com.samleighton.sethomestwo.models.TeleportAttempt; +import com.samleighton.sethomestwo.support.HomeFixtures; +import com.samleighton.sethomestwo.support.ServerTestBase; +import com.samleighton.sethomestwo.support.TestPlayer; +import org.bukkit.Location; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The three bypass nodes, each proven with a holder and a non-holder so the node + * is what makes the difference rather than the surrounding config. + */ +class BypassNodesTest extends ServerTestBase { + + private static final List NODES = List.of( + "sh2.bypass-max-homes", + "sh2.bypass-blacklist", + "sh2.bypass-teleport-delay" + ); + + @Test + void aFixturePlayerHoldsNoneOfTheNodes() { + // All three default to op. An opped fixture would switch off the rules the + // blacklist and max-homes suites pin, so those tests would pass for the + // wrong reason. + TestPlayer player = addPlayer(); + + assertFalse(player.isOp(), "a fixture player must not be an operator"); + NODES.forEach(node -> assertFalse(player.hasPermission(node), node)); + } + + @Test + void theNodesDefaultToOpAndSitInTheAdminBundle() { + Permission adminBundle = server.getPluginManager().getPermission("sh2.admin"); + assertNotNull(adminBundle); + + for (String node : NODES) { + Permission permission = server.getPluginManager().getPermission(node); + assertNotNull(permission, node); + assertEquals(PermissionDefault.OP, permission.getDefault(), node); + assertTrue(adminBundle.getChildren().containsKey(node), node); + } + } + + @Test + void aHolderOfTheMaxHomesNodeExceedsTheLimit() { + limitToOneHome(); + TestPlayer player = addPlayer(); + player.addAttachment(plugin, "sh2.bypass-max-homes", true); + HomeFixtures.persist(player, "base"); + + server.execute("create-home", player, "second").assertSucceeded(); + + assertEquals(2, new HomesDao().getAll(player.getUniqueId()).size()); + } + + @Test + void withoutTheMaxHomesNodeTheLimitApplies() { + limitToOneHome(); + TestPlayer player = addPlayer(); + HomeFixtures.persist(player, "base"); + + server.execute("create-home", player, "second").assertSucceeded(); + + assertEquals(1, new HomesDao().getAll(player.getUniqueId()).size()); + } + + @Test + void aHolderOfTheBlacklistNodeCreatesAHomeInABlacklistedWorld() { + TestPlayer player = addPlayer(); + player.addAttachment(plugin, "sh2.bypass-blacklist", true); + HomeFixtures.blacklist(nether.getName()); + player.teleport(new Location(nether, 10, 70, 10)); + + server.execute("create-home", player, "base").assertSucceeded(); + + assertEquals(1, new HomesDao().getAll(player.getUniqueId()).size()); + } + + @Test + void withoutTheBlacklistNodeCreatingInABlacklistedWorldIsRefused() { + TestPlayer player = addPlayer(); + HomeFixtures.blacklist(nether.getName()); + player.teleport(new Location(nether, 10, 70, 10)); + + server.execute("create-home", player, "base").assertSucceeded(); + + assertTrue(player.nextMessage().contains("blacklisted")); + assertTrue(new HomesDao().getAll(player.getUniqueId()).isEmpty()); + } + + @Test + void aHolderOfTheBlacklistNodeMovesAHomeIntoABlacklistedWorld() { + TestPlayer player = addPlayer(); + player.addAttachment(plugin, "sh2.bypass-blacklist", true); + HomeFixtures.persist(player, "base"); + HomeFixtures.blacklist(nether.getName()); + player.teleport(new Location(nether, 10, 70, 10)); + + server.execute("move-home", player, "base").assertSucceeded(); + + Home moved = new HomesDao(true).getAll(player.getUniqueId()).get(0); + assertEquals(nether.getUID().toString(), moved.getWorld()); + assertEquals(10.0, moved.getX()); + } + + @Test + void withoutTheBlacklistNodeMovingIntoABlacklistedWorldIsRefused() { + TestPlayer player = addPlayer(); + HomeFixtures.persist(player, "base"); + HomeFixtures.blacklist(nether.getName()); + player.teleport(new Location(nether, 10, 70, 10)); + + server.execute("move-home", player, "base").assertSucceeded(); + + assertTrue(player.nextMessage().contains("blacklisted")); + assertEquals(overworld.getUID().toString(), + new HomesDao(true).getAll(player.getUniqueId()).get(0).getWorld()); + } + + @Test + void aHolderOfTheBlacklistNodeTeleportsToAHomeInABlacklistedWorld() { + disableTeleportSafety(); + plugin.getConfig().set("delay", 0); + TestPlayer player = addPlayer(); + player.addAttachment(plugin, "sh2.bypass-blacklist", true); + player.teleport(new Location(overworld, 0, 64, 0)); + HomeFixtures.persist(HomeFixtures.home(player, "far", new Location(nether, 100, 70, 100))); + HomeFixtures.blacklist(nether.getName()); + + server.execute("go-home", player, "far").assertSucceeded(); + server.getScheduler().performTicks(100L); + + assertEquals(nether.getName(), player.getWorld().getName()); + assertEquals(100, player.getLocation().getBlockX()); + } + + @Test + void withoutTheBlacklistNodeTheHomeStaysUnreachable() { + disableTeleportSafety(); + plugin.getConfig().set("delay", 0); + TestPlayer player = addPlayer(); + player.teleport(new Location(overworld, 0, 64, 0)); + HomeFixtures.persist(HomeFixtures.home(player, "far", new Location(nether, 100, 70, 100))); + HomeFixtures.blacklist(nether.getName()); + + server.execute("go-home", player, "far").assertSucceeded(); + server.getScheduler().performTicks(100L); + + assertEquals(overworld.getName(), player.getWorld().getName()); + assertEquals(0, player.getLocation().getBlockX()); + } + + @Test + void aHolderOfTheDelayNodeArrivesWithoutWaitingOutTheCountdown() { + disableTeleportSafety(); + plugin.getConfig().set("delay", 3); + TestPlayer player = addPlayer(); + player.addAttachment(plugin, "sh2.bypass-teleport-delay", true); + player.teleport(new Location(overworld, 0, 64, 0)); + HomeFixtures.persist(HomeFixtures.home(player, "base", new Location(overworld, 100, 70, 100))); + + server.execute("go-home", player, "base").assertSucceeded(); + server.getScheduler().performOneTick(); + + assertEquals(100, player.getLocation().getBlockX()); + } + + @Test + void withoutTheDelayNodeTheCountdownStillRuns() { + disableTeleportSafety(); + plugin.getConfig().set("delay", 3); + TestPlayer player = addPlayer(); + player.teleport(new Location(overworld, 0, 64, 0)); + HomeFixtures.persist(HomeFixtures.home(player, "base", new Location(overworld, 100, 70, 100))); + + server.execute("go-home", player, "base").assertSucceeded(); + server.getScheduler().performOneTick(); + + assertEquals(0, player.getLocation().getBlockX()); + } + + @Test + void aHolderOfTheDelayNodeIsNotCancelledByMoving() { + TestPlayer player = addPlayer(); + player.addAttachment(plugin, "sh2.bypass-teleport-delay", true); + player.teleport(new Location(overworld, 0, 64, 0)); + + Dao attempts = new TeleportAttemptsDao(); + attempts.save(new TeleportAttempt(player, player.getLocation())); + player.teleport(new Location(overworld, 20, 64, 20)); + + TeleportAttempt attempt = attempts.get(player); + assertNotNull(attempt); + assertTrue(attempt.canTeleport()); + } + + @Test + void withoutTheDelayNodeMovingCancelsTheAttempt() { + TestPlayer player = addPlayer(); + player.teleport(new Location(overworld, 0, 64, 0)); + + Dao attempts = new TeleportAttemptsDao(); + attempts.save(new TeleportAttempt(player, player.getLocation())); + player.teleport(new Location(overworld, 20, 64, 20)); + + TeleportAttempt attempt = attempts.get(player); + assertNotNull(attempt); + assertFalse(attempt.canTeleport()); + } + + private void limitToOneHome() { + plugin.getConfig().set("maxHomeEnabled", true); + plugin.getConfig().set("maxHomesType", "singular"); + plugin.getConfig().set("maxHomes", 1); + } + + /** + * TeleportSafetyUtil.prefetchChunks calls WorldMock.addPluginChunkTicket, which + * MockBukkit 4.110.0 does not implement. + */ + private void disableTeleportSafety() { + plugin.getConfig().set("teleportSafety", false); + } +} From 2a4486e9614a20995179cdf9898828d0d55f2a7f Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 12:17:16 -0400 Subject: [PATCH 25/75] fix: name the home when a command cannot find it Four commands reported 'That home no longer exists', which is the GUI's message for a home that vanished while the menu was open. Typed at a prompt after a typo it reads as though the plugin deleted the home, and it echoed nothing back. They now use the same message /go-home does, which names the home that was looked for. The two GUI sites keep the original wording, where it is accurate. --- .../sethomestwo/commands/DeletePlayerHome.java | 4 ++-- .../sethomestwo/commands/GoPlayerHome.java | 4 ++-- .../sethomestwo/commands/MoveHome.java | 4 ++-- .../sethomestwo/commands/MovePlayerHome.java | 4 ++-- .../sethomestwo/commands/MoveHomeTest.java | 5 ++++- .../commands/PlayerHomeAdminCommandsTest.java | 16 +++++++++++++--- 6 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java index 4180bca..8128a20 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java @@ -50,8 +50,8 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command Home home = homesDao.get(UUID.fromString(uuid), args[1]); if (home == null) { - ChatUtils.sendError(admin, ConfigUtil.getConfig().getString( - "homeNoLongerExists", UserError.HOME_NO_LONGER_EXISTS.getValue())); + ChatUtils.sendError(admin, String.format(ConfigUtil.getConfig().getString( + "homeDoesNotExist", UserError.HOME_DOES_NOT_EXIST.getValue()), args[1])); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java index ec43c05..f80a853 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java @@ -50,8 +50,8 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command Home home = new HomesDao(true).get(UUID.fromString(uuid), args[1]); if (home == null) { - ChatUtils.sendError(admin, ConfigUtil.getConfig().getString( - "homeNoLongerExists", UserError.HOME_NO_LONGER_EXISTS.getValue())); + ChatUtils.sendError(admin, String.format(ConfigUtil.getConfig().getString( + "homeDoesNotExist", UserError.HOME_DOES_NOT_EXIST.getValue()), args[1])); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/MoveHome.java b/src/main/java/com/samleighton/sethomestwo/commands/MoveHome.java index aaea642..58221de 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/MoveHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/MoveHome.java @@ -40,8 +40,8 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command switch (HomeActionsGui.applyMove(player, home)) { case GONE: - ChatUtils.sendError(player, ConfigUtil.getConfig().getString( - "homeNoLongerExists", UserError.HOME_NO_LONGER_EXISTS.getValue())); + ChatUtils.sendError(player, String.format(ConfigUtil.getConfig().getString( + "homeDoesNotExist", UserError.HOME_DOES_NOT_EXIST.getValue()), args[0])); break; case BLACKLISTED: ChatUtils.sendError(player, ConfigUtil.getConfig().getString( diff --git a/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java index c72ad55..89cf3f7 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java @@ -51,8 +51,8 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command switch (HomeActionsGui.applyMove(admin, home)) { case GONE: - ChatUtils.sendError(admin, ConfigUtil.getConfig().getString( - "homeNoLongerExists", UserError.HOME_NO_LONGER_EXISTS.getValue())); + ChatUtils.sendError(admin, String.format(ConfigUtil.getConfig().getString( + "homeDoesNotExist", UserError.HOME_DOES_NOT_EXIST.getValue()), args[1])); break; case BLACKLISTED: ChatUtils.sendError(admin, ConfigUtil.getConfig().getString( diff --git a/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java index 3f42fb9..530005e 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java @@ -9,6 +9,7 @@ import org.mockbukkit.mockbukkit.entity.PlayerMock; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class MoveHomeTest extends ServerTestBase { @@ -32,7 +33,9 @@ void anUnknownHomeIsReported() { server.execute("move-home", player, "nope").assertSucceeded(); - assertTrue(player.nextMessage().contains("no longer exists")); + String message = player.nextMessage(); + assertTrue(message.contains("nope"), message); + assertFalse(message.contains("%s"), message); } @Test diff --git a/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java b/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java index 7154720..cb9d9f3 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java @@ -208,13 +208,13 @@ void anUnknownHomeIsReportedByEachCommand() { admin.addAttachment(plugin, "sh2.go-player-home", true); server.execute("delete-player-home", admin, "Steve", "nope").assertSucceeded(); - assertTrue(admin.nextMessage().contains("no longer exists")); + assertUnknownHomeNamed(admin, "nope"); server.execute("move-player-home", admin, "Steve", "nope").assertSucceeded(); - assertTrue(admin.nextMessage().contains("no longer exists")); + assertUnknownHomeNamed(admin, "nope"); server.execute("go-player-home", admin, "Steve", "nope").assertSucceeded(); - assertTrue(admin.nextMessage().contains("no longer exists")); + assertUnknownHomeNamed(admin, "nope"); assertEquals(1, new HomesDao(true).getAll(target.getUniqueId()).size()); } @@ -235,4 +235,14 @@ void theSuccessMessageNamesTheOwnerAndHomeCanonically() { assertTrue(reply.contains("Steve"), reply); assertTrue(reply.contains("base"), reply); } + + /** + * Every command must name the home it looked for. "That home no longer + * exists" is the GUI's message, for a home that vanished mid-menu. + */ + private static void assertUnknownHomeNamed(PlayerMock admin, String typed) { + String message = admin.nextMessage(); + assertTrue(message.contains(typed), message); + assertFalse(message.contains("%s"), message); + } } From b1b95a5b64012ca327c145846dacd60266b51227 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 12:30:13 -0400 Subject: [PATCH 26/75] docs: document the new commands, permission config and blacklist fix README now covers /move-home and the three -of admin commands, the consolidated /blacklist, the bypass nodes and the two permission bundles, the config permissions block, a Set Homes v1 migration table for both commands and permissions, and the behaviour changes an existing v2 server will notice on upgrade. default-config.yml gains playerNotFound, playerHomeDeleted and playerHomeMoved, which the code already read but the file never shipped. Marks invalidHomeItem vestigial and documents the placeholder hazards. Rewords CREATE_HOME_USAGE, which carried the word "default" twice with two different meanings on the one line a player sees when they have no homes. --- .changeset/eager-badgers-return.md | 5 + .changeset/eager-seals-shine.md | 5 + .changeset/lucky-wolves-smile.md | 5 + .changeset/tidy-wolves-travel.md | 5 + README.md | 167 ++++++++++++++++-- .../sethomestwo/enums/UserInfo.java | 2 +- src/main/resources/default-config.yml | 37 +++- 7 files changed, 209 insertions(+), 17 deletions(-) create mode 100644 .changeset/eager-badgers-return.md create mode 100644 .changeset/eager-seals-shine.md create mode 100644 .changeset/lucky-wolves-smile.md create mode 100644 .changeset/tidy-wolves-travel.md diff --git a/.changeset/eager-badgers-return.md b/.changeset/eager-badgers-return.md new file mode 100644 index 0000000..21f82a6 --- /dev/null +++ b/.changeset/eager-badgers-return.md @@ -0,0 +1,5 @@ +--- +bump: minor +--- + +Added the Set Homes v1 admin commands. /home-of teleports you to another player's home, /delhome-of deletes one, and /uhome-of moves one to where you are standing. /uhome moves one of your own homes. All of them work on players who are offline, as long as they have saved homes, and player and home names now match without regard to case. diff --git a/.changeset/eager-seals-shine.md b/.changeset/eager-seals-shine.md new file mode 100644 index 0000000..8193e28 --- /dev/null +++ b/.changeset/eager-seals-shine.md @@ -0,0 +1,5 @@ +--- +bump: minor +--- + +/sethome and /home work with no arguments again, creating and finding a home called default, which is the name an unnamed Set Homes v1 home is imported under. /sethome also takes a description straight after the name as it did in v1, so a second word that names an item is read as the icon and the reply tells you which one it chose. Put d in the icon position to keep the whole phrase as the description. diff --git a/.changeset/lucky-wolves-smile.md b/.changeset/lucky-wolves-smile.md new file mode 100644 index 0000000..7162844 --- /dev/null +++ b/.changeset/lucky-wolves-smile.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +Fixed the world blacklist only ever being enforced on the first three worlds of a server. Blacklisting any world beyond those reported success and then did nothing. It is enforced from this release on, so if you have blacklisted more than three worlds, run /blacklist list before you update. Homes in a world that starts being enforced stop being reachable by players who do not hold sh2.bypass-blacklist. diff --git a/.changeset/tidy-wolves-travel.md b/.changeset/tidy-wolves-travel.md new file mode 100644 index 0000000..da0bf1a --- /dev/null +++ b/.changeset/tidy-wolves-travel.md @@ -0,0 +1,5 @@ +--- +bump: minor +--- + +Permission defaults can now be changed from config.yml with no permissions plugin installed. Uncomment the permissions block and set any sh2 node to true, false, op or not-op. Two bundles, sh2.player and sh2.admin, move a whole role at once, and three new bypass permissions were added for admins: sh2.bypass-max-homes, sh2.bypass-blacklist and sh2.bypass-teleport-delay. diff --git a/README.md b/README.md index 606df93..b02089f 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ - **Teleports that do not kill you.** Set Homes Two checks the destination and relocates you to the nearest safe spot rather than dropping you into blocks, lava, or a fall. - **Switch without losing anything.** One command imports every home from EssentialsX or Set Homes v1, and shows you exactly what it will do before it does it. - **Per-rank home limits.** Give donors more homes than default players with LuckPerms groups, or set one server-wide limit. +- **Permissions you can change from the config.** Every `sh2.*` node has a sensible default, and any of them can be moved in `config.yml`. No permissions plugin required. ## Quick start @@ -21,7 +22,7 @@ 2. Run `/sethome base` where you are standing. 3. Run `/homes` and click it. -That is genuinely the whole setup. Since 1.1.0 the player-facing permissions default to granted, so your players can create and use homes the moment the plugin loads. You only need a permissions plugin if you want per-rank home limits. +That is genuinely the whole setup. Since 1.1.0 the player-facing permissions default to granted, so your players can create and use homes the moment the plugin loads. If you want to take one of those away, or hand an admin command to a non-operator, you can do it from `config.yml`. A permissions plugin is only needed for per-rank home limits. ## Managing homes @@ -66,17 +67,69 @@ Your players keep their homes. The old plugin does not even need to be running, Afterwards, remove the old jar. Set Homes Two provides `/sethome`, `/home` and `/delhome`, and two plugins claiming the same commands will fight over them. +
+Set Homes v1: what each command and permission became + +| Set Homes v1 | Set Homes Two | +| --- | --- | +| `/sethome [name] [description]` | `/sethome [name] [icon] [description]` | +| `/home [name]` | `/home [name]` | +| `/homes [player]` | `/list-homes` for your own list, `/get-player-homes ` for someone else's. `/homes` now opens the menu instead. | +| `/delhome [name]` | `/delhome ` | +| `/uhome [description]` | `/uhome ` | +| `/home-of [home]` | `/home-of ` | +| `/delhome-of [home]` | `/delhome-of ` | +| `/uhome-of [home]` | `/uhome-of ` | +| `/blacklist ` | `/blacklist ` | +| `/setmax ` | `/set-max-homes ` | +| `/strike` | Gone. See below. | + +| v1 permission | Set Homes Two permission | +| --- | --- | +| `homes.home` | `sh2.go-home`, plus `sh2.teleport` to actually arrive | +| `homes.sethome` | `sh2.create-home` | +| `homes.delhome` | `sh2.delete-home` | +| `homes.gethomes` | `sh2.get-player-homes` | +| `homes.home-of` | `sh2.go-player-home` | +| `homes.delhome-of` | `sh2.delete-player-home` | +| `homes.uhome` | `sh2.move-home` | +| `homes.uhome-of` | `sh2.move-player-home` | +| `homes.blacklist_add` | `sh2.add-to-blacklist` | +| `homes.blacklist_remove` | `sh2.remove-from-blacklist` | +| `homes.blacklist_list` | `sh2.get-blacklisted-dimensions` | +| `homes.setmax` | `sh2.set-max-homes` | +| `homes.config_bypass` | `sh2.bypass-max-homes`, `sh2.bypass-blacklist` and `sh2.bypass-teleport-delay` | +| `homes.strike` | Nothing | +| `homes.*` | `sh2.admin` | + +Worth knowing before you copy a permissions file across: + +- **`homes.config_bypass` is three nodes now.** In v1 it let a player exceed the home limit, set homes in blacklisted worlds, and skip the teleport delay and cooldown, all at once. Grant all three `sh2.bypass-*` nodes to reproduce that. Nothing is lost on the cooldown, because Set Homes Two has no cooldown feature. +- **Your v1 unnamed home is called `default`.** The importer files it under that name, and a bare `/sethome` or `/home` uses the same name, so both keep working exactly as they did. `/home-of steve default` reaches an imported unnamed home. +- **`/sethome` takes a description straight after the name again**, as it did in v1. Set Homes Two adds an optional icon in between, so a second word naming a real item is read as the icon. `/sethome base d my main base` forces the default icon and keeps the whole phrase. +- **The one-letter aliases are not provided.** v1 registered `/h`, `/sh`, `/dh`, `/lh`, `/ho`, `/dho`, `/uh`, `/uho`, `/bl` and `/sm`. `/h` in particular collides with several other homes plugins, and Bukkit resolves a collision silently by prefixing one of them, which is worse than not having it. If you want them, map them yourself in the server's own `commands.yml`. +- **`/setmax` is not an alias either.** The command is `/set-max-homes`. +- **`/strike` was removed on purpose.** It was a lightning wand, not a homes feature. +- **`/homes` means something different.** In v1 it printed a chat list. In Set Homes Two it opens the homes menu, and `/list-homes` prints the chat list. + +
+ ## Commands | Command | Long form | What it does | | --- | --- | --- | -| `/sethome [item] [description]` | `/create-home` | Creates a home where you stand. The optional item becomes its icon. | -| `/home ` | `/go-home` | Teleports you to a home. | +| `/sethome [name] [icon] [description]` | `/create-home` | Creates a home where you stand. With no name it is called `default`. | +| `/home [name]` | `/go-home` | Teleports you to a home. With no name it goes to `default`. | | `/homes` | - | Opens the homes menu. | | `/delhome ` | `/delete-home` | Deletes a home. | +| `/uhome ` | `/move-home` | Moves one of your homes to where you are standing. | | `/list-homes` | - | Lists your homes in chat. Click a name to teleport. | | `/give-homes-item` | - | Gives you the item that opens the menu. | +Home names ignore case, so `/home Base` and `/delhome Base` both find a home called `base`. + +On `/sethome`, a second word that names a real item becomes the icon, and everything after it is the description. So `/sethome base stone house` creates `base` with a stone icon and the description "house". If you wanted the whole phrase as the description, put `d` in the icon position: `/sethome base d stone house`. The reply names the icon it chose, so there is never any guessing. +
Admin commands @@ -84,15 +137,24 @@ Afterwards, remove the old jar. Set Homes Two provides `/sethome`, `/home` and ` | --- | --- | | `/set-max-homes [group] ` | Sets the home limit, per LuckPerms group or server-wide. | | `/get-player-homes ` | Lists another player's homes. | -| `/blacklist add ` (alias `/add-to-blacklist`) | Stops homes being set in a dimension. | -| `/blacklist remove ` (alias `/remove-from-blacklist`) | Lifts the restriction again. | -| `/blacklist list` (alias `/get-blacklisted-dimensions`) | Shows which dimensions are blacklisted. | +| `/home-of ` (long form `/go-player-home`) | Teleports you to another player's home. | +| `/delhome-of ` (long form `/delete-player-home`) | Deletes another player's home. | +| `/uhome-of ` (long form `/move-player-home`) | Moves another player's home to where you are standing. | +| `/blacklist add ` (alias `/add-to-blacklist`) | Stops homes being set in a world. | +| `/blacklist remove ` (alias `/remove-from-blacklist`) | Lifts the restriction again. | +| `/blacklist list` (alias `/get-blacklisted-dimensions`) | Shows which worlds are blacklisted. | | `/import-homes [confirm]` | Imports homes from another plugin. Dry-run unless `confirm` is given. | +The three blacklist commands are now one command with three aliases. Nothing you already type changes: `/add-to-blacklist world_nether` still adds that world, and `/get-blacklisted-dimensions` still lists them. Give worlds by the name the server knows them by, in lower case, which on a default setup means `world`, `world_nether` and `world_the_end`. + +The three commands that take a player accept anyone who has saved homes, whether or not they are online. Tab completion only offers online players, because there is no lookup for every stored name. Both the player name and the home name ignore case. +
## Permissions +Nothing here needs a permissions plugin. Every node has a default, and you can change any of those defaults from `config.yml`. See [Changing permissions](#changing-permissions) below. +
Full permission list @@ -105,15 +167,70 @@ Afterwards, remove the old jar. Set Homes Two provides `/sethome`, `/home` and ` | `sh2.delete-home` | everyone | Deleting your own homes | | `sh2.give-homes-item` | everyone | Getting the menu item | | `sh2.manage-homes` | everyone | Renaming, moving, re-iconing and deleting from the GUI | +| `sh2.move-home` | everyone | Moving your own home with `/uhome` | | `sh2.set-max-homes` | OP | Setting home limits | | `sh2.get-player-homes` | OP | Viewing another player's homes | -| `sh2.add-to-blacklist` | OP | Blacklisting a dimension | -| `sh2.remove-from-blacklist` | OP | Un-blacklisting a dimension | -| `sh2.get-blacklisted-dimensions` | OP | Listing blacklisted dimensions | +| `sh2.go-player-home` | OP | Teleporting to another player's home | +| `sh2.delete-player-home` | OP | Deleting another player's home | +| `sh2.move-player-home` | OP | Moving another player's home | +| `sh2.add-to-blacklist` | OP | Blacklisting a world | +| `sh2.remove-from-blacklist` | OP | Un-blacklisting a world | +| `sh2.get-blacklisted-dimensions` | OP | Listing blacklisted worlds | | `sh2.import-homes` | OP | Importing from another plugin | +| `sh2.update-notify` | OP | Being told on join that a newer release exists | +| `sh2.bypass-max-homes` | OP | Creating homes past the configured maximum, whether the limit is server-wide or per group | +| `sh2.bypass-blacklist` | OP | Creating a home in a blacklisted world, moving a home into one, and teleporting to a home already in one. It also stops `/homes` and `/list-homes` replacing the home's description with "Cannot teleport here: dimension blacklisted" | +| `sh2.bypass-teleport-delay` | OP | Teleporting with no countdown, and not being cancelled by moving | + +Two bundles group those nodes so you can grant a whole role at once: + +| Bundle | Default | Contains | +| --- | --- | --- | +| `sh2.player` | everyone | `sh2.create-home`, `sh2.go-home`, `sh2.list-homes`, `sh2.delete-home`, `sh2.teleport`, `sh2.give-homes-item`, `sh2.manage-homes`, `sh2.move-home` | +| `sh2.admin` | OP | `sh2.player`, plus every OP node in the table above | + +Note that `sh2.move-home` sits in `sh2.player`, not behind `sh2.manage-homes`. If you took `sh2.manage-homes` away to stop players relocating their homes, deny `sh2.move-home` as well or `/uhome` gives the ability back.
+### Changing permissions + +You can change any node's default from `config.yml`, with no permissions plugin involved. Uncomment the `permissions:` block and list the nodes you want to move: + +```yaml +permissions: + sh2.manage-homes: false + sh2.get-player-homes: true + sh2.import-homes: op +``` + +Accepted values are `true` (everyone), `false` (nobody), `op` (operators only) and `not-op` (everyone except operators). Bukkit reads these, so case variants such as `OP` and spellings such as `notop` are accepted too, but stick to the four above. A value it cannot read is skipped with a warning in the server log, as is a node name that does not exist, and every override that does apply is written to the log at startup. There is no wildcard form, so list each node. + +The two bundles are nodes in their own right, so `sh2.player: false` moves the whole player set at once and `sh2.admin: true` hands every admin command to everybody. That last one is rarely what you want. + +**This only changes a default.** If you run LuckPerms or similar, an explicit grant or deny there still wins. The config block decides what happens to a player the permissions plugin says nothing about. + +Take care with `sh2.import-homes`. `/import-homes confirm` writes homes for every player on the server and there is no second check inside the command, so granting it to everyone is a real risk. The plugin logs a warning if you move it off `op`. + +With LuckPerms, the equivalent one-liner is: + +``` +/lp group default permission set sh2.player true +``` + +If you would rather not touch `config.yml` at all, the server's own `permissions.yml` can wrap the nodes in a rank of your own: + +```yaml +myserver.moderator: + default: false + children: + sh2.player: true + sh2.get-player-homes: true + sh2.go-player-home: true +``` + +Then grant `myserver.moderator` to whoever should have it. + ## Configuration Settings live in **`plugins/SetHomesTwo/config.yml`** on your server, written the first time the plugin starts. Edit it in any text editor, save, then **restart the server**. There is no in-game reload command, so changes do not apply until the server comes back up. @@ -128,9 +245,10 @@ The file is commented throughout, and every message the plugin sends can be rewr | `maxHomeEnabled` | `false` | Turn home limits on. | | `maxHomesType` | `groups` | `singular` for one server-wide limit, `groups` for per-rank limits. | | `openHomeItem` | `compass` | The item players right-click to open the menu. | -| `defaultHomeItem` | `white_wool` | Icon used when a home is created without one. | +| `defaultHomeItem` | `white_wool` | Icon a home gets when the player names none. | | `inventoryTitle` | `Your homes` | Title of the homes menu. | | `maxHomeNameLength` | `32` | Longest home name allowed. | +| `permissions` | commented out | Changes the default of any `sh2.*` node. See [Changing permissions](#changing-permissions). | Per-rank limits need [LuckPerms](https://luckperms.net/download) and `maxHomesType: groups`. @@ -145,6 +263,19 @@ To pick one up, copy the key you want out of [`default-config.yml`](https://gith +## Upgrading an existing Set Homes Two server + +Your homes, your config and your permissions carry over untouched. These are the changes a player or an admin can notice, listed so that nobody has to work them out from the symptom. Only the first one is worth checking before you update. + +- **The world blacklist now works on every world.** Blacklisting always accepted any world name and reported success, but only the first three worlds were ever enforced, so a fourth was quietly ignored. It is enforced now. If you blacklisted a world beyond the first three, check `/blacklist list`, because homes there will start being refused and existing ones will stop being reachable. +- **`/sethome base stone house` now means a stone icon and the description "house".** A second word naming a real item is taken as the icon. Put `d` in that position to force the default icon and keep the whole phrase: `/sethome base d stone house`. The reply names the icon it chose. +- **A word like `water`, `fire`, `lava` or `wall_torch` stays description text.** Those are real materials but not items, so they cannot be a home icon. Rather than refusing the command, Set Homes Two treats them as the start of the description. +- **Home names and player names now ignore case everywhere.** `/home Base` always found `base`; `/delhome`, `/uhome` and the admin commands now match it. `/delhome Base` therefore deletes `base`. +- **`defaultHomeItem` now applies to homes players create.** It used to apply only to imported homes, so a server that set it to `chest` still got white wool on everything new. New homes with no icon now take the configured item. +- **`/sethome` now checks the home name.** A blank name is refused and `maxHomeNameLength` is enforced. Both were previously checked only when renaming from the menu, which allowed a home nothing could address. +- **A missing home is named in the error.** Four commands used to say "That home no longer exists"; they now say "The home 'base' does not exist". The menu keeps the old wording, where it is still the accurate one. +- **Operators can now bypass the blacklist.** That includes moving another player's home into a blacklisted world. The owner, who does not hold `sh2.bypass-blacklist`, then sees "Cannot teleport here: dimension blacklisted" on that home and cannot reach it, so move it back out or grant them the node. + ## FAQ
@@ -157,7 +288,21 @@ Three ways, all equivalent: `/home `, opening `/homes` and left-clicking,
Only OPs can create homes. How do I let everyone in? -Update to 1.1.0 or later. On older versions every permission defaulted to OP; they now default to granted for players. If you use a permissions plugin that denies unlisted nodes, grant `sh2.create-home`, `sh2.go-home` and `sh2.teleport`. +Update to 1.1.0 or later. On older versions every permission defaulted to OP; they now default to granted for players. If you use a permissions plugin that denies unlisted nodes, grant `sh2.player`, which covers every ordinary player node in one go. + +
+ +
+How do I turn a permission off without installing a permissions plugin? + +Uncomment the `permissions:` block in `config.yml` and set the node to `false`, `op` or `not-op`. See [Changing permissions](#changing-permissions). + +
+ +
+A player says one of their homes shows "Cannot teleport here: dimension blacklisted". Why? + +The world that home is in has been blacklisted, so the home is listed but not reachable. Check `/blacklist list`. Either take the world off the list with `/blacklist remove `, or move the home somewhere else with `/uhome-of ` while standing where it should go.
diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java b/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java index faae905..d1f679b 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java @@ -3,7 +3,7 @@ public enum UserInfo { GET_PLAYER_HOMES_USAGE("Usage: /get-player-homes [playerName]"), BLACKLIST_USAGE("Usage: /blacklist [world]"), - CREATE_HOME_USAGE("Usage: /create-home [name] [display_material | d | default] [description]"), + CREATE_HOME_USAGE("Usage: /create-home [name] [icon material, or d for the default icon] [description]. Omit the name and the home is called 'default'."), NO_HOMES("You have not setup any homes yet, you can use the /create-home command to create one."), NO_MAX_HOMES("There is no max number of homes."), NO_BLACKLISTED_DIMENSIONS("No dimensions are blacklisted"), diff --git a/src/main/resources/default-config.yml b/src/main/resources/default-config.yml index b3ff08e..7d5d917 100644 --- a/src/main/resources/default-config.yml +++ b/src/main/resources/default-config.yml @@ -3,9 +3,12 @@ # right-clicking to open the homes list. openHomeItem: "compass" -# The default item when create-home command -# is only given a home name, or is only -# provided with the default material name. +# The icon a home gets when the create-home command +# names no icon, or is given "d" or "default" in the +# icon position. This covers a bare /sethome, a +# /sethome with only a name, and a /sethome whose +# second word is description text rather than an item. +# A value naming no item falls back to white wool. defaultHomeItem: "white_wool" # The item to use for moving to the next page @@ -51,7 +54,19 @@ homeCreated: "%s has been created successfully. Icon: %s" homeDeleted: "%s has been deleted successfully." # You can use %s here as a placeholder for the players home name. dimensionAddedToBlacklist: "%s has been added to the blacklist." # You can use %s here as a placeholder for the dimension names. +# Replies to the admin commands that act on another player's home. +# Both take TWO placeholders, in this order: the player name first, then +# the home name. Swapping them swaps the two words in the message, and +# adding a third %s makes the command fail with a formatting error. +playerHomeDeleted: "%s's home '%s' has been deleted." +playerHomeMoved: "%s's home '%s' has been moved to your location." + # -- ERROR MESSAGES -- + +# Vestigial. No code path reaches this message any more: create-home treats a +# word that names no item as description text, and the management menu can only +# be handed an item you are already holding. Kept so an existing config.yml that +# overrides it is not suddenly rejected. invalidHomeItem: "The material you entered is not valid, please try a different one." falseHomeItem: "This home item does not belong to you." teleportedWhileTeleporting: "You cannot teleport while already teleporting." @@ -60,7 +75,14 @@ noHomes: "You have not created any homes yet. Use /create-home to make your firs teleportToBlacklistedDimension: "You cannot teleport to this home because the dimension it is in has been blacklisted." maxHomesReached: "You have reached the maximum number of homes allowed." dimensionBlacklisted: "You cannot set a home in this dimension because it has been blacklisted." -homeDoesNotExist: "The home '%s' does not exist." # %s is the home name that was looked for. A bare /go-home looks for 'default'. +# Shown by /go-home, /move-home and the three admin commands that take a home +# name. The single %s is the home name that was looked for; a bare /go-home +# looks for 'default'. Keep exactly one %s. A bare percent sign anywhere in an +# override throws a formatting error every time the message is sent. +homeDoesNotExist: "The home '%s' does not exist." + +# Shown when a player name matches nobody online and nobody with saved homes. +playerNotFound: "No player by that name is online or has any saved homes." unsafeHome: "Teleport cancelled: this home is not safe to stand in and no safe spot was found nearby." movedToSafeSpot: "Your home was not safe to stand in, so you were moved to the nearest safe spot." @@ -135,7 +157,12 @@ updateReminderDays: 7 # wins. Unknown node names are ignored with a warning in the server log, and # every applied override is logged at startup. # -# There is no wildcard form. List each node you want to change. +# There is no wildcard form. List each node you want to change. The two +# bundles, sh2.player and sh2.admin, are nodes in their own right, so you can +# name either of them here to move a whole set at once. +# +# Values are read by Bukkit, which also accepts case variants such as OP and +# True, plus its own spellings such as notop. Stick to the four above. # # Take care with sh2.import-homes: /import-homes confirm writes homes # for every player on the server, and the command has no second permission check. From 2c09501db4ff6eef19ed4f6d5c63884678657a00 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 12:34:01 -0400 Subject: [PATCH 27/75] fix: tell admins the real world names when the blacklist rejects one The rejection read 'Valid dimensions are (nether, overworld, end)', but the command validates against the server's actual world names, so all three of the words it suggested were themselves rejected. It now names what was typed and lists the server's own worlds. Also read dimensionAddedToBlacklist from config, which was documented but never consulted while its remove counterpart was consulted but never documented, and document homeItemName and homeItemLore, which were live and undiscoverable. --- .../sethomestwo/commands/Blacklist.java | 12 +++++++++--- .../sethomestwo/enums/UserError.java | 2 +- src/main/resources/default-config.yml | 14 +++++++++++++- .../sethomestwo/commands/BlacklistTest.java | 17 ++++++++++++++++- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java b/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java index bd5c6ab..bcaff19 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java @@ -117,7 +117,9 @@ private boolean add(Player player, String[] dimensions) { for (String dimension : dimensions) { if (!ServerUtil.getValidDimensions().contains(dimension)) { - ChatUtils.sendError(player, String.format(UserError.INVALID_DIMENSION.getValue(), dimension)); + ChatUtils.sendError(player, String.format( + ConfigUtil.getConfig().getString("invalidWorld", UserError.INVALID_WORLD.getValue()), + dimension, String.join(", ", ServerUtil.getValidDimensions()))); continue; } @@ -131,7 +133,9 @@ private boolean add(Player player, String[] dimensions) { Bukkit.getLogger().info(String.format("Failed to add dimension to blacklist. %s", dimension)); } - ChatUtils.sendSuccess(player, String.format(UserSuccess.DIMENSION_ADDED_TO_BLACKLIST.getValue(), dimension)); + ChatUtils.sendSuccess(player, String.format( + ConfigUtil.getConfig().getString("dimensionAddedToBlacklist", UserSuccess.DIMENSION_ADDED_TO_BLACKLIST.getValue()), + dimension)); } return true; @@ -154,7 +158,9 @@ private boolean remove(Player player, String[] dimensions) { for (String dimension : dimensions) { if (!ServerUtil.getValidDimensions().contains(dimension)) { - ChatUtils.sendError(player, String.format(UserError.INVALID_DIMENSION.getValue(), dimension)); + ChatUtils.sendError(player, String.format( + ConfigUtil.getConfig().getString("invalidWorld", UserError.INVALID_WORLD.getValue()), + dimension, String.join(", ", ServerUtil.getValidDimensions()))); continue; } diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserError.java b/src/main/java/com/samleighton/sethomestwo/enums/UserError.java index 35c55a3..69f5d02 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserError.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserError.java @@ -18,7 +18,7 @@ public enum UserError { /** Command Input Errors */ DIMENSION_IS_NOT_BLACKLISTED("The %s dimension has not been blacklisted yet therefore you cannot remove it."), - INVALID_DIMENSION("%s is not a valid dimension. Valid dimensions are (nether, overworld, end)."), + INVALID_WORLD("%s is not a valid world. This server's worlds are: %s"), DELETE_HOME_USAGE("Usage: /delete-home [name]"), INVALID_MATERIAL("The material you entered is not valid, please try a different one."), PLAYER_NOT_FOUND("No player by that name is online or has any saved homes."), diff --git a/src/main/resources/default-config.yml b/src/main/resources/default-config.yml index 7d5d917..1198576 100644 --- a/src/main/resources/default-config.yml +++ b/src/main/resources/default-config.yml @@ -52,7 +52,19 @@ teleportSafety: true # Relocate to the nearest safe spot (or cancel) when a home # is not: the command will fail with a formatting error every time. homeCreated: "%s has been created successfully. Icon: %s" homeDeleted: "%s has been deleted successfully." # You can use %s here as a placeholder for the players home name. -dimensionAddedToBlacklist: "%s has been added to the blacklist." # You can use %s here as a placeholder for the dimension names. +dimensionAddedToBlacklist: "%s has been added to the blacklist." # %s is the world name. +dimensionRemovedFromBlacklist: "%s has been removed from the blacklist." # %s is the world name. + +# Shown when a world name given to /blacklist is not a world on this server. +# Takes TWO placeholders, in this order: what was typed, then the list of +# this server's world names. A bare percent sign anywhere in an override +# throws a formatting error, so write %% if you need a literal one. +invalidWorld: "%s is not a valid world. This server's worlds are: %s" + +# The compass item handed out by /give-homes-item. +# homeItemName takes one placeholder, the player's name. +homeItemName: "Home's of %s" +homeItemLore: "Right click this item to open your home's list." # Replies to the admin commands that act on another player's home. # Both take TWO placeholders, in this order: the player name first, then diff --git a/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java b/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java index 843940c..b8f6474 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java @@ -98,7 +98,11 @@ void anUnknownWorldIsRejected() { server.execute("blacklist", player, "add", "not_a_world").assertSucceeded(); - assertTrue(player.nextMessage().contains("not a valid")); + String message = player.nextMessage(); + assertTrue(message.contains("not_a_world"), message); + // The old wording advertised nether, overworld and end, none of which + // the validator accepts. It must name the server's real worlds instead. + assertTrue(message.contains("world_nether"), message); assertTrue(new BlacklistDao().getAll().isEmpty()); } @@ -110,6 +114,17 @@ void anUnknownWorldIsRejected() { // server.execute, which always reports the canonical command name as the // label regardless of which alias was used to look it up. + @Test + void theAddSuccessMessageIsOverridableInConfig() { + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.add-to-blacklist", true); + plugin.getConfig().set("dimensionAddedToBlacklist", "Blocked %s."); + + server.execute("blacklist", player, "add", "world_nether").assertSucceeded(); + + assertTrue(player.nextMessage().contains("Blocked world_nether.")); + } + @Test void bareAddToBlacklistAliasWithNoSubcommandStillAdds() { PlayerMock player = addPlayer(); From 08baf078286306285e5bf5eb207033abcf84f15d Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 12:57:55 -0400 Subject: [PATCH 28/75] fix: make config permission overrides able to deny, and enforce sh2.teleport Lowering a node's default denied nothing. Bukkit writes the children of a default-granted parent straight into a player's effective permission map, and hasPermission reads that map before consulting the node's default, so the sh2.player and sh2.admin bundles kept granting whatever the config had just taken away. The plugin logged that the override applied, which made the failure silent. Overrides now detach the node from any bundle that lists it, and the bundles are the sole grant for their sets, so denying a bundle takes its whole set away. Stock defaults are unchanged and are now pinned by tests in both directions. sh2.teleport was documented as what lets a player actually arrive but was only checked when opening the menu from the compass item. It now sits at the single choke point every teleport route passes through. --- README.md | 2 + .../samleighton/sethomestwo/models/Home.java | 7 ++ .../utils/PermissionOverrides.java | 25 +++++ src/main/resources/plugin.yml | 42 +++---- .../sethomestwo/commands/GoHomeTest.java | 14 +++ .../sethomestwo/utils/BypassNodesTest.java | 4 +- .../utils/PermissionBundlesTest.java | 33 ++++++ .../utils/PermissionOverridesTest.java | 104 +++++++++++++++++- 8 files changed, 204 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index b02089f..c042385 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,8 @@ Two bundles group those nodes so you can grant a whole role at once: | `sh2.player` | everyone | `sh2.create-home`, `sh2.go-home`, `sh2.list-homes`, `sh2.delete-home`, `sh2.teleport`, `sh2.give-homes-item`, `sh2.manage-homes`, `sh2.move-home` | | `sh2.admin` | OP | `sh2.player`, plus every OP node in the table above | +The bundles are what actually grant these nodes. Each individual node is declared off in `plugin.yml`, and `sh2.player` or `sh2.admin` switches its whole set on, which is why denying a bundle takes that whole set away in one line. Granting or denying an individual node still works exactly as the table describes. + Note that `sh2.move-home` sits in `sh2.player`, not behind `sh2.manage-homes`. If you took `sh2.manage-homes` away to stop players relocating their homes, deny `sh2.move-home` as well or `/uhome` gives the ability back.
diff --git a/src/main/java/com/samleighton/sethomestwo/models/Home.java b/src/main/java/com/samleighton/sethomestwo/models/Home.java index af0626f..ff98035 100644 --- a/src/main/java/com/samleighton/sethomestwo/models/Home.java +++ b/src/main/java/com/samleighton/sethomestwo/models/Home.java @@ -194,6 +194,13 @@ public void setPlayerName(String playerName) { } public void teleport(Player player) { + // The single choke point for every teleport route: the go-home command, + // the admin command, and a click in the homes menu. + if (!player.hasPermission("sh2.teleport")) { + ChatUtils.invalidPermissions(player); + return; + } + // Home is blacklisted guard if(!this.getCanTeleport()) { ChatUtils.sendError(player, ConfigUtil.getConfig().getString("teleportToBlacklistedDimension", UserError.TELEPORT_IS_BLACKLISTED.getValue())); diff --git a/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java b/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java index a527927..6dc3f23 100644 --- a/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java +++ b/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java @@ -43,6 +43,11 @@ public static void apply() { continue; } + // Detach before the no-op check below. A node whose default already + // matches still needs freeing from its bundle, or the bundle keeps + // granting the very thing the admin just asked to take away. + if (parsed != PermissionDefault.TRUE) detachFromBundles(pluginManager, node); + PermissionDefault previous = permission.getDefault(); if (previous == parsed) continue; @@ -59,4 +64,24 @@ public static void apply() { } } } + + /** + * Drop a node from every bundle that lists it as a child, so the node's own + * default governs again. + * + * Bukkit writes the children of a default-granted parent straight into a + * player's effective permission map, and hasPermission reads that map before + * falling back to the node's default. Lowering the default alone therefore + * denies nothing while a bundle still grants the node. + */ + private static void detachFromBundles(PluginManager pluginManager, String node) { + for (Permission bundle : pluginManager.getPermissions()) { + if (bundle.getChildren().remove(node) == null) continue; + + pluginManager.recalculatePermissionDefaults(bundle); + Bukkit.getLogger().info(String.format( + "SetHomesTwo: removed %s from the %s bundle so the override applies.", + node, bundle.getName())); + } + } } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 5f166f5..5d737ba 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -73,67 +73,67 @@ commands: permissions: sh2.create-home: description: Allow player to create homes. - default: true + default: false sh2.go-home: description: Allow player to teleport to homes by command. - default: true + default: false sh2.list-homes: description: Allow player to list and view their homes. - default: true + default: false sh2.delete-home: description: Allow player to delete their own homes. - default: true + default: false sh2.teleport: description: Allow player to teleport to created homes. - default: true + default: false sh2.give-homes-item: description: Allow player to get the homes viewing item. - default: true + default: false sh2.manage-homes: description: Allow player to rename, move, re-icon, and delete their homes from the GUI. - default: true + default: false sh2.move-home: description: Allow a player to move their own homes by command. - default: true + default: false sh2.add-to-blacklist: description: Add dimensions to the blacklist. - default: op + default: false sh2.remove-from-blacklist: description: Remove dimensions from the blacklist. - default: op + default: false sh2.get-blacklisted-dimensions: description: List blacklisted dimensions. - default: op + default: false sh2.get-player-homes: description: View another player's homes. - default: op + default: false sh2.set-max-homes: description: Set the max number of homes. - default: op + default: false sh2.import-homes: description: Import homes from other plugins. - default: op + default: false sh2.update-notify: description: Be told on join when a newer SetHomesTwo release is available. - default: op + default: false sh2.go-player-home: description: Teleport to another player's home. - default: op + default: false sh2.delete-player-home: description: Delete another player's home. - default: op + default: false sh2.move-player-home: description: Move another player's home. - default: op + default: false sh2.bypass-max-homes: description: Create homes beyond the configured maximum. - default: op + default: false sh2.bypass-blacklist: description: Create, move and teleport to homes in blacklisted worlds. - default: op + default: false sh2.bypass-teleport-delay: description: Teleport instantly, without the countdown or the cancel on move. - default: op + default: false sh2.player: description: Everything an ordinary player needs. default: true diff --git a/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java index d0a5f05..2d0da03 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java @@ -168,4 +168,18 @@ void thePlayerArrivesOnceTheCountdownCompletes() { assertEquals(100, player.getLocation().getBlockZ()); assertNull(new TeleportAttemptsDao().get(player)); } + + @Test + void withoutTheTeleportNodeNoRouteToAHomeWorks() { + TestPlayer player = addPlayer(); + HomeFixtures.persist(HomeFixtures.home(player, "base", new Location(overworld, 60, 70, 60))); + player.addAttachment(plugin, "sh2.teleport", false); + player.teleport(new Location(overworld, 0, 70, 0)); + + server.execute("go-home", player, "base").assertSucceeded(); + server.getScheduler().performTicks(100L); + + assertTrue(player.nextMessage().contains("permission")); + assertEquals(0.0, player.getLocation().getX()); + } } diff --git a/src/test/java/com/samleighton/sethomestwo/utils/BypassNodesTest.java b/src/test/java/com/samleighton/sethomestwo/utils/BypassNodesTest.java index afc1f18..71a5d62 100644 --- a/src/test/java/com/samleighton/sethomestwo/utils/BypassNodesTest.java +++ b/src/test/java/com/samleighton/sethomestwo/utils/BypassNodesTest.java @@ -44,14 +44,14 @@ void aFixturePlayerHoldsNoneOfTheNodes() { } @Test - void theNodesDefaultToOpAndSitInTheAdminBundle() { + void theNodesAreGrantedOnlyByTheAdminBundle() { Permission adminBundle = server.getPluginManager().getPermission("sh2.admin"); assertNotNull(adminBundle); for (String node : NODES) { Permission permission = server.getPluginManager().getPermission(node); assertNotNull(permission, node); - assertEquals(PermissionDefault.OP, permission.getDefault(), node); + assertEquals(PermissionDefault.FALSE, permission.getDefault(), node); assertTrue(adminBundle.getChildren().containsKey(node), node); } } diff --git a/src/test/java/com/samleighton/sethomestwo/utils/PermissionBundlesTest.java b/src/test/java/com/samleighton/sethomestwo/utils/PermissionBundlesTest.java index a0a0259..0dc3bb7 100644 --- a/src/test/java/com/samleighton/sethomestwo/utils/PermissionBundlesTest.java +++ b/src/test/java/com/samleighton/sethomestwo/utils/PermissionBundlesTest.java @@ -4,9 +4,11 @@ import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionDefault; import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class PermissionBundlesTest extends ServerTestBase { @@ -51,4 +53,35 @@ void theBundleDefaultsMatchTheirMembers() { assertEquals(PermissionDefault.OP, server.getPluginManager().getPermission("sh2.admin").getDefault()); } + + @Test + void stockDefaultsGiveOrdinaryPlayersTheirNodesAndNothingElse() { + PlayerMock player = addPlayer(); + + for (String node : new String[]{"sh2.create-home", "sh2.go-home", "sh2.list-homes", + "sh2.delete-home", "sh2.teleport", "sh2.give-homes-item", "sh2.manage-homes", + "sh2.move-home"}) { + assertTrue(player.hasPermission(node), node); + } + + for (String node : new String[]{"sh2.import-homes", "sh2.get-player-homes", + "sh2.set-max-homes", "sh2.add-to-blacklist", "sh2.remove-from-blacklist", + "sh2.get-blacklisted-dimensions", "sh2.go-player-home", "sh2.delete-player-home", + "sh2.move-player-home", "sh2.bypass-max-homes", "sh2.bypass-blacklist", + "sh2.bypass-teleport-delay", "sh2.update-notify"}) { + assertFalse(player.hasPermission(node), node); + } + } + + @Test + void stockDefaultsGiveOperatorsEverything() { + PlayerMock op = addPlayer(); + op.setOp(true); + + for (String node : new String[]{"sh2.create-home", "sh2.manage-homes", "sh2.import-homes", + "sh2.get-player-homes", "sh2.delete-player-home", "sh2.bypass-blacklist", + "sh2.bypass-max-homes", "sh2.bypass-teleport-delay"}) { + assertTrue(op.hasPermission(node), node); + } + } } diff --git a/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java b/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java index 5d544c3..b8f6638 100644 --- a/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java +++ b/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java @@ -4,6 +4,7 @@ import org.bukkit.Bukkit; import org.bukkit.permissions.PermissionDefault; import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; import java.util.ArrayList; import java.util.List; @@ -13,6 +14,7 @@ import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class PermissionOverridesTest extends ServerTestBase { @@ -55,8 +57,9 @@ void anUnparseableValueLeavesTheDefaultAlone() { List logged = captureLog(PermissionOverrides::apply); - assertEquals(PermissionDefault.OP, + assertEquals(PermissionDefault.FALSE, server.getPluginManager().getPermission("sh2.import-homes").getDefault()); + assertTrue(opped().hasPermission("sh2.import-homes")); assertTrue(loggedWarning(logged, "SetHomesTwo: ignoring permission 'sh2.import-homes', value 'sometimes' is not one of true, false, op, not-op."), "Expected a warning naming the node and the rejected value"); @@ -68,18 +71,23 @@ void aWildcardIsNotHonoured() { PermissionOverrides.apply(); - assertEquals(PermissionDefault.OP, + assertEquals(PermissionDefault.FALSE, server.getPluginManager().getPermission("sh2.import-homes").getDefault()); + assertTrue(opped().hasPermission("sh2.import-homes")); } @Test void noPermissionsSectionLeavesStockDefaults() { PermissionOverrides.apply(); - assertEquals(PermissionDefault.OP, + assertEquals(PermissionDefault.FALSE, server.getPluginManager().getPermission("sh2.import-homes").getDefault()); - assertEquals(PermissionDefault.TRUE, + assertTrue(opped().hasPermission("sh2.import-homes")); + // The player nodes declare false and are granted by the sh2.player + // bundle, so denying the bundle takes all eight away at once. + assertEquals(PermissionDefault.FALSE, server.getPluginManager().getPermission("sh2.create-home").getDefault()); + assertTrue(addPlayer().hasPermission("sh2.create-home")); } @Test @@ -129,4 +137,92 @@ private boolean loggedWarning(List records, String message) { return records.stream().anyMatch( record -> record.getLevel() == Level.WARNING && message.equals(record.getMessage())); } + + @Test + void aDeniedNodeIsDeniedDespiteItsBundle() { + plugin.getConfig().set("permissions.sh2.manage-homes", false); + + PermissionOverrides.apply(); + + // sh2.player is default true and lists sh2.manage-homes as a child, so + // Bukkit writes the child straight into every player's effective map. + // Lowering the node's own default is not enough on its own. + assertFalse(addPlayer().hasPermission("sh2.manage-homes")); + } + + @Test + void aDeniedAdminNodeIsDeniedForOperators() { + plugin.getConfig().set("permissions.sh2.import-homes", false); + + PermissionOverrides.apply(); + + PlayerMock op = addPlayer(); + op.setOp(true); + + assertFalse(op.hasPermission("sh2.import-homes")); + } + + @Test + void aNodeRaisedToOpNoLongerReachesOrdinaryPlayers() { + plugin.getConfig().set("permissions.sh2.manage-homes", "op"); + + PermissionOverrides.apply(); + + PlayerMock player = addPlayer(); + PlayerMock op = addPlayer("Op"); + op.setOp(true); + + assertFalse(player.hasPermission("sh2.manage-homes")); + assertTrue(op.hasPermission("sh2.manage-homes")); + } + + @Test + void aGrantedNodeStillReachesPlayersThroughItsBundle() { + plugin.getConfig().set("permissions.sh2.get-player-homes", true); + + PermissionOverrides.apply(); + + assertTrue(addPlayer().hasPermission("sh2.get-player-homes")); + // Untouched nodes must keep working. + assertTrue(addPlayer("Other").hasPermission("sh2.create-home")); + } + + @Test + void denyingTheBundleTakesEveryPlayerNodeAtOnce() { + plugin.getConfig().set("permissions.sh2.player", false); + + PermissionOverrides.apply(); + + PlayerMock player = addPlayer(); + assertFalse(player.hasPermission("sh2.player")); + assertFalse(player.hasPermission("sh2.create-home")); + assertFalse(player.hasPermission("sh2.go-home")); + assertFalse(player.hasPermission("sh2.manage-homes")); + } + + @Test + void denyingTheAdminBundleLeavesOrdinaryPlayersAlone() { + plugin.getConfig().set("permissions.sh2.admin", false); + + PermissionOverrides.apply(); + + PlayerMock op = addPlayer(); + op.setOp(true); + + assertFalse(op.hasPermission("sh2.import-homes")); + assertTrue(addPlayer("Plain").hasPermission("sh2.create-home")); + } + + /** + * The admin nodes declare false and are granted by the sh2.admin bundle, + * which is op by default, so effective access is what tells you whether a + * node is still operator only. + */ + private PlayerMock opped() { + PlayerMock op = addPlayer("Op" + (opCount++)); + op.setOp(true); + return op; + } + + private int opCount = 0; } From 2414471b0352396dfb86ca02778ae70969b54bdc Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 12:59:15 -0400 Subject: [PATCH 29/75] docs: note the two things that only bite on upgrade Offline player lookup needs each player to have logged in once since the update, because the owner name is recorded on join and an older database has none. The sethome confirmation cannot name the icon until an existing config.yml picks up the new homeCreated wording. --- .changeset/eager-badgers-return.md | 2 +- README.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/eager-badgers-return.md b/.changeset/eager-badgers-return.md index 21f82a6..b30249b 100644 --- a/.changeset/eager-badgers-return.md +++ b/.changeset/eager-badgers-return.md @@ -2,4 +2,4 @@ bump: minor --- -Added the Set Homes v1 admin commands. /home-of teleports you to another player's home, /delhome-of deletes one, and /uhome-of moves one to where you are standing. /uhome moves one of your own homes. All of them work on players who are offline, as long as they have saved homes, and player and home names now match without regard to case. +Added the Set Homes v1 admin commands. /home-of teleports you to another player's home, /delhome-of deletes one, and /uhome-of moves one to where you are standing. /uhome moves one of your own homes. All of them work on players who are offline, as long as they have saved homes, and player and home names now match without regard to case. On a server upgrading from an earlier release, each player has to log in once before the offline lookup can find them. diff --git a/README.md b/README.md index c042385..e4ecc76 100644 --- a/README.md +++ b/README.md @@ -272,6 +272,8 @@ Your homes, your config and your permissions carry over untouched. These are the - **The world blacklist now works on every world.** Blacklisting always accepted any world name and reported success, but only the first three worlds were ever enforced, so a fourth was quietly ignored. It is enforced now. If you blacklisted a world beyond the first three, check `/blacklist list`, because homes there will start being refused and existing ones will stop being reachable. - **`/sethome base stone house` now means a stone icon and the description "house".** A second word naming a real item is taken as the icon. Put `d` in that position to force the default icon and keep the whole phrase: `/sethome base d stone house`. The reply names the icon it chose. - **A word like `water`, `fire`, `lava` or `wall_torch` stays description text.** Those are real materials but not items, so they cannot be a home icon. Rather than refusing the command, Set Homes Two treats them as the start of the description. +- **The admin commands cannot find an offline player until that player logs in once.** Set Homes Two learns which name belongs to which account when a player joins, and a database written by an earlier release has none of those names recorded yet. Until a player has reconnected once, `/home-of`, `/delhome-of` and `/uhome-of` will say no player by that name is online or has any saved homes, even though their homes are safe and still there. It corrects itself the first time they log in. +- **The confirmation for `/sethome` will not name the icon until you update `homeCreated`.** The icon is what tells you which word was taken as the icon rather than as description text, and it comes from a new second `%s` in that message. Your existing `config.yml` keeps the old wording, so copy `homeCreated` out of `default-config.yml` if you want it. - **Home names and player names now ignore case everywhere.** `/home Base` always found `base`; `/delhome`, `/uhome` and the admin commands now match it. `/delhome Base` therefore deletes `base`. - **`defaultHomeItem` now applies to homes players create.** It used to apply only to imported homes, so a server that set it to `chest` still got white wool on everything new. New homes with no icon now take the configured item. - **`/sethome` now checks the home name.** A blank name is refused and `maxHomeNameLength` is enforced. Both were previously checked only when renaming from the menu, which allowed a home nothing could address. From 5f158368d9dd792f1b779780f87ce7d33982704d Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 13:16:39 -0400 Subject: [PATCH 30/75] fix: honour sh2.bypass-blacklist in the two admin routes that ignored it An admin whose bypass node was denied could still reach another player's home in a blacklisted world, through /go-player-home and by clicking one in the /get-player-homes list, while being correctly refused on their own home in the same world. The node could be granted but not taken away. The two routes needed different fixes. The admin list passes the node to the dao like every other getAll caller. go-player-home reads through get, which applies no blacklist rule at all, so passing the flag there would have changed nothing and the check has to be explicit. An operator is unaffected: sh2.admin still grants the node. --- .../sethomestwo/commands/GetPlayerHomes.java | 2 +- .../sethomestwo/commands/GoPlayerHome.java | 11 +- .../commands/PlayerHomeAdminCommandsTest.java | 107 ++++++++++++++++++ 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java b/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java index 36c601d..ed3c383 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java @@ -61,7 +61,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command return true; } - Dao homesDao = new HomesDao(true); + Dao homesDao = new HomesDao(requester.hasPermission("sh2.bypass-blacklist")); List playersHomes = homesDao.getAll(UUID.fromString(uuidString)); Player target = Bukkit.getPlayer(UUID.fromString(uuidString)); diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java index f80a853..2aeebc6 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java @@ -7,6 +7,7 @@ import com.samleighton.sethomestwo.utils.ChatUtils; import com.samleighton.sethomestwo.utils.ConfigUtil; import com.samleighton.sethomestwo.utils.ServerUtil; +import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -45,8 +46,6 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command return true; } - // Unlike getAll, get applies no blacklist rule, so canTeleport stays at - // its default and an admin reaches the home whatever world it is in. Home home = new HomesDao(true).get(UUID.fromString(uuid), args[1]); if (home == null) { @@ -55,6 +54,14 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command return true; } + // get applies no blacklist rule of its own, unlike getAll, so the node + // has to be honoured here or it could not be taken away. Clearing the + // flag lets Home.teleport send the same refusal a player would see. + if (!admin.hasPermission("sh2.bypass-blacklist") + && ServerUtil.isWorldBlacklisted(Bukkit.getWorld(UUID.fromString(home.getWorld())))) { + home.setCanTeleport(false); + } + home.teleport(admin); return true; } diff --git a/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java b/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java index cb9d9f3..aa00c43 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java @@ -3,8 +3,15 @@ import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; +import com.samleighton.sethomestwo.support.TestPlayer; import com.samleighton.sethomestwo.tabcompleters.PlayerHomesTabCompleter; import org.bukkit.Location; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.ClickType; +import com.samleighton.sethomestwo.gui.HomesGui; +import com.samleighton.sethomestwo.gui.GuiSession; import org.junit.jupiter.api.Test; import org.mockbukkit.mockbukkit.entity.PlayerMock; @@ -245,4 +252,104 @@ private static void assertUnknownHomeNamed(PlayerMock admin, String typed) { assertTrue(message.contains(typed), message); assertFalse(message.contains("%s"), message); } + + @Test + void withoutTheBypassNodeAnAdminCannotReachAnotherPlayersBlacklistedHome() { + TestPlayer owner = addPlayer("Steve"); + HomeFixtures.persist(HomeFixtures.home(owner, "hideout", new Location(nether, 33, 70, 33))); + HomeFixtures.blacklist("world_nether"); + owner.disconnect(); + + TestPlayer admin = adminAt(new Location(overworld, 0, 70, 0)); + admin.addAttachment(plugin, "sh2.bypass-blacklist", false); + + server.execute("go-player-home", admin, "Steve", "hideout").assertSucceeded(); + server.getScheduler().performTicks(100L); + + assertEquals("world", admin.getLocation().getWorld().getName()); + assertTrue(admin.nextMessage().contains("blacklisted")); + } + + @Test + void withTheBypassNodeAnAdminReachesAnotherPlayersBlacklistedHome() { + TestPlayer owner = addPlayer("Steve"); + HomeFixtures.persist(HomeFixtures.home(owner, "hideout", new Location(nether, 33, 70, 33))); + HomeFixtures.blacklist("world_nether"); + owner.disconnect(); + + TestPlayer admin = adminAt(new Location(overworld, 0, 70, 0)); + admin.addAttachment(plugin, "sh2.bypass-blacklist", true); + + server.execute("go-player-home", admin, "Steve", "hideout").assertSucceeded(); + server.getScheduler().performTicks(100L); + + assertEquals("world_nether", admin.getLocation().getWorld().getName()); + assertEquals(33.0, admin.getLocation().getX()); + } + + @Test + void withoutTheBypassNodeClickingABlacklistedHomeInTheAdminViewDoesNotTeleport() { + TestPlayer owner = addPlayer("Steve"); + HomeFixtures.persist(HomeFixtures.home(owner, "hideout", new Location(nether, 33, 70, 33))); + HomeFixtures.blacklist("world_nether"); + + TestPlayer admin = adminAt(new Location(overworld, 0, 70, 0)); + admin.addAttachment(plugin, "sh2.get-player-homes", true); + admin.addAttachment(plugin, "sh2.bypass-blacklist", false); + + server.execute("get-player-homes", admin, "Steve").assertSucceeded(); + clickFirstHome(admin); + server.getScheduler().performTicks(100L); + + assertEquals("world", admin.getLocation().getWorld().getName()); + } + + @Test + void withTheBypassNodeClickingABlacklistedHomeInTheAdminViewTeleports() { + TestPlayer owner = addPlayer("Steve"); + HomeFixtures.persist(HomeFixtures.home(owner, "hideout", new Location(nether, 33, 70, 33))); + HomeFixtures.blacklist("world_nether"); + + TestPlayer admin = adminAt(new Location(overworld, 0, 70, 0)); + admin.addAttachment(plugin, "sh2.get-player-homes", true); + admin.addAttachment(plugin, "sh2.bypass-blacklist", true); + + server.execute("get-player-homes", admin, "Steve").assertSucceeded(); + clickFirstHome(admin); + server.getScheduler().performTicks(100L); + + assertEquals("world_nether", admin.getLocation().getWorld().getName()); + } + + /** + * Left-click the first home of whatever list the player currently has open, + * routed through the live GuiSession the command created. + */ + private void clickFirstHome(TestPlayer player) { + GuiSession session = plugin.getGuiSessionMap().get(player.getUniqueId()); + HomesGui gui = (HomesGui) session.getActiveScreen(); + + gui.onClick(new InventoryClickEvent( + player.getOpenInventory(), + InventoryType.SlotType.CONTAINER, + 0, + ClickType.LEFT, + InventoryAction.PICKUP_ALL + ), session); + } + + /** + * An admin holding the two nodes every go-player-home test needs, standing + * where the caller says, with the teleport machinery set to resolve inside + * the ticks these tests advance. + */ + private TestPlayer adminAt(Location where) { + TestPlayer admin = addPlayer("Admin"); + admin.addAttachment(plugin, "sh2.go-player-home", true); + admin.addAttachment(plugin, "sh2.teleport", true); + admin.teleport(where); + plugin.getConfig().set("delay", 0); + plugin.getConfig().set("teleportSafety", false); + return admin; + } } From 0bed378fa84a4dde945efe60bcd66b6ba7419d31 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 15:36:37 -0400 Subject: [PATCH 31/75] fix: complete import-homes arguments and name the typed alias in usage /import-homes had no tab completer, so Bukkit fell back to suggesting online player names on every argument, including past the two the command takes. It now offers the import sources, then confirm, then nothing. Usage messages named the canonical command, so typing /uhome-of was told 'Usage: /move-player-home', which is not what the player typed. Every usage string now takes the label the command was invoked with. The permission override log also names the resulting value, because a node whose default already matches logs only the bundle line. --- .../samleighton/sethomestwo/SetHomesTwo.java | 2 + .../sethomestwo/commands/Blacklist.java | 22 +++++----- .../sethomestwo/commands/DeleteHome.java | 2 +- .../commands/DeletePlayerHome.java | 2 +- .../sethomestwo/commands/GetPlayerHomes.java | 2 +- .../sethomestwo/commands/GoPlayerHome.java | 2 +- .../sethomestwo/commands/ImportHomes.java | 2 +- .../sethomestwo/commands/MoveHome.java | 2 +- .../sethomestwo/commands/MovePlayerHome.java | 2 +- .../sethomestwo/enums/UserError.java | 2 +- .../sethomestwo/enums/UserInfo.java | 12 +++--- .../ImportSourcesTabCompleter.java | 36 ++++++++++++++++ .../utils/PermissionOverrides.java | 8 ++-- .../sethomestwo/commands/MoveHomeTest.java | 10 +++++ .../ImportSourcesTabCompleterTest.java | 42 +++++++++++++++++++ 15 files changed, 119 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/samleighton/sethomestwo/tabcompleters/ImportSourcesTabCompleter.java create mode 100644 src/test/java/com/samleighton/sethomestwo/tabcompleters/ImportSourcesTabCompleterTest.java diff --git a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java index 1cda590..1e09203 100644 --- a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java +++ b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java @@ -15,6 +15,7 @@ import com.samleighton.sethomestwo.updates.UpdateChecker; import com.samleighton.sethomestwo.tabcompleters.BlacklistTabCompleter; import com.samleighton.sethomestwo.tabcompleters.HomesTabCompleter; +import com.samleighton.sethomestwo.tabcompleters.ImportSourcesTabCompleter; import com.samleighton.sethomestwo.tabcompleters.MaterialsTabCompleter; import com.samleighton.sethomestwo.tabcompleters.PlayerHomesTabCompleter; import com.samleighton.sethomestwo.utils.ConfigUtil; @@ -189,6 +190,7 @@ public void registerCommands() { PluginCommand importHomes = Objects.requireNonNull(this.getCommand("import-homes")); importHomes.setExecutor(new ImportHomes()); + importHomes.setTabCompleter(new ImportSourcesTabCompleter()); PluginCommand moveHome = Objects.requireNonNull(this.getCommand("move-home")); moveHome.setExecutor(new MoveHome()); diff --git a/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java b/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java index bcaff19..8361f37 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java @@ -70,7 +70,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command default: if (args.length < 1) { ChatUtils.incorrectNumArguments(player); - ChatUtils.sendInfo(player, UserInfo.BLACKLIST_USAGE.getValue()); + ChatUtils.sendInfo(player, String.format(UserInfo.BLACKLIST_USAGE.getValue(), label)); return true; } subcommand = args[0].toLowerCase(); @@ -80,13 +80,13 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command switch (subcommand) { case "add": - return add(player, names); + return add(player, label, names); case "remove": - return remove(player, names); + return remove(player, label, names); case "list": - return list(player, names); + return list(player, label, names); default: - ChatUtils.sendInfo(player, UserInfo.BLACKLIST_USAGE.getValue()); + ChatUtils.sendInfo(player, String.format(UserInfo.BLACKLIST_USAGE.getValue(), label)); return true; } } @@ -100,7 +100,7 @@ private boolean isExplicitSubcommand(String[] args) { return args.length >= 1 && SUBCOMMANDS.contains(args[0].toLowerCase()); } - private boolean add(Player player, String[] dimensions) { + private boolean add(Player player, String label, String[] dimensions) { if (!player.hasPermission("sh2.add-to-blacklist")) { ChatUtils.invalidPermissions(player); return true; @@ -108,7 +108,7 @@ private boolean add(Player player, String[] dimensions) { if (dimensions.length < 1) { ChatUtils.incorrectNumArguments(player); - ChatUtils.sendInfo(player, UserInfo.BLACKLIST_USAGE.getValue()); + ChatUtils.sendInfo(player, String.format(UserInfo.BLACKLIST_USAGE.getValue(), label)); return true; } @@ -141,7 +141,7 @@ private boolean add(Player player, String[] dimensions) { return true; } - private boolean remove(Player player, String[] dimensions) { + private boolean remove(Player player, String label, String[] dimensions) { if (!player.hasPermission("sh2.remove-from-blacklist")) { ChatUtils.invalidPermissions(player); return true; @@ -149,7 +149,7 @@ private boolean remove(Player player, String[] dimensions) { if (dimensions.length < 1) { ChatUtils.incorrectNumArguments(player); - ChatUtils.sendInfo(player, UserInfo.BLACKLIST_USAGE.getValue()); + ChatUtils.sendInfo(player, String.format(UserInfo.BLACKLIST_USAGE.getValue(), label)); return true; } @@ -184,7 +184,7 @@ private boolean remove(Player player, String[] dimensions) { return true; } - private boolean list(Player player, String[] extraArgs) { + private boolean list(Player player, String label, String[] extraArgs) { if (!player.hasPermission("sh2.get-blacklisted-dimensions")) { ChatUtils.invalidPermissions(player); return true; @@ -192,7 +192,7 @@ private boolean list(Player player, String[] extraArgs) { if (extraArgs.length > 0) { ChatUtils.incorrectNumArguments(player); - ChatUtils.sendInfo(player, UserInfo.BLACKLIST_USAGE.getValue()); + ChatUtils.sendInfo(player, String.format(UserInfo.BLACKLIST_USAGE.getValue(), label)); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/DeleteHome.java b/src/main/java/com/samleighton/sethomestwo/commands/DeleteHome.java index ce24f71..4bd394f 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/DeleteHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/DeleteHome.java @@ -33,7 +33,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command // Args length guard if (args.length != 1) { ChatUtils.incorrectNumArguments(player); - ChatUtils.sendInfo(player, UserError.DELETE_HOME_USAGE.getValue()); + ChatUtils.sendInfo(player, String.format(UserError.DELETE_HOME_USAGE.getValue(), s)); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java index 8128a20..fb51a89 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/DeletePlayerHome.java @@ -34,7 +34,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command if (args.length != 2) { ChatUtils.incorrectNumArguments(admin); - ChatUtils.sendInfo(admin, UserInfo.DELETE_PLAYER_HOME_USAGE.getValue()); + ChatUtils.sendInfo(admin, String.format(UserInfo.DELETE_PLAYER_HOME_USAGE.getValue(), label)); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java b/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java index ed3c383..9449af2 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GetPlayerHomes.java @@ -48,7 +48,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command // Args length guard if (args.length != 1) { ChatUtils.incorrectNumArguments(requester); - ChatUtils.sendError(requester, UserInfo.GET_PLAYER_HOMES_USAGE.getValue()); + ChatUtils.sendError(requester, String.format(UserInfo.GET_PLAYER_HOMES_USAGE.getValue(), s)); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java index 2aeebc6..7af2ede 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java @@ -34,7 +34,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command if (args.length != 2) { ChatUtils.incorrectNumArguments(admin); - ChatUtils.sendInfo(admin, UserInfo.GO_PLAYER_HOME_USAGE.getValue()); + ChatUtils.sendInfo(admin, String.format(UserInfo.GO_PLAYER_HOME_USAGE.getValue(), label)); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java b/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java index 672d530..dd993cb 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java @@ -28,7 +28,7 @@ public class ImportHomes implements CommandExecutor { public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, String[] args) { // Console and players may both run this; permission handled by plugin.yml (default op) if (args.length < 1 || !SOURCES.containsKey(args[0].toLowerCase())) { - commandSender.sendMessage(String.format("Usage: /import-homes <%s> [confirm]", String.join("|", SOURCES.keySet()))); + commandSender.sendMessage(String.format("Usage: /%s <%s> [confirm]", s, String.join("|", SOURCES.keySet()))); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/MoveHome.java b/src/main/java/com/samleighton/sethomestwo/commands/MoveHome.java index 58221de..a6751d8 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/MoveHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/MoveHome.java @@ -32,7 +32,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command if (args.length != 1) { ChatUtils.incorrectNumArguments(player); - ChatUtils.sendInfo(player, UserInfo.MOVE_HOME_USAGE.getValue()); + ChatUtils.sendInfo(player, String.format(UserInfo.MOVE_HOME_USAGE.getValue(), label)); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java index 89cf3f7..c3e0c32 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/MovePlayerHome.java @@ -35,7 +35,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command if (args.length != 2) { ChatUtils.incorrectNumArguments(admin); - ChatUtils.sendInfo(admin, UserInfo.MOVE_PLAYER_HOME_USAGE.getValue()); + ChatUtils.sendInfo(admin, String.format(UserInfo.MOVE_PLAYER_HOME_USAGE.getValue(), label)); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserError.java b/src/main/java/com/samleighton/sethomestwo/enums/UserError.java index 69f5d02..9a95d61 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserError.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserError.java @@ -19,7 +19,7 @@ public enum UserError { /** Command Input Errors */ DIMENSION_IS_NOT_BLACKLISTED("The %s dimension has not been blacklisted yet therefore you cannot remove it."), INVALID_WORLD("%s is not a valid world. This server's worlds are: %s"), - DELETE_HOME_USAGE("Usage: /delete-home [name]"), + DELETE_HOME_USAGE("Usage: /%s [name]"), INVALID_MATERIAL("The material you entered is not valid, please try a different one."), PLAYER_NOT_FOUND("No player by that name is online or has any saved homes."), NO_HOMES("You have not created any homes yet. Use /create-home."), diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java b/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java index d1f679b..3822712 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java @@ -1,17 +1,17 @@ package com.samleighton.sethomestwo.enums; public enum UserInfo { - GET_PLAYER_HOMES_USAGE("Usage: /get-player-homes [playerName]"), - BLACKLIST_USAGE("Usage: /blacklist [world]"), + GET_PLAYER_HOMES_USAGE("Usage: /%s [playerName]"), + BLACKLIST_USAGE("Usage: /%s [world]"), CREATE_HOME_USAGE("Usage: /create-home [name] [icon material, or d for the default icon] [description]. Omit the name and the home is called 'default'."), NO_HOMES("You have not setup any homes yet, you can use the /create-home command to create one."), NO_MAX_HOMES("There is no max number of homes."), NO_BLACKLISTED_DIMENSIONS("No dimensions are blacklisted"), MOVED_TO_SAFE_SPOT("Your home was not safe to stand in, so you were moved to the nearest safe spot."), - MOVE_HOME_USAGE("Usage: /move-home "), - GO_PLAYER_HOME_USAGE("Usage: /go-player-home "), - DELETE_PLAYER_HOME_USAGE("Usage: /delete-player-home "), - MOVE_PLAYER_HOME_USAGE("Usage: /move-player-home "); + MOVE_HOME_USAGE("Usage: /%s "), + GO_PLAYER_HOME_USAGE("Usage: /%s "), + DELETE_PLAYER_HOME_USAGE("Usage: /%s "), + MOVE_PLAYER_HOME_USAGE("Usage: /%s "); private final String value; diff --git a/src/main/java/com/samleighton/sethomestwo/tabcompleters/ImportSourcesTabCompleter.java b/src/main/java/com/samleighton/sethomestwo/tabcompleters/ImportSourcesTabCompleter.java new file mode 100644 index 0000000..6951bab --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/tabcompleters/ImportSourcesTabCompleter.java @@ -0,0 +1,36 @@ +package com.samleighton.sethomestwo.tabcompleters; + +import com.samleighton.sethomestwo.commands.ImportHomes; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.util.StringUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Completions for /import-homes. Returns an empty list rather than null past the + * arguments it knows, because Bukkit falls back to suggesting online player + * names whenever a completer returns null, which is meaningless here. + */ +public class ImportSourcesTabCompleter implements TabCompleter { + + @Nullable + @Override + public List onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] args) { + List completions = new ArrayList<>(); + + if (args.length == 1) { + StringUtil.copyPartialMatches(args[0], ImportHomes.SOURCES.keySet(), completions); + } + + if (args.length == 2) { + StringUtil.copyPartialMatches(args[1], List.of("confirm"), completions); + } + + return completions; + } +} diff --git a/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java b/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java index 6dc3f23..d9b56f6 100644 --- a/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java +++ b/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java @@ -46,7 +46,7 @@ public static void apply() { // Detach before the no-op check below. A node whose default already // matches still needs freeing from its bundle, or the bundle keeps // granting the very thing the admin just asked to take away. - if (parsed != PermissionDefault.TRUE) detachFromBundles(pluginManager, node); + if (parsed != PermissionDefault.TRUE) detachFromBundles(pluginManager, node, parsed); PermissionDefault previous = permission.getDefault(); if (previous == parsed) continue; @@ -74,14 +74,14 @@ public static void apply() { * falling back to the node's default. Lowering the default alone therefore * denies nothing while a bundle still grants the node. */ - private static void detachFromBundles(PluginManager pluginManager, String node) { + private static void detachFromBundles(PluginManager pluginManager, String node, PermissionDefault applied) { for (Permission bundle : pluginManager.getPermissions()) { if (bundle.getChildren().remove(node) == null) continue; pluginManager.recalculatePermissionDefaults(bundle); Bukkit.getLogger().info(String.format( - "SetHomesTwo: removed %s from the %s bundle so the override applies.", - node, bundle.getName())); + "SetHomesTwo: %s is now %s, and was removed from the %s bundle so that applies.", + node, applied, bundle.getName())); } } } diff --git a/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java index 530005e..7d5d4fa 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java @@ -97,4 +97,14 @@ void theWrongNumberOfArgumentsShowsTheUsage() { assertTrue(player.nextMessage().contains("Incorrect number of arguments")); assertTrue(player.nextMessage().contains("Usage: /move-home ")); } + + @Test + void theUsageNamesTheAliasThatWasTyped() { + PlayerMock player = addPlayer(); + + server.dispatchCommand(player, "uhome"); + + player.nextMessage(); + assertTrue(player.nextMessage().contains("Usage: /uhome ")); + } } diff --git a/src/test/java/com/samleighton/sethomestwo/tabcompleters/ImportSourcesTabCompleterTest.java b/src/test/java/com/samleighton/sethomestwo/tabcompleters/ImportSourcesTabCompleterTest.java new file mode 100644 index 0000000..98a5031 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/tabcompleters/ImportSourcesTabCompleterTest.java @@ -0,0 +1,42 @@ +package com.samleighton.sethomestwo.tabcompleters; + +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.command.PluginCommand; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +import java.util.List; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ImportSourcesTabCompleterTest extends ServerTestBase { + + private List complete(String... args) { + PlayerMock player = addPlayer(); + PluginCommand command = Objects.requireNonNull(plugin.getCommand("import-homes")); + + return new ImportSourcesTabCompleter().onTabComplete(player, command, "import-homes", args); + } + + @Test + void theFirstArgumentOffersTheImportSources() { + List completions = complete(""); + + assertTrue(completions.contains("sethomes"), completions.toString()); + assertTrue(completions.contains("essentialsx"), completions.toString()); + } + + @Test + void theSecondArgumentOffersConfirm() { + assertEquals(List.of("confirm"), complete("sethomes", "")); + } + + @Test + void beyondTheKnownArgumentsNothingIsOffered() { + // Empty, not null. Bukkit falls back to suggesting online player names + // when a completer returns null, which is meaningless for this command. + assertEquals(List.of(), complete("sethomes", "confirm", "")); + } +} From 8e5c43a3b768042664fc2e600835e4742b98b125 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 15:38:07 -0400 Subject: [PATCH 32/75] docs: changeset for the tab completion and usage message fixes --- .changeset/brave-otters-listen.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/brave-otters-listen.md diff --git a/.changeset/brave-otters-listen.md b/.changeset/brave-otters-listen.md new file mode 100644 index 0000000..aa259f4 --- /dev/null +++ b/.changeset/brave-otters-listen.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +Tab completion for /import-homes now offers the import sources and then confirm, instead of suggesting player names on every argument. Usage messages also name the command you actually typed, so /uhome-of now says Usage: /uhome-of rather than naming the full command behind the alias. From bd1c728ef8a274abc5fb5b9e37d291cbc40f4cfc Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 17:33:39 -0400 Subject: [PATCH 33/75] feat: import the Set Homes v1 world blacklist alongside homes --- .changeset/wobbly-hedgehogs-import.md | 5 + README.md | 1 + .../sethomestwo/commands/ImportHomes.java | 5 +- .../sethomestwo/importers/ImportReport.java | 14 ++ .../importers/SetHomesV1Importer.java | 45 ++++++ .../sethomestwo/commands/ImportHomesTest.java | 67 +++++++++ .../importers/SetHomesV1ImporterTest.java | 132 ++++++++++++++++++ 7 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 .changeset/wobbly-hedgehogs-import.md create mode 100644 src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java create mode 100644 src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java diff --git a/.changeset/wobbly-hedgehogs-import.md b/.changeset/wobbly-hedgehogs-import.md new file mode 100644 index 0000000..afad234 --- /dev/null +++ b/.changeset/wobbly-hedgehogs-import.md @@ -0,0 +1,5 @@ +--- +bump: minor +--- + +/import-homes sethomes now brings across the Set Homes v1 world blacklist along with the homes, so a server that had worlds blocked in v1 does not silently lose that protection on migration. Re-running the import never blacklists a world twice. diff --git a/README.md b/README.md index e4ecc76..1144ab3 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Your players keep their homes. The old plugin does not even need to be running, - Run `/import-homes essentialsx` (or `/import-homes sethomes`). This is a **preview only**. It reports how many homes it would import and warns about any it would skip, and changes nothing. - Happy with the numbers? Run it again with `confirm` on the end. - Existing homes are never overwritten, so re-running it is always safe. Homes in worlds that no longer exist are skipped with a warning naming the world. +- `/import-homes sethomes` also brings across the v1 world blacklist, added to your Set Homes Two blacklist alongside the homes. Re-running never adds a world twice. Afterwards, remove the old jar. Set Homes Two provides `/sethome`, `/home` and `/delhome`, and two plugins claiming the same commands will fight over them. diff --git a/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java b/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java index dd993cb..74fea1f 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java @@ -37,10 +37,13 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command ImportReport report = importer.run(dryRun); commandSender.sendMessage(report.summary(dryRun)); + if (report.hasBlacklistActivity()) { + commandSender.sendMessage(report.blacklistSummary(dryRun)); + } for (String warning : report.warnings) { commandSender.sendMessage("Warning: " + warning); } - if (dryRun && report.imported > 0) { + if (dryRun && (report.imported > 0 || report.hasBlacklistActivity())) { commandSender.sendMessage(String.format("Dry run only. Run '/import-homes %s confirm' to apply.", importer.sourceName())); } return true; diff --git a/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java b/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java index 0aa9213..fa5427c 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java @@ -8,6 +8,8 @@ public class ImportReport { public int skippedExisting = 0; public int skippedWorldMissing = 0; public int failed = 0; + public int blacklistImported = 0; + public int blacklistSkippedExisting = 0; public final List warnings = new ArrayList<>(); public String summary(boolean dryRun) { @@ -17,4 +19,16 @@ public String summary(boolean dryRun) { verb, imported, skippedExisting, skippedWorldMissing, failed ); } + + public boolean hasBlacklistActivity() { + return blacklistImported > 0 || blacklistSkippedExisting > 0; + } + + public String blacklistSummary(boolean dryRun) { + String verb = dryRun ? "Would add" : "Added"; + return String.format( + "%s %d world(s) to the blacklist (%d already blacklisted).", + verb, blacklistImported, blacklistSkippedExisting + ); + } } diff --git a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java index 691f332..d89d966 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java @@ -1,6 +1,7 @@ package com.samleighton.sethomestwo.importers; import com.samleighton.sethomestwo.SetHomesTwo; +import com.samleighton.sethomestwo.dao.BlacklistDao; import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.models.Home; import org.bukkit.Bukkit; @@ -10,6 +11,7 @@ import org.bukkit.configuration.file.YamlConfiguration; import java.io.File; +import java.util.List; import java.util.UUID; public class SetHomesV1Importer implements HomesImporter { @@ -53,6 +55,8 @@ public ImportReport run(boolean dryRun) { } } + importBlacklist(pluginsDir, report, dryRun); + return report; } @@ -105,4 +109,45 @@ private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSect report.warnings.add(String.format("Home '%s' for player %s could not be read: %s", homeName, playerUUID, e.getMessage())); } } + + /** + * v1's world_blacklist.yml holds a flat blacklisted_worlds list. Missing or + * empty is normal (v1 shipped it empty by default), not an error. A world + * absent from this server is still stored - it is harmless to block a world + * that does not exist - but is called out with a warning in case the name + * was a typo. + */ + private void importBlacklist(File pluginsDir, ImportReport report, boolean dryRun) { + File blacklistFile = new File(pluginsDir, "SetHomes/world_blacklist.yml"); + if (!blacklistFile.exists()) return; + + YamlConfiguration source = YamlConfiguration.loadConfiguration(blacklistFile); + List worlds = source.getStringList("blacklisted_worlds"); + if (worlds.isEmpty()) return; + + BlacklistDao blacklistDao = new BlacklistDao(); + List existing = blacklistDao.getAll(); + + for (String world : worlds) { + String lowered = world.toLowerCase(); + + if (existing.contains(lowered)) { + report.blacklistSkippedExisting++; + continue; + } + + if (Bukkit.getWorld(lowered) == null) { + report.warnings.add(String.format("Blacklisted world '%s' does not exist on this server.", lowered)); + } + + if (!dryRun) { + blacklistDao.save(lowered); + } + + // Tracked locally too, so a world repeated in the source file is + // only ever counted (and written) once per run. + existing.add(lowered); + report.blacklistImported++; + } + } } diff --git a/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java b/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java new file mode 100644 index 0000000..b37d872 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java @@ -0,0 +1,67 @@ +package com.samleighton.sethomestwo.commands; + +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ImportHomesTest extends ServerTestBase { + + private File setHomesDir() { + File dir = new File(plugin.getDataFolder().getParentFile(), "SetHomes"); + dir.mkdirs(); + return dir; + } + + private void writeEmptyHomesFile() throws IOException { + YamlConfiguration yaml = new YamlConfiguration(); + yaml.save(new File(setHomesDir(), "homes.yml")); + } + + private void writeBlacklist(String... worlds) throws IOException { + YamlConfiguration yaml = new YamlConfiguration(); + yaml.set("blacklisted_worlds", List.of(worlds)); + yaml.save(new File(setHomesDir(), "world_blacklist.yml")); + } + + private PlayerMock authorizedPlayer() { + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.import-homes", true); + return player; + } + + @Test + void blacklistActivityAddsASecondReplyLine() throws IOException { + writeEmptyHomesFile(); + writeBlacklist("world_nether"); + PlayerMock player = authorizedPlayer(); + + server.execute("import-homes", player, "sethomes").assertSucceeded(); + + player.nextMessage(); // homes summary line + assertTrue(player.nextMessage().contains("blacklist")); + } + + @Test + void noBlacklistActivityMeansNoSecondLine() throws IOException { + writeEmptyHomesFile(); + PlayerMock player = authorizedPlayer(); + + server.execute("import-homes", player, "sethomes").assertSucceeded(); + + // 0 homes and 0 blacklist activity: only the summary line is sent. + // The dry-run hint is gated on (imported > 0 || hasBlacklistActivity()), + // so it does not fire here either. + String summary = player.nextMessage(); + assertFalse(summary.contains("blacklist")); + assertNull(player.nextMessage()); + } +} diff --git a/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java new file mode 100644 index 0000000..2a5aa2a --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java @@ -0,0 +1,132 @@ +package com.samleighton.sethomestwo.importers; + +import com.samleighton.sethomestwo.dao.BlacklistDao; +import com.samleighton.sethomestwo.support.HomeFixtures; +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SetHomesV1ImporterTest extends ServerTestBase { + + private final SetHomesV1Importer importer = new SetHomesV1Importer(); + + private File setHomesDir() { + File dir = new File(plugin.getDataFolder().getParentFile(), "SetHomes"); + dir.mkdirs(); + return dir; + } + + /** + * SetHomesV1Importer.run() returns early with only a warning when + * plugins/SetHomes/homes.yml is missing (see the existing code at + * src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java:28-31). + * Every test that wants the blacklist path to actually run needs this + * called first, even though these tests have no homes of their own. + */ + private void writeEmptyHomesFile() throws IOException { + YamlConfiguration yaml = new YamlConfiguration(); + yaml.save(new File(setHomesDir(), "homes.yml")); + } + + private void writeBlacklist(String... worlds) throws IOException { + YamlConfiguration yaml = new YamlConfiguration(); + yaml.set("blacklisted_worlds", List.of(worlds)); + yaml.save(new File(setHomesDir(), "world_blacklist.yml")); + } + + @Test + void dryRunReportsWorldsItWouldBlacklistAndWritesNothing() throws IOException { + writeEmptyHomesFile(); + writeBlacklist("world_nether"); + + ImportReport report = importer.run(true); + + assertEquals(1, report.blacklistImported); + assertEquals(0, report.blacklistSkippedExisting); + assertTrue(new BlacklistDao().getAll().isEmpty()); + } + + @Test + void confirmAddsEachBlacklistedWorldLowercased() throws IOException { + writeEmptyHomesFile(); + writeBlacklist("World_Nether"); + + ImportReport report = importer.run(false); + + assertEquals(1, report.blacklistImported); + assertTrue(new BlacklistDao().getAll().contains("world_nether")); + } + + @Test + void reRunningDoesNotDuplicateAnAlreadyBlacklistedWorld() throws IOException { + writeEmptyHomesFile(); + HomeFixtures.blacklist("world_nether"); + writeBlacklist("world_nether"); + + ImportReport report = importer.run(false); + + assertEquals(0, report.blacklistImported); + assertEquals(1, report.blacklistSkippedExisting); + assertEquals(1, new BlacklistDao().getAll().size()); + } + + @Test + void duplicatesWithinTheSourceFileAreOnlyAddedOnce() throws IOException { + writeEmptyHomesFile(); + writeBlacklist("world_nether", "world_nether"); + + ImportReport report = importer.run(false); + + assertEquals(1, report.blacklistImported); + assertEquals(1, report.blacklistSkippedExisting); + assertEquals(1, new BlacklistDao().getAll().size()); + } + + @Test + void aBlacklistedWorldThatDoesNotExistIsStoredWithAWarning() throws IOException { + writeEmptyHomesFile(); + writeBlacklist("world_the_void"); + + ImportReport report = importer.run(false); + + assertEquals(1, report.blacklistImported); + assertTrue(new BlacklistDao().getAll().contains("world_the_void")); + assertTrue(report.warnings.stream().anyMatch(w -> w.contains("world_the_void"))); + } + + @Test + void missingWorldBlacklistFileIsNotAnError() throws IOException { + // homes.yml present, world_blacklist.yml genuinely absent - the case + // v1 servers hit constantly, since it shipped empty by default. + writeEmptyHomesFile(); + + ImportReport report = importer.run(true); + + assertEquals(0, report.blacklistImported); + assertEquals(0, report.blacklistSkippedExisting); + assertTrue(report.warnings.isEmpty()); + } + + @Test + void emptyBlacklistedWorldsListIsNotAnError() throws IOException { + writeEmptyHomesFile(); + YamlConfiguration yaml = new YamlConfiguration(); + yaml.set("blacklisted_worlds", List.of()); + yaml.save(new File(setHomesDir(), "world_blacklist.yml")); + + ImportReport report = importer.run(true); + + assertEquals(0, report.blacklistImported); + assertEquals(0, report.blacklistSkippedExisting); + assertTrue(report.warnings.isEmpty()); + } +} From 998b59021dff0a214781aa057800c13eeafa1fd4 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 17:41:21 -0400 Subject: [PATCH 34/75] fix: resolve imported players' names from the server's player cache --- .changeset/quiet-otters-names.md | 5 ++ README.md | 1 + .../importers/EssentialsXImporter.java | 17 +++-- .../sethomestwo/importers/HomesImporter.java | 22 +++++++ .../sethomestwo/importers/ImportReport.java | 7 ++- .../importers/SetHomesV1Importer.java | 10 ++- .../importers/EssentialsXImporterTest.java | 62 +++++++++++++++++++ .../importers/SetHomesV1ImporterTest.java | 49 +++++++++++++++ 8 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 .changeset/quiet-otters-names.md create mode 100644 src/test/java/com/samleighton/sethomestwo/importers/EssentialsXImporterTest.java diff --git a/.changeset/quiet-otters-names.md b/.changeset/quiet-otters-names.md new file mode 100644 index 0000000..173d9b6 --- /dev/null +++ b/.changeset/quiet-otters-names.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +Importing from Set Homes v1 or EssentialsX now resolves each player's name from the server's own player cache, so admin commands like /get-player-homes and /home-of work on a migrated player immediately instead of requiring them to log in first. A player the server has never seen still imports fine; they are picked up on their next join, same as before. diff --git a/README.md b/README.md index 1144ab3..8b5b730 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Your players keep their homes. The old plugin does not even need to be running, - Happy with the numbers? Run it again with `confirm` on the end. - Existing homes are never overwritten, so re-running it is always safe. Homes in worlds that no longer exist are skipped with a warning naming the world. - `/import-homes sethomes` also brings across the v1 world blacklist, added to your Set Homes Two blacklist alongside the homes. Re-running never adds a world twice. +- Admin commands (`/get-player-homes`, `/home-of`, `/delhome-of`, `/uhome-of`) work on an imported player immediately, for any player this server has seen before - the importer resolves their name from the server's own player cache, no network lookup involved. A player the server has never seen imports with no name and is picked up automatically on their first join, same as any other offline lookup. Afterwards, remove the old jar. Set Homes Two provides `/sethome`, `/home` and `/delhome`, and two plugins claiming the same commands will fight over them. diff --git a/src/main/java/com/samleighton/sethomestwo/importers/EssentialsXImporter.java b/src/main/java/com/samleighton/sethomestwo/importers/EssentialsXImporter.java index 121f03c..7b7fcab 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/EssentialsXImporter.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/EssentialsXImporter.java @@ -45,15 +45,19 @@ public ImportReport run(boolean dryRun) { ConfigurationSection homes = userData.getConfigurationSection("homes"); if (homes == null) continue; + // EssentialsX user files name by UUID and carry the last known + // account name directly, unlike v1's homes.yml, which has none. + String playerName = userData.getString("lastAccountName"); + for (String homeName : homes.getKeys(false)) { - importOne(homesDao, report, homes.getConfigurationSection(homeName), playerUUID, homeName, dryRun); + importOne(homesDao, report, homes.getConfigurationSection(homeName), playerUUID, playerName, homeName, dryRun); } } return report; } - private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSection home, String playerUUID, String homeName, boolean dryRun) { + private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSection home, String playerUUID, String playerName, String homeName, boolean dryRun) { try { if (home == null) { report.failed++; @@ -81,15 +85,20 @@ private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSect (float) home.getDouble("pitch") ); + if (playerName != null) report.namesResolved++; + if (!dryRun) { - boolean saved = homesDao.save(new Home( + Home importedHome = new Home( playerUUID, HomesImporter.defaultMaterial(), location, homeName, null, world.getEnvironment().toString() - )); + ); + importedHome.setPlayerName(playerName); + + boolean saved = homesDao.save(importedHome); if (!saved) { report.failed++; return; diff --git a/src/main/java/com/samleighton/sethomestwo/importers/HomesImporter.java b/src/main/java/com/samleighton/sethomestwo/importers/HomesImporter.java index bbd92e5..7af54ef 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/HomesImporter.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/HomesImporter.java @@ -1,7 +1,11 @@ package com.samleighton.sethomestwo.importers; import com.samleighton.sethomestwo.utils.ConfigUtil; +import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.OfflinePlayer; + +import java.util.UUID; public interface HomesImporter { @@ -15,4 +19,22 @@ static String defaultMaterial() { Material material = Material.matchMaterial(ConfigUtil.getConfig().getString("defaultHomeItem", "white_wool")); return material == null ? Material.WHITE_WOOL.name() : material.name(); } + + /** + * The name this server has cached for a player, or null if it has never + * seen them. Deliberately scans {@link Bukkit#getOfflinePlayers()} rather + * than calling {@code Bukkit.getOfflinePlayer(UUID)} directly: the latter + * always returns a non-null object by contract (real Bukkit backs it with + * usercache.json and reports a null name on a genuine miss, but that + * distinction should not be relied on for null-safety), and never makes a + * network call either way, so an import can never block on Mojang. + */ + static String resolveCachedName(UUID uuid) { + for (OfflinePlayer candidate : Bukkit.getOfflinePlayers()) { + if (candidate.getUniqueId().equals(uuid)) { + return candidate.getName(); + } + } + return null; + } } diff --git a/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java b/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java index fa5427c..75c8a5e 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java @@ -10,14 +10,19 @@ public class ImportReport { public int failed = 0; public int blacklistImported = 0; public int blacklistSkippedExisting = 0; + public int namesResolved = 0; public final List warnings = new ArrayList<>(); public String summary(boolean dryRun) { String verb = dryRun ? "Would import" : "Imported"; - return String.format( + String base = String.format( "%s %d homes (%d skipped: name exists, %d skipped: world missing, %d failed).", verb, imported, skippedExisting, skippedWorldMissing, failed ); + if (namesResolved > 0) { + base += String.format(" %d player name(s) resolved from the server's cache.", namesResolved); + } + return base; } public boolean hasBlacklistActivity() { diff --git a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java index d89d966..0961b8c 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java @@ -88,15 +88,21 @@ private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSect (float) home.getDouble("pitch") ); + String playerName = HomesImporter.resolveCachedName(UUID.fromString(playerUUID)); + if (playerName != null) report.namesResolved++; + if (!dryRun) { - boolean saved = homesDao.save(new Home( + Home importedHome = new Home( playerUUID, HomesImporter.defaultMaterial(), location, homeName, home.getString("desc"), world.getEnvironment().toString() - )); + ); + importedHome.setPlayerName(playerName); + + boolean saved = homesDao.save(importedHome); if (!saved) { report.failed++; return; diff --git a/src/test/java/com/samleighton/sethomestwo/importers/EssentialsXImporterTest.java b/src/test/java/com/samleighton/sethomestwo/importers/EssentialsXImporterTest.java new file mode 100644 index 0000000..348fa6e --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/importers/EssentialsXImporterTest.java @@ -0,0 +1,62 @@ +package com.samleighton.sethomestwo.importers; + +import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class EssentialsXImporterTest extends ServerTestBase { + + private final EssentialsXImporter importer = new EssentialsXImporter(); + + private File userdataDir() { + File dir = new File(plugin.getDataFolder().getParentFile(), "Essentials/userdata"); + dir.mkdirs(); + return dir; + } + + private void writeUserFile(UUID owner, String lastAccountName, String homeName, String worldName) throws IOException { + YamlConfiguration yaml = new YamlConfiguration(); + if (lastAccountName != null) { + yaml.set("lastAccountName", lastAccountName); + } + String path = "homes." + homeName + "."; + yaml.set(path + "world", worldName); + yaml.set(path + "x", 0.0); + yaml.set(path + "y", 64.0); + yaml.set(path + "z", 0.0); + yaml.set(path + "pitch", 0.0); + yaml.set(path + "yaw", 0.0); + yaml.save(new File(userdataDir(), owner + ".yml")); + } + + @Test + void theImportedHomeGetsTheStoredAccountName() throws IOException { + UUID owner = UUID.randomUUID(); + writeUserFile(owner, "Steve", "base", "world"); + + ImportReport report = importer.run(false); + + assertEquals("Steve", new HomesDao().get(owner, "base").getPlayerName()); + assertEquals(1, report.namesResolved); + } + + @Test + void aUserFileWithNoLastAccountNameImportsWithNoNameAndNoWarning() throws IOException { + UUID owner = UUID.randomUUID(); + writeUserFile(owner, null, "base", "world"); + + ImportReport report = importer.run(false); + + assertNull(new HomesDao().get(owner, "base").getPlayerName()); + assertTrue(report.warnings.isEmpty()); + } +} diff --git a/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java index 2a5aa2a..a55add9 100644 --- a/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java +++ b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java @@ -1,10 +1,12 @@ package com.samleighton.sethomestwo.importers; import com.samleighton.sethomestwo.dao.BlacklistDao; +import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; import org.bukkit.configuration.file.YamlConfiguration; import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; import java.io.File; import java.io.IOException; @@ -13,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; class SetHomesV1ImporterTest extends ServerTestBase { @@ -129,4 +132,50 @@ void emptyBlacklistedWorldsListIsNotAnError() throws IOException { assertEquals(0, report.blacklistSkippedExisting); assertTrue(report.warnings.isEmpty()); } + + @Test + void aHomeImportedForAPlayerTheServerHasSeenGetsThatPlayersName() throws IOException { + PlayerMock steve = addPlayer("Steve"); + steve.disconnect(); + writeHomesFile(steve.getUniqueId(), "base", "world"); + + importer.run(false); + + assertEquals("Steve", new HomesDao().get(steve.getUniqueId(), "base").getPlayerName()); + } + + @Test + void aHomeImportedForAPlayerTheServerHasNeverSeenGetsNoNameAndNoWarning() throws IOException { + UUID neverSeen = UUID.randomUUID(); + writeHomesFile(neverSeen, "base", "world"); + + ImportReport report = importer.run(false); + + assertNull(new HomesDao().get(neverSeen, "base").getPlayerName()); + assertTrue(report.warnings.isEmpty()); + } + + @Test + void dryRunReportsNamesResolvedWithoutWritingAnything() throws IOException { + PlayerMock steve = addPlayer("Steve"); + steve.disconnect(); + writeHomesFile(steve.getUniqueId(), "base", "world"); + + ImportReport report = importer.run(true); + + assertEquals(1, report.namesResolved); + assertNull(new HomesDao().get(steve.getUniqueId(), "base")); + } + + private void writeHomesFile(UUID owner, String homeName, String worldName) throws IOException { + YamlConfiguration yaml = new YamlConfiguration(); + String path = "allNamedHomes." + owner + "." + homeName + "."; + yaml.set(path + "world", worldName); + yaml.set(path + "x", 0.0); + yaml.set(path + "y", 64.0); + yaml.set(path + "z", 0.0); + yaml.set(path + "pitch", 0.0); + yaml.set(path + "yaw", 0.0); + yaml.save(new File(setHomesDir(), "homes.yml")); + } } From 584cd9820ae42ac972ce97f91c1159a4a3498b80 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 17:49:15 -0400 Subject: [PATCH 35/75] feat: report v1 config settings with a Set Homes Two equivalent on import --- .changeset/gentle-foxes-config.md | 5 ++ README.md | 16 ++++ .../sethomestwo/commands/ImportHomes.java | 3 + .../sethomestwo/importers/ImportReport.java | 1 + .../importers/SetHomesV1Importer.java | 46 ++++++++++++ .../sethomestwo/commands/ImportHomesTest.java | 20 +++++ .../importers/SetHomesV1ImporterTest.java | 75 +++++++++++++++++++ 7 files changed, 166 insertions(+) create mode 100644 .changeset/gentle-foxes-config.md diff --git a/.changeset/gentle-foxes-config.md b/.changeset/gentle-foxes-config.md new file mode 100644 index 0000000..e3cf32e --- /dev/null +++ b/.changeset/gentle-foxes-config.md @@ -0,0 +1,5 @@ +--- +bump: minor +--- + +/import-homes sethomes now lists any v1 config.yml settings that have a Set Homes Two equivalent, and the key to set each one under, so a migrating admin does not have to discover a changed teleport delay or a vanished per-group home limit by accident. Nothing is written to config.yml automatically; the report just tells you what to paste in. diff --git a/README.md b/README.md index 8b5b730..93b7cbe 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Your players keep their homes. The old plugin does not even need to be running, - Happy with the numbers? Run it again with `confirm` on the end. - Existing homes are never overwritten, so re-running it is always safe. Homes in worlds that no longer exist are skipped with a warning naming the world. - `/import-homes sethomes` also brings across the v1 world blacklist, added to your Set Homes Two blacklist alongside the homes. Re-running never adds a world twice. +- `/import-homes sethomes` also lists any v1 `config.yml` settings that carry over, and the Set Homes Two key to put each one under. Nothing is written to `config.yml` automatically - the table below has the same mapping for pasting in by hand. - Admin commands (`/get-player-homes`, `/home-of`, `/delhome-of`, `/uhome-of`) work on an imported player immediately, for any player this server has seen before - the importer resolves their name from the server's own player cache, no network lookup involved. A player the server has never seen imports with no name and is picked up automatically on their first join, same as any other offline lookup. Afterwards, remove the old jar. Set Homes Two provides `/sethome`, `/home` and `/delhome`, and two plugins claiming the same commands will fight over them. @@ -116,6 +117,21 @@ Worth knowing before you copy a permissions file across: +
+Set Homes v1: config.yml settings and their Set Homes Two equivalent + +| v1 `config.yml` | Set Homes Two `config.yml` | Note | +| --- | --- | --- | +| `tp-delay` | `delay` | direct | +| `tp-cancelOnMove` | `cancelOnMove` | direct | +| `max-homes.` | `maxHomes.` | also set `maxHomesType: groups` and `maxHomeEnabled: true`. A v1 value of `0` means unlimited; leave that group out of `maxHomes` in Set Homes Two rather than setting it to `0`, which would cap it at zero homes instead. | +| `max-homes-msg` | `maxHomesReached` | direct - v1's `§` colour codes paste in unchanged | +| `tp-cancelOnMove-msg` | `movedWhileTeleporting` | direct - v1's `§` colour codes paste in unchanged | +| `tp-cooldown` | none | Set Homes Two has no cooldown feature | +| `tp-cooldown-msg` | none | follows the above | + +
+ ## Commands | Command | Long form | What it does | diff --git a/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java b/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java index 74fea1f..1634411 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java @@ -43,6 +43,9 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command for (String warning : report.warnings) { commandSender.sendMessage("Warning: " + warning); } + for (String note : report.configNotes) { + commandSender.sendMessage("Config: " + note); + } if (dryRun && (report.imported > 0 || report.hasBlacklistActivity())) { commandSender.sendMessage(String.format("Dry run only. Run '/import-homes %s confirm' to apply.", importer.sourceName())); } diff --git a/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java b/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java index 75c8a5e..aadc7ac 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java @@ -12,6 +12,7 @@ public class ImportReport { public int blacklistSkippedExisting = 0; public int namesResolved = 0; public final List warnings = new ArrayList<>(); + public final List configNotes = new ArrayList<>(); public String summary(boolean dryRun) { String verb = dryRun ? "Would import" : "Imported"; diff --git a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java index 0961b8c..35e542c 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java @@ -56,6 +56,7 @@ public ImportReport run(boolean dryRun) { } importBlacklist(pluginsDir, report, dryRun); + reportConfig(pluginsDir, report); return report; } @@ -156,4 +157,49 @@ private void importBlacklist(File pluginsDir, ImportReport report, boolean dryRu report.blacklistImported++; } } + + /** + * Never writes config.yml - issue #40 chose the report-only option over an + * automatic merge, because a real merge needs the config-merge behaviour + * from issue #35, which does not exist yet. This only tells the admin what + * to set and where. + */ + private void reportConfig(File pluginsDir, ImportReport report) { + File configFile = new File(pluginsDir, "SetHomes/config.yml"); + if (!configFile.exists()) return; + + YamlConfiguration v1 = YamlConfiguration.loadConfiguration(configFile); + + if (v1.isSet("tp-delay")) { + report.configNotes.add(String.format("v1 tp-delay: %s -> set delay: %s in config.yml", v1.get("tp-delay"), v1.get("tp-delay"))); + } + + if (v1.isSet("tp-cancelOnMove")) { + report.configNotes.add(String.format("v1 tp-cancelOnMove: %s -> set cancelOnMove: %s in config.yml", v1.get("tp-cancelOnMove"), v1.get("tp-cancelOnMove"))); + } + + if (v1.isSet("max-homes-msg")) { + report.configNotes.add(String.format("v1 max-homes-msg: '%s' -> set maxHomesReached: '%s' in config.yml", v1.getString("max-homes-msg"), v1.getString("max-homes-msg"))); + } + + if (v1.isSet("tp-cancelOnMove-msg")) { + report.configNotes.add(String.format("v1 tp-cancelOnMove-msg: '%s' -> set movedWhileTeleporting: '%s' in config.yml", v1.getString("tp-cancelOnMove-msg"), v1.getString("tp-cancelOnMove-msg"))); + } + + ConfigurationSection maxHomes = v1.getConfigurationSection("max-homes"); + if (maxHomes != null && !maxHomes.getKeys(false).isEmpty()) { + report.configNotes.add("v1 max-homes -> set maxHomesType: groups and maxHomeEnabled: true in config.yml, then set maxHomes. to:"); + for (String group : maxHomes.getKeys(false)) { + int limit = maxHomes.getInt(group); + String value = limit == 0 + ? "unlimited (v1 treats 0 as unlimited; leave this group out of maxHomes rather than setting it to 0)" + : String.valueOf(limit); + report.configNotes.add(String.format(" maxHomes.%s: %s (v1 max-homes.%s was %d)", group, value, group, limit)); + } + } + + if (v1.isSet("tp-cooldown")) { + report.configNotes.add(String.format("v1 tp-cooldown: %s has no Set Homes Two equivalent; teleport cooldown is not supported.", v1.get("tp-cooldown"))); + } + } } diff --git a/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java b/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java index b37d872..0b4ddcd 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java @@ -64,4 +64,24 @@ void noBlacklistActivityMeansNoSecondLine() throws IOException { assertFalse(summary.contains("blacklist")); assertNull(player.nextMessage()); } + + @Test + void configNotesArePrintedAsConfigLines() throws IOException { + writeEmptyHomesFile(); + YamlConfiguration v1Config = new YamlConfiguration(); + v1Config.set("tp-cooldown", 30); + v1Config.save(new File(setHomesDir(), "config.yml")); + PlayerMock player = authorizedPlayer(); + + server.execute("import-homes", player, "sethomes").assertSucceeded(); + + boolean sawConfigLine = false; + String message; + while ((message = player.nextMessage()) != null) { + if (message.startsWith("Config: ") && message.contains("tp-cooldown")) { + sawConfigLine = true; + } + } + assertTrue(sawConfigLine); + } } diff --git a/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java index a55add9..671fe6f 100644 --- a/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java +++ b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java @@ -178,4 +178,79 @@ private void writeHomesFile(UUID owner, String homeName, String worldName) throw yaml.set(path + "yaw", 0.0); yaml.save(new File(setHomesDir(), "homes.yml")); } + + @Test + void directlyMappedSettingsAreReportedWithBothKeys() throws IOException { + writeEmptyHomesFile(); + writeV1Config(v1 -> { + v1.set("tp-delay", 5); + v1.set("tp-cancelOnMove", true); + }); + + ImportReport report = importer.run(true); + + assertTrue(report.configNotes.stream().anyMatch(n -> n.contains("tp-delay") && n.contains("delay"))); + assertTrue(report.configNotes.stream().anyMatch(n -> n.contains("tp-cancelOnMove") && n.contains("cancelOnMove"))); + } + + @Test + void tpCooldownIsCalledOutAsHavingNoEquivalent() throws IOException { + writeEmptyHomesFile(); + writeV1Config(v1 -> v1.set("tp-cooldown", 30)); + + ImportReport report = importer.run(true); + + assertTrue(report.configNotes.stream().anyMatch(n -> n.contains("tp-cooldown") && n.contains("no Set Homes Two equivalent"))); + } + + @Test + void aZeroMaxHomesGroupIsDescribedAsUnlimitedNotZero() throws IOException { + writeEmptyHomesFile(); + writeV1Config(v1 -> v1.set("max-homes.default", 0)); + + ImportReport report = importer.run(true); + + String note = report.configNotes.stream() + .filter(n -> n.contains("default")) + .findFirst() + .orElseThrow(); + assertTrue(note.contains("unlimited")); + assertFalse(note.matches(".*\\bmaxHomes\\.default:\\s*0\\b.*")); + } + + @Test + void aNonZeroMaxHomesGroupIsReportedWithItsNumber() throws IOException { + writeEmptyHomesFile(); + writeV1Config(v1 -> v1.set("max-homes.vip", 6)); + + ImportReport report = importer.run(true); + + assertTrue(report.configNotes.stream().anyMatch(n -> n.contains("vip") && n.contains("6"))); + } + + @Test + void missingV1ConfigFileProducesNoNotes() throws IOException { + writeEmptyHomesFile(); + + ImportReport report = importer.run(true); + + assertTrue(report.configNotes.isEmpty()); + } + + @Test + void theConfigReportNeverWritesToConfigYml() throws IOException { + writeEmptyHomesFile(); + writeV1Config(v1 -> v1.set("tp-delay", 5)); + long before = new File(plugin.getDataFolder(), "config.yml").lastModified(); + + importer.run(false); + + assertEquals(before, new File(plugin.getDataFolder(), "config.yml").lastModified()); + } + + private void writeV1Config(java.util.function.Consumer body) throws IOException { + YamlConfiguration yaml = new YamlConfiguration(); + body.accept(yaml); + yaml.save(new File(setHomesDir(), "config.yml")); + } } From 5f61883c068567767a3426eb142911e8a94e1bbe Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 18:07:00 -0400 Subject: [PATCH 36/75] fix: address final review findings in the v1 import completeness work Snapshot the offline-player cache once per import run instead of rescanning it per home, correctly report a failed blacklist write instead of counting it as a success, tell an admin that an imported blacklist entry for a nonexistent world can't be removed by command until that world exists, reword the names-resolved summary sentence so it's accurate for both import sources, and stop suggesting confirm when the only blacklist activity was already present. --- .../sethomestwo/commands/ImportHomes.java | 2 +- .../sethomestwo/importers/HomesImporter.java | 26 +++++++++-------- .../sethomestwo/importers/ImportReport.java | 2 +- .../importers/SetHomesV1Importer.java | 28 ++++++++++++++----- .../sethomestwo/commands/ImportHomesTest.java | 18 ++++++++++++ .../importers/SetHomesV1ImporterTest.java | 2 +- 6 files changed, 56 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java b/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java index 1634411..370f530 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java @@ -46,7 +46,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command for (String note : report.configNotes) { commandSender.sendMessage("Config: " + note); } - if (dryRun && (report.imported > 0 || report.hasBlacklistActivity())) { + if (dryRun && (report.imported > 0 || report.blacklistImported > 0)) { commandSender.sendMessage(String.format("Dry run only. Run '/import-homes %s confirm' to apply.", importer.sourceName())); } return true; diff --git a/src/main/java/com/samleighton/sethomestwo/importers/HomesImporter.java b/src/main/java/com/samleighton/sethomestwo/importers/HomesImporter.java index 7af54ef..c7a7442 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/HomesImporter.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/HomesImporter.java @@ -5,6 +5,8 @@ import org.bukkit.Material; import org.bukkit.OfflinePlayer; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; public interface HomesImporter { @@ -21,20 +23,20 @@ static String defaultMaterial() { } /** - * The name this server has cached for a player, or null if it has never - * seen them. Deliberately scans {@link Bukkit#getOfflinePlayers()} rather - * than calling {@code Bukkit.getOfflinePlayer(UUID)} directly: the latter - * always returns a non-null object by contract (real Bukkit backs it with - * usercache.json and reports a null name on a genuine miss, but that - * distinction should not be relied on for null-safety), and never makes a - * network call either way, so an import can never block on Mojang. + * Every name this server has cached, snapshotted once so a bulk import + * doesn't re-scan {@link Bukkit#getOfflinePlayers()} per home - on a + * server with a large playerdata directory that call is expensive to + * repeat thousands of times in one command. Deliberately built from + * {@code getOfflinePlayers()} rather than calling + * {@code Bukkit.getOfflinePlayer(UUID)} per player: the latter always + * returns a non-null object by contract, and never makes a network call + * either way, so an import can never block on Mojang. */ - static String resolveCachedName(UUID uuid) { + static Map cachedNames() { + Map names = new HashMap<>(); for (OfflinePlayer candidate : Bukkit.getOfflinePlayers()) { - if (candidate.getUniqueId().equals(uuid)) { - return candidate.getName(); - } + names.put(candidate.getUniqueId(), candidate.getName()); } - return null; + return names; } } diff --git a/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java b/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java index aadc7ac..d01d643 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java @@ -21,7 +21,7 @@ public String summary(boolean dryRun) { verb, imported, skippedExisting, skippedWorldMissing, failed ); if (namesResolved > 0) { - base += String.format(" %d player name(s) resolved from the server's cache.", namesResolved); + base += String.format(" %d home(s) matched an owner name.", namesResolved); } return base; } diff --git a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java index 35e542c..4355283 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java @@ -12,6 +12,7 @@ import java.io.File; import java.util.List; +import java.util.Map; import java.util.UUID; public class SetHomesV1Importer implements HomesImporter { @@ -34,6 +35,10 @@ public ImportReport run(boolean dryRun) { YamlConfiguration source = YamlConfiguration.loadConfiguration(homesFile); HomesDao homesDao = new HomesDao(); + // Snapshotted once, not per home - Bukkit.getOfflinePlayers() scans the + // server's playerdata directory, which is expensive to re-run for every + // imported home on a server with years of accumulated players. + Map cachedNames = HomesImporter.cachedNames(); // Named homes: allNamedHomes...{world,x,y,z,pitch,yaw,desc} ConfigurationSection allNamed = source.getConfigurationSection("allNamedHomes"); @@ -42,7 +47,7 @@ public ImportReport run(boolean dryRun) { ConfigurationSection playerSection = allNamed.getConfigurationSection(uuid); if (playerSection == null) continue; for (String homeName : playerSection.getKeys(false)) { - importOne(homesDao, report, playerSection.getConfigurationSection(homeName), uuid, homeName, dryRun); + importOne(homesDao, report, playerSection.getConfigurationSection(homeName), uuid, homeName, cachedNames, dryRun); } } } @@ -51,7 +56,7 @@ public ImportReport run(boolean dryRun) { ConfigurationSection unknown = source.getConfigurationSection("unknownHomes"); if (unknown != null) { for (String uuid : unknown.getKeys(false)) { - importOne(homesDao, report, unknown.getConfigurationSection(uuid), uuid, "default", dryRun); + importOne(homesDao, report, unknown.getConfigurationSection(uuid), uuid, "default", cachedNames, dryRun); } } @@ -61,7 +66,7 @@ public ImportReport run(boolean dryRun) { return report; } - private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSection home, String playerUUID, String homeName, boolean dryRun) { + private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSection home, String playerUUID, String homeName, Map cachedNames, boolean dryRun) { try { if (home == null) { report.failed++; @@ -89,7 +94,7 @@ private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSect (float) home.getDouble("pitch") ); - String playerName = HomesImporter.resolveCachedName(UUID.fromString(playerUUID)); + String playerName = cachedNames.get(UUID.fromString(playerUUID)); if (playerName != null) report.namesResolved++; if (!dryRun) { @@ -122,7 +127,10 @@ private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSect * empty is normal (v1 shipped it empty by default), not an error. A world * absent from this server is still stored - it is harmless to block a world * that does not exist - but is called out with a warning in case the name - * was a typo. + * was a typo. That warning also flags that /remove-from-blacklist refuses + * any world name it cannot validate against this server's current worlds, + * so an absent one can only be removed once the world exists (or by editing + * the database directly). */ private void importBlacklist(File pluginsDir, ImportReport report, boolean dryRun) { File blacklistFile = new File(pluginsDir, "SetHomes/world_blacklist.yml"); @@ -144,11 +152,17 @@ private void importBlacklist(File pluginsDir, ImportReport report, boolean dryRu } if (Bukkit.getWorld(lowered) == null) { - report.warnings.add(String.format("Blacklisted world '%s' does not exist on this server.", lowered)); + report.warnings.add(String.format( + "Blacklisted world '%s' does not exist on this server. It will still be stored, but /remove-from-blacklist will refuse it until that world exists.", + lowered)); } if (!dryRun) { - blacklistDao.save(lowered); + boolean saved = blacklistDao.save(lowered); + if (!saved) { + report.warnings.add(String.format("Blacklisted world '%s' could not be saved.", lowered)); + continue; + } } // Tracked locally too, so a world repeated in the source file is diff --git a/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java b/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java index 0b4ddcd..cfcc2be 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java @@ -1,5 +1,6 @@ package com.samleighton.sethomestwo.commands; +import com.samleighton.sethomestwo.dao.BlacklistDao; import com.samleighton.sethomestwo.support.ServerTestBase; import org.bukkit.configuration.file.YamlConfiguration; import org.junit.jupiter.api.Test; @@ -65,6 +66,23 @@ void noBlacklistActivityMeansNoSecondLine() throws IOException { assertNull(player.nextMessage()); } + @Test + void dryRunHintIsNotShownWhenTheOnlyBlacklistActivityIsAlreadyPresent() throws IOException { + writeEmptyHomesFile(); + new BlacklistDao().save("world_nether"); + writeBlacklist("world_nether"); + PlayerMock player = authorizedPlayer(); + + server.execute("import-homes", player, "sethomes").assertSucceeded(); + + boolean sawHint = false; + String message; + while ((message = player.nextMessage()) != null) { + if (message.contains("Dry run only")) sawHint = true; + } + assertFalse(sawHint); + } + @Test void configNotesArePrintedAsConfigLines() throws IOException { writeEmptyHomesFile(); diff --git a/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java index 671fe6f..e22405b 100644 --- a/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java +++ b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java @@ -103,7 +103,7 @@ void aBlacklistedWorldThatDoesNotExistIsStoredWithAWarning() throws IOException assertEquals(1, report.blacklistImported); assertTrue(new BlacklistDao().getAll().contains("world_the_void")); - assertTrue(report.warnings.stream().anyMatch(w -> w.contains("world_the_void"))); + assertTrue(report.warnings.stream().anyMatch(w -> w.contains("world_the_void") && w.contains("/remove-from-blacklist"))); } @Test From 21eaeb29390fb0e85a1225c14238f349b78433ca Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 21:42:16 -0400 Subject: [PATCH 37/75] fix version bumps --- .changeset/eager-badgers-return.md | 2 +- .changeset/eager-seals-shine.md | 2 +- .changeset/gentle-foxes-config.md | 2 +- .changeset/tidy-wolves-travel.md | 2 +- .changeset/wobbly-hedgehogs-import.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/eager-badgers-return.md b/.changeset/eager-badgers-return.md index b30249b..d12abee 100644 --- a/.changeset/eager-badgers-return.md +++ b/.changeset/eager-badgers-return.md @@ -1,5 +1,5 @@ --- -bump: minor +bump: patch --- Added the Set Homes v1 admin commands. /home-of teleports you to another player's home, /delhome-of deletes one, and /uhome-of moves one to where you are standing. /uhome moves one of your own homes. All of them work on players who are offline, as long as they have saved homes, and player and home names now match without regard to case. On a server upgrading from an earlier release, each player has to log in once before the offline lookup can find them. diff --git a/.changeset/eager-seals-shine.md b/.changeset/eager-seals-shine.md index 8193e28..3e07cf7 100644 --- a/.changeset/eager-seals-shine.md +++ b/.changeset/eager-seals-shine.md @@ -1,5 +1,5 @@ --- -bump: minor +bump: patch --- /sethome and /home work with no arguments again, creating and finding a home called default, which is the name an unnamed Set Homes v1 home is imported under. /sethome also takes a description straight after the name as it did in v1, so a second word that names an item is read as the icon and the reply tells you which one it chose. Put d in the icon position to keep the whole phrase as the description. diff --git a/.changeset/gentle-foxes-config.md b/.changeset/gentle-foxes-config.md index e3cf32e..ebbf426 100644 --- a/.changeset/gentle-foxes-config.md +++ b/.changeset/gentle-foxes-config.md @@ -1,5 +1,5 @@ --- -bump: minor +bump: patch --- /import-homes sethomes now lists any v1 config.yml settings that have a Set Homes Two equivalent, and the key to set each one under, so a migrating admin does not have to discover a changed teleport delay or a vanished per-group home limit by accident. Nothing is written to config.yml automatically; the report just tells you what to paste in. diff --git a/.changeset/tidy-wolves-travel.md b/.changeset/tidy-wolves-travel.md index da0bf1a..541e1b1 100644 --- a/.changeset/tidy-wolves-travel.md +++ b/.changeset/tidy-wolves-travel.md @@ -1,5 +1,5 @@ --- -bump: minor +bump: patch --- Permission defaults can now be changed from config.yml with no permissions plugin installed. Uncomment the permissions block and set any sh2 node to true, false, op or not-op. Two bundles, sh2.player and sh2.admin, move a whole role at once, and three new bypass permissions were added for admins: sh2.bypass-max-homes, sh2.bypass-blacklist and sh2.bypass-teleport-delay. diff --git a/.changeset/wobbly-hedgehogs-import.md b/.changeset/wobbly-hedgehogs-import.md index afad234..189af73 100644 --- a/.changeset/wobbly-hedgehogs-import.md +++ b/.changeset/wobbly-hedgehogs-import.md @@ -1,5 +1,5 @@ --- -bump: minor +bump: patch --- /import-homes sethomes now brings across the Set Homes v1 world blacklist along with the homes, so a server that had worlds blocked in v1 does not silently lose that protection on migration. Re-running the import never blacklists a world twice. From 9046e23b7a3a75d1ab732728c7c844ee9c4bfee6 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 22:13:39 -0400 Subject: [PATCH 38/75] feat: add /setmax alias for /set-max-homes Matches the v1 command name so admins upgrading do not have to learn a new one. Usage messages for both singular and groups mode now name whichever form was actually typed, matching the pattern already used by the other admin commands. Also cleans up the README: documents the new alias, drops the now-inaccurate "/setmax is not an alias" note, and trims the per-release upgrade-notes section, which had grown into more text than server owners need. --- .changeset/quiet-otters-jump.md | 5 ++ README.md | 20 +---- .../sethomestwo/commands/SetMaxHomes.java | 6 +- .../sethomestwo/enums/UserError.java | 4 +- src/main/resources/plugin.yml | 2 + .../sethomestwo/commands/SetMaxHomesTest.java | 77 +++++++++++++++++++ 6 files changed, 90 insertions(+), 24 deletions(-) create mode 100644 .changeset/quiet-otters-jump.md create mode 100644 src/test/java/com/samleighton/sethomestwo/commands/SetMaxHomesTest.java diff --git a/.changeset/quiet-otters-jump.md b/.changeset/quiet-otters-jump.md new file mode 100644 index 0000000..ee0f00d --- /dev/null +++ b/.changeset/quiet-otters-jump.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +/set-max-homes now has the alias /setmax, matching Set Homes v1. Its usage message also names whichever form you typed, rather than always naming /set-max-homes. diff --git a/README.md b/README.md index e4ecc76..96787c4 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,6 @@ Afterwards, remove the old jar. Set Homes Two provides `/sethome`, `/home` and ` | `/uhome-of [home]` | `/uhome-of ` | | `/blacklist ` | `/blacklist ` | | `/setmax ` | `/set-max-homes ` | -| `/strike` | Gone. See below. | | v1 permission | Set Homes Two permission | | --- | --- | @@ -108,8 +107,6 @@ Worth knowing before you copy a permissions file across: - **Your v1 unnamed home is called `default`.** The importer files it under that name, and a bare `/sethome` or `/home` uses the same name, so both keep working exactly as they did. `/home-of steve default` reaches an imported unnamed home. - **`/sethome` takes a description straight after the name again**, as it did in v1. Set Homes Two adds an optional icon in between, so a second word naming a real item is read as the icon. `/sethome base d my main base` forces the default icon and keeps the whole phrase. - **The one-letter aliases are not provided.** v1 registered `/h`, `/sh`, `/dh`, `/lh`, `/ho`, `/dho`, `/uh`, `/uho`, `/bl` and `/sm`. `/h` in particular collides with several other homes plugins, and Bukkit resolves a collision silently by prefixing one of them, which is worse than not having it. If you want them, map them yourself in the server's own `commands.yml`. -- **`/setmax` is not an alias either.** The command is `/set-max-homes`. -- **`/strike` was removed on purpose.** It was a lightning wand, not a homes feature. - **`/homes` means something different.** In v1 it printed a chat list. In Set Homes Two it opens the homes menu, and `/list-homes` prints the chat list. @@ -135,7 +132,7 @@ On `/sethome`, a second word that names a real item becomes the icon, and everyt | Command | What it does | | --- | --- | -| `/set-max-homes [group] ` | Sets the home limit, per LuckPerms group or server-wide. | +| `/set-max-homes [group] ` (alias `/setmax`) | Sets the home limit, per LuckPerms group or server-wide. | | `/get-player-homes ` | Lists another player's homes. | | `/home-of ` (long form `/go-player-home`) | Teleports you to another player's home. | | `/delhome-of ` (long form `/delete-player-home`) | Deletes another player's home. | @@ -265,21 +262,6 @@ To pick one up, copy the key you want out of [`default-config.yml`](https://gith -## Upgrading an existing Set Homes Two server - -Your homes, your config and your permissions carry over untouched. These are the changes a player or an admin can notice, listed so that nobody has to work them out from the symptom. Only the first one is worth checking before you update. - -- **The world blacklist now works on every world.** Blacklisting always accepted any world name and reported success, but only the first three worlds were ever enforced, so a fourth was quietly ignored. It is enforced now. If you blacklisted a world beyond the first three, check `/blacklist list`, because homes there will start being refused and existing ones will stop being reachable. -- **`/sethome base stone house` now means a stone icon and the description "house".** A second word naming a real item is taken as the icon. Put `d` in that position to force the default icon and keep the whole phrase: `/sethome base d stone house`. The reply names the icon it chose. -- **A word like `water`, `fire`, `lava` or `wall_torch` stays description text.** Those are real materials but not items, so they cannot be a home icon. Rather than refusing the command, Set Homes Two treats them as the start of the description. -- **The admin commands cannot find an offline player until that player logs in once.** Set Homes Two learns which name belongs to which account when a player joins, and a database written by an earlier release has none of those names recorded yet. Until a player has reconnected once, `/home-of`, `/delhome-of` and `/uhome-of` will say no player by that name is online or has any saved homes, even though their homes are safe and still there. It corrects itself the first time they log in. -- **The confirmation for `/sethome` will not name the icon until you update `homeCreated`.** The icon is what tells you which word was taken as the icon rather than as description text, and it comes from a new second `%s` in that message. Your existing `config.yml` keeps the old wording, so copy `homeCreated` out of `default-config.yml` if you want it. -- **Home names and player names now ignore case everywhere.** `/home Base` always found `base`; `/delhome`, `/uhome` and the admin commands now match it. `/delhome Base` therefore deletes `base`. -- **`defaultHomeItem` now applies to homes players create.** It used to apply only to imported homes, so a server that set it to `chest` still got white wool on everything new. New homes with no icon now take the configured item. -- **`/sethome` now checks the home name.** A blank name is refused and `maxHomeNameLength` is enforced. Both were previously checked only when renaming from the menu, which allowed a home nothing could address. -- **A missing home is named in the error.** Four commands used to say "That home no longer exists"; they now say "The home 'base' does not exist". The menu keeps the old wording, where it is still the accurate one. -- **Operators can now bypass the blacklist.** That includes moving another player's home into a blacklisted world. The owner, who does not hold `sh2.bypass-blacklist`, then sees "Cannot teleport here: dimension blacklisted" on that home and cannot reach it, so move it back out or grant them the node. - ## FAQ
diff --git a/src/main/java/com/samleighton/sethomestwo/commands/SetMaxHomes.java b/src/main/java/com/samleighton/sethomestwo/commands/SetMaxHomes.java index a533833..f5abd53 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/SetMaxHomes.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/SetMaxHomes.java @@ -24,7 +24,7 @@ public class SetMaxHomes implements CommandExecutor { public SetMaxHomes(Plugin plugin) { this.plugin = plugin; } - public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, String[] args) { + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String label, String[] args) { if (!(commandSender instanceof Player)) { commandSender.sendMessage(UserError.PLAYERS_ONLY.getValue()); return false; @@ -46,13 +46,13 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command // Depending on if grouping is singular or groups, guard against incorrect number of args if (maxHomesType.equals("singular") && args.length != 1) { ChatUtils.incorrectNumArguments(player); - ChatUtils.sendError(player, UserError.SET_MAX_HOMES_SINGULAR.getValue()); + ChatUtils.sendError(player, String.format(UserError.SET_MAX_HOMES_SINGULAR.getValue(), label)); return false; } if (maxHomesType.equals("groups") && args.length != 2) { ChatUtils.incorrectNumArguments(player); - ChatUtils.sendError(player, UserError.SET_MAX_HOMES_GROUPS.getValue()); + ChatUtils.sendError(player, String.format(UserError.SET_MAX_HOMES_GROUPS.getValue(), label)); return false; } diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserError.java b/src/main/java/com/samleighton/sethomestwo/enums/UserError.java index 9a95d61..f86d955 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserError.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserError.java @@ -5,8 +5,8 @@ public enum UserError { INVALID_HOME_ITEM("This home item does not belong to you."), /** Max Home restriction */ - SET_MAX_HOMES_SINGULAR("Max Homes Type is singular. Usage: /set-max-homes [max number of homes]"), - SET_MAX_HOMES_GROUPS("Max Homes Type is groups. Usage: /set-max-homes [group name] [max number of homes]"), + SET_MAX_HOMES_SINGULAR("Max Homes Type is singular. Usage: /%s [max number of homes]"), + SET_MAX_HOMES_GROUPS("Max Homes Type is groups. Usage: /%s [group name] [max number of homes]"), MAX_HOMES("You have reached the maximum number of homes allowed."), /** Teleport restriction */ diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 5d737ba..1d131b2 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -36,6 +36,8 @@ commands: set-max-homes: description: Sets the max number of homes all players, or individual groups, can have. permission: sh2.set-max-homes + aliases: + - setmax go-home: description: Allows a player to use commands to teleport to homes. permission: sh2.go-home diff --git a/src/test/java/com/samleighton/sethomestwo/commands/SetMaxHomesTest.java b/src/test/java/com/samleighton/sethomestwo/commands/SetMaxHomesTest.java new file mode 100644 index 0000000..96e4236 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/commands/SetMaxHomesTest.java @@ -0,0 +1,77 @@ +package com.samleighton.sethomestwo.commands; + +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +import java.io.File; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SetMaxHomesTest extends ServerTestBase { + + // SetMaxHomes reads and writes config.yml directly from disk rather than + // through plugin.getConfig(), so tests that need a particular maxHomesType + // write the file themselves. + private void useSingularMaxHomes() throws IOException { + File configFile = new File(plugin.getDataFolder(), "config.yml"); + YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); + config.set("maxHomesType", "singular"); + config.set("maxHomes", 5); + config.save(configFile); + } + + @Test + void theAliasWorks() throws IOException { + useSingularMaxHomes(); + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.set-max-homes", true); + + server.execute("setmax", player, "7"); + + assertEquals(7, savedConfig().getInt("maxHomes")); + } + + @Test + void theUsageNamesTheAliasThatWasTyped() throws IOException { + useSingularMaxHomes(); + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.set-max-homes", true); + + server.dispatchCommand(player, "setmax"); + + player.nextMessage(); + assertTrue(player.nextMessage().contains("Usage: /setmax")); + } + + // The shipped default-config.yml is in groups mode, where maxHomes is a + // section (admin: 5, user: 4) rather than a scalar. getString("maxHomes") + // returns null for a section, which used to be misread as "not configured". + @Test + void groupsModeAcceptsTheShippedDefaultConfig() { + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.set-max-homes", true); + + server.execute("set-max-homes", player, "admin", "9"); + + assertEquals(9, savedConfig().getInt("maxHomes.admin")); + assertEquals(4, savedConfig().getInt("maxHomes.user")); + } + + @Test + void groupsModeRejectsAnUnknownGroup() { + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.set-max-homes", true); + + server.execute("set-max-homes", player, "nope", "9"); + + assertTrue(player.nextMessage().contains("Group does not exist")); + } + + private YamlConfiguration savedConfig() { + return YamlConfiguration.loadConfiguration(new File(plugin.getDataFolder(), "config.yml")); + } +} From 4b8b7ac24ff26510c6cdc140b5def664cf4995eb Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 22:39:37 -0400 Subject: [PATCH 39/75] fix: report blacklist write failures and match permission nodes case-insensitively /blacklist add and /blacklist remove inspected the DAO result, logged it behind a debug level, and then sent the success message anyway. At debugLevel: error a failed remove sent both the failure and the success on the same line. PermissionOverrides looked a node up through getPermission, which lowercases, but detached it from its bundle through the children map, which does not. A node written in any other case was logged as applied while sh2.player or sh2.admin carried on granting it. Also documents that denying a node takes it from operators too, since sh2.admin lists sh2.player as a child, and prunes comments that narrated the change rather than the code. The comment on ImportHomes still said the import permission defaulted to op, and one in ImportHomesTest named a gate the code does not use. --- .changeset/plain-lions-report.md | 5 ++ README.md | 2 + .../samleighton/sethomestwo/SetHomesTwo.java | 2 +- .../sethomestwo/commands/Blacklist.java | 18 ++++---- .../sethomestwo/commands/ImportHomes.java | 3 +- .../sethomestwo/importers/HomesImporter.java | 11 ++--- .../importers/SetHomesV1Importer.java | 20 +++----- .../utils/PermissionOverrides.java | 17 +++++-- .../sethomestwo/commands/BlacklistTest.java | 46 +++++++++++++++---- .../sethomestwo/commands/ImportHomesTest.java | 6 +-- .../sethomestwo/support/HomeFixtures.java | 22 +++++++++ .../utils/PermissionOverridesTest.java | 28 +++++++++++ 12 files changed, 131 insertions(+), 49 deletions(-) create mode 100644 .changeset/plain-lions-report.md diff --git a/.changeset/plain-lions-report.md b/.changeset/plain-lions-report.md new file mode 100644 index 0000000..2b90156 --- /dev/null +++ b/.changeset/plain-lions-report.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +/blacklist add and /blacklist remove no longer report success when the database write actually failed. A failure now says so and the world is left as it was. Permission overrides in config.yml are also matched without regard to the case you write the node in, so a line like SH2.manage-homes now applies rather than being logged as applied and quietly ignored. diff --git a/README.md b/README.md index de023d9..85d4a53 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,8 @@ Accepted values are `true` (everyone), `false` (nobody), `op` (operators only) a The two bundles are nodes in their own right, so `sh2.player: false` moves the whole player set at once and `sh2.admin: true` hands every admin command to everybody. That last one is rarely what you want. +**Denying a node takes it away from operators too.** `sh2.admin` contains `sh2.player`, so `sh2.player: false` stops operators creating homes as well, and `sh2.manage-homes: false` applies to them just the same. This is what makes a deny a real deny rather than something operators quietly keep. Every node the config detaches from a bundle is named in the server log at startup, so you can see exactly what moved. If you want a node gone for everyone except operators, set it to `op` instead of `false`. + **This only changes a default.** If you run LuckPerms or similar, an explicit grant or deny there still wins. The config block decides what happens to a player the permissions plugin says nothing about. Take care with `sh2.import-homes`. `/import-homes confirm` writes homes for every player on the server and there is no second check inside the command, so granting it to everyone is a real risk. The plugin logs a warning if you move it off `op`. diff --git a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java index 1e09203..acca8fc 100644 --- a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java +++ b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java @@ -57,7 +57,7 @@ public void onEnable() { // Create config initConfig(); - // After the config exists, before commands are registered. + // Needs the config on disk, so it cannot move above initConfig. PermissionOverrides.apply(); // Built before the listeners: the join listener is handed this instance. diff --git a/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java b/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java index 8361f37..6be8ea9 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/Blacklist.java @@ -2,7 +2,6 @@ import com.samleighton.sethomestwo.dao.BlacklistDao; import com.samleighton.sethomestwo.dao.Dao; -import com.samleighton.sethomestwo.enums.DebugLevel; import com.samleighton.sethomestwo.enums.PluginError; import com.samleighton.sethomestwo.enums.UserError; import com.samleighton.sethomestwo.enums.UserInfo; @@ -25,8 +24,8 @@ * into one executor. Bukkit only hands onCommand the label the player actually * typed (add-to-blacklist, remove-from-blacklist, get-blacklisted-dimensions, or * blacklist), so the subcommand is inferred from that label rather than always - * reading args[0] - otherwise "/add-to-blacklist world_nether", the exact - * pre-existing usage this command replaces, would silently fail. + * reading args[0]. Reading args[0] unconditionally would break the bare + * "/add-to-blacklist world_nether" form, which takes no subcommand. */ public class Blacklist implements CommandExecutor { @@ -128,9 +127,10 @@ private boolean add(Player player, String label, String[] dimensions) { continue; } - boolean success = blacklistDao.save(dimension); - if (!success && ConfigUtil.getDebugLevel().equals(DebugLevel.INFO)) { - Bukkit.getLogger().info(String.format("Failed to add dimension to blacklist. %s", dimension)); + if (!blacklistDao.save(dimension)) { + Bukkit.getLogger().severe(String.format("Failed to add dimension to blacklist. %s", dimension)); + ChatUtils.sendError(player, PluginError.ADD_DIMENSION_FAILED.getValue()); + continue; } ChatUtils.sendSuccess(player, String.format( @@ -169,10 +169,10 @@ private boolean remove(Player player, String label, String[] dimensions) { continue; } - boolean success = blacklistDao.delete(dimension); - if (!success && ConfigUtil.getDebugLevel().equals(DebugLevel.ERROR)) { - Bukkit.getLogger().info(String.format("Failed to remove dimension from blacklist. %s", dimension)); + if (!blacklistDao.delete(dimension)) { + Bukkit.getLogger().severe(String.format("Failed to remove dimension from blacklist. %s", dimension)); ChatUtils.sendError(player, PluginError.REMOVE_DIMENSION_FAILED.getValue()); + continue; } ChatUtils.sendSuccess(player, String.format( diff --git a/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java b/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java index 370f530..ee34a3f 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/ImportHomes.java @@ -26,7 +26,8 @@ public class ImportHomes implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, String[] args) { - // Console and players may both run this; permission handled by plugin.yml (default op) + // Console and players may both run this; the permission is enforced by + // plugin.yml before onCommand is reached. if (args.length < 1 || !SOURCES.containsKey(args[0].toLowerCase())) { commandSender.sendMessage(String.format("Usage: /%s <%s> [confirm]", s, String.join("|", SOURCES.keySet()))); return true; diff --git a/src/main/java/com/samleighton/sethomestwo/importers/HomesImporter.java b/src/main/java/com/samleighton/sethomestwo/importers/HomesImporter.java index c7a7442..26d7dd6 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/HomesImporter.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/HomesImporter.java @@ -23,14 +23,9 @@ static String defaultMaterial() { } /** - * Every name this server has cached, snapshotted once so a bulk import - * doesn't re-scan {@link Bukkit#getOfflinePlayers()} per home - on a - * server with a large playerdata directory that call is expensive to - * repeat thousands of times in one command. Deliberately built from - * {@code getOfflinePlayers()} rather than calling - * {@code Bukkit.getOfflinePlayer(UUID)} per player: the latter always - * returns a non-null object by contract, and never makes a network call - * either way, so an import can never block on Mojang. + * Every name this server has cached, keyed by UUID. Snapshotted once per + * import because {@link Bukkit#getOfflinePlayers()} scans the playerdata + * directory on every call. */ static Map cachedNames() { Map names = new HashMap<>(); diff --git a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java index 4355283..644798f 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java @@ -35,9 +35,6 @@ public ImportReport run(boolean dryRun) { YamlConfiguration source = YamlConfiguration.loadConfiguration(homesFile); HomesDao homesDao = new HomesDao(); - // Snapshotted once, not per home - Bukkit.getOfflinePlayers() scans the - // server's playerdata directory, which is expensive to re-run for every - // imported home on a server with years of accumulated players. Map cachedNames = HomesImporter.cachedNames(); // Named homes: allNamedHomes...{world,x,y,z,pitch,yaw,desc} @@ -124,13 +121,10 @@ private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSect /** * v1's world_blacklist.yml holds a flat blacklisted_worlds list. Missing or - * empty is normal (v1 shipped it empty by default), not an error. A world - * absent from this server is still stored - it is harmless to block a world - * that does not exist - but is called out with a warning in case the name - * was a typo. That warning also flags that /remove-from-blacklist refuses - * any world name it cannot validate against this server's current worlds, - * so an absent one can only be removed once the world exists (or by editing - * the database directly). + * empty is normal, not an error. A world absent from this server is still + * stored, but warned about: /blacklist remove validates against the + * server's current worlds, so an absent one cannot be removed by command + * until that world exists. */ private void importBlacklist(File pluginsDir, ImportReport report, boolean dryRun) { File blacklistFile = new File(pluginsDir, "SetHomes/world_blacklist.yml"); @@ -173,10 +167,8 @@ private void importBlacklist(File pluginsDir, ImportReport report, boolean dryRu } /** - * Never writes config.yml - issue #40 chose the report-only option over an - * automatic merge, because a real merge needs the config-merge behaviour - * from issue #35, which does not exist yet. This only tells the admin what - * to set and where. + * Reports the v1 settings that have an equivalent here, and the key to put + * each one under. Never writes config.yml. */ private void reportConfig(File pluginsDir, ImportReport report) { File configFile = new File(pluginsDir, "SetHomes/config.yml"); diff --git a/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java b/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java index d9b56f6..1add5f2 100644 --- a/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java +++ b/src/main/java/com/samleighton/sethomestwo/utils/PermissionOverrides.java @@ -6,6 +6,8 @@ import org.bukkit.permissions.PermissionDefault; import org.bukkit.plugin.PluginManager; +import java.util.Locale; + /** * Applies the config.yml permissions block over the defaults declared in * plugin.yml. Only a node's default changes, so an explicit grant or deny in a @@ -24,22 +26,27 @@ public static void apply() { // Deep keys, because Bukkit splits a dotted key such as sh2.import-homes // into nested sections. The intermediate sections are not nodes. - for (String node : section.getKeys(true)) { - if (section.isConfigurationSection(node)) continue; + for (String key : section.getKeys(true)) { + if (section.isConfigurationSection(key)) continue; + + // Bukkit lowercases a node name when looking it up but not when + // storing it as a bundle child, so detachFromBundles only matches + // the canonical spelling. The config value still reads by raw key. + String node = key.toLowerCase(Locale.ROOT); Permission permission = pluginManager.getPermission(node); if (permission == null) { Bukkit.getLogger().warning(String.format( - "SetHomesTwo: ignoring unknown permission node '%s' in config.yml.", node)); + "SetHomesTwo: ignoring unknown permission node '%s' in config.yml.", key)); continue; } - String raw = section.getString(node); + String raw = section.getString(key); PermissionDefault parsed = raw == null ? null : PermissionDefault.getByName(raw); if (parsed == null) { Bukkit.getLogger().warning(String.format( "SetHomesTwo: ignoring permission '%s', value '%s' is not one of true, false, op, not-op.", - node, raw)); + key, raw)); continue; } diff --git a/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java b/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java index b8f6474..ed7e84f 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java @@ -7,6 +7,7 @@ import org.mockbukkit.mockbukkit.entity.PlayerMock; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; class BlacklistTest extends ServerTestBase { @@ -106,14 +107,6 @@ void anUnknownWorldIsRejected() { assertTrue(new BlacklistDao().getAll().isEmpty()); } - // The old command names arrive at onCommand with no subcommand token at - // all (e.g. "/add-to-blacklist world_nether"), unlike the new "blacklist" - // name, which always expects one. Bukkit hands onCommand the exact label - // the player typed only when the command is dispatched through the real - // command line, so these use server.dispatchCommand rather than - // server.execute, which always reports the canonical command name as the - // label regardless of which alias was used to look it up. - @Test void theAddSuccessMessageIsOverridableInConfig() { PlayerMock player = addPlayer(); @@ -125,6 +118,43 @@ void theAddSuccessMessageIsOverridableInConfig() { assertTrue(player.nextMessage().contains("Blocked world_nether.")); } + @Test + void aFailedAddIsReportedInsteadOfClaimingSuccess() { + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.add-to-blacklist", true); + HomeFixtures.breakBlacklistWrites(); + + server.execute("blacklist", player, "add", "world_nether").assertSucceeded(); + + String message = player.nextMessage(); + assertTrue(message.contains("issue adding dimension"), message); + assertNull(player.nextMessage(), "a failed write must not also send the success message"); + assertFalse(new BlacklistDao().getAll().contains("world_nether")); + } + + @Test + void aFailedRemoveIsReportedInsteadOfClaimingSuccess() { + HomeFixtures.blacklist("world_nether"); + PlayerMock player = addPlayer(); + player.addAttachment(plugin, "sh2.remove-from-blacklist", true); + HomeFixtures.breakBlacklistWrites(); + + server.execute("blacklist", player, "remove", "world_nether").assertSucceeded(); + + String message = player.nextMessage(); + assertTrue(message.contains("issue removing dimension"), message); + assertNull(player.nextMessage(), "a failed write must not also send the success message"); + assertTrue(new BlacklistDao().getAll().contains("world_nether")); + } + + // The old command names arrive at onCommand with no subcommand token at + // all (e.g. "/add-to-blacklist world_nether"), unlike the new "blacklist" + // name, which always expects one. Bukkit hands onCommand the exact label + // the player typed only when the command is dispatched through the real + // command line, so these use server.dispatchCommand rather than + // server.execute, which always reports the canonical command name as the + // label regardless of which alias was used to look it up. + @Test void bareAddToBlacklistAliasWithNoSubcommandStillAdds() { PlayerMock player = addPlayer(); diff --git a/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java b/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java index cfcc2be..8059df1 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java @@ -58,9 +58,9 @@ void noBlacklistActivityMeansNoSecondLine() throws IOException { server.execute("import-homes", player, "sethomes").assertSucceeded(); - // 0 homes and 0 blacklist activity: only the summary line is sent. - // The dry-run hint is gated on (imported > 0 || hasBlacklistActivity()), - // so it does not fire here either. + // 0 homes and 0 blacklist activity: only the summary line is sent. The + // dry-run hint is gated on (imported > 0 || blacklistImported > 0), so + // it does not fire here either. String summary = player.nextMessage(); assertFalse(summary.contains("blacklist")); assertNull(player.nextMessage()); diff --git a/src/test/java/com/samleighton/sethomestwo/support/HomeFixtures.java b/src/test/java/com/samleighton/sethomestwo/support/HomeFixtures.java index c4ce719..62498fb 100644 --- a/src/test/java/com/samleighton/sethomestwo/support/HomeFixtures.java +++ b/src/test/java/com/samleighton/sethomestwo/support/HomeFixtures.java @@ -1,5 +1,6 @@ package com.samleighton.sethomestwo.support; +import com.samleighton.sethomestwo.SetHomesTwo; import com.samleighton.sethomestwo.dao.BlacklistDao; import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.models.Home; @@ -7,6 +8,9 @@ import org.bukkit.Material; import org.bukkit.entity.Player; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; import java.util.Comparator; import java.util.List; import java.util.Objects; @@ -73,4 +77,22 @@ public static Home persist(Player owner, String name) { public static void blacklist(String worldName) { new BlacklistDao().save(worldName.toLowerCase()); } + + /** + * Make every write to the blacklist table fail while leaving reads working, + * which is the state a command has to survive: it has already listed the + * blacklist and only the insert or delete goes wrong. + */ + public static void breakBlacklistWrites() { + Connection connection = SetHomesTwo.instance().getConnectionManager().getConnection("homes"); + + try (Statement statement = connection.createStatement()) { + statement.execute("create trigger no_blacklist_insert before insert on blacklist" + + " begin select raise(abort, 'blacklist writes disabled'); end;"); + statement.execute("create trigger no_blacklist_delete before delete on blacklist" + + " begin select raise(abort, 'blacklist writes disabled'); end;"); + } catch (SQLException e) { + throw new IllegalStateException("Fixture could not disable blacklist writes", e); + } + } } diff --git a/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java b/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java index b8f6638..0f42028 100644 --- a/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java +++ b/src/test/java/com/samleighton/sethomestwo/utils/PermissionOverridesTest.java @@ -200,6 +200,34 @@ void denyingTheBundleTakesEveryPlayerNodeAtOnce() { assertFalse(player.hasPermission("sh2.manage-homes")); } + @Test + void denyingThePlayerBundleTakesItFromOperatorsToo() { + plugin.getConfig().set("permissions.sh2.player", false); + + PermissionOverrides.apply(); + + PlayerMock op = addPlayer(); + op.setOp(true); + + // sh2.admin lists sh2.player as a child, so a deny that stopped at + // ordinary players would leave operators holding the whole set. + assertFalse(op.hasPermission("sh2.player")); + assertFalse(op.hasPermission("sh2.create-home")); + assertTrue(op.hasPermission("sh2.import-homes"), "the admin-only nodes are untouched"); + } + + @Test + void aMixedCaseNodeStillDetachesFromItsBundle() { + plugin.getConfig().set("permissions.SH2.Manage-Homes", false); + + PermissionOverrides.apply(); + + // getPermission lowercases its lookup but a bundle's children map does + // not, so a node found by one and missed by the other reads as applied + // in the log while sh2.player carries on granting it. + assertFalse(addPlayer().hasPermission("sh2.manage-homes")); + } + @Test void denyingTheAdminBundleLeavesOrdinaryPlayersAlone() { plugin.getConfig().set("permissions.sh2.admin", false); From 173bde85357c6ecbc2101e87eb92e13c23ecdb9b Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 23:12:44 -0400 Subject: [PATCH 40/75] docs: reorder the README and collapse the reference material The commands table sat a third of the way down, behind seventy lines of EssentialsX and v1 migration that only some readers need. Migration now sits below Configuration and commands come straight after the quick start. The permissions section was inverted: the node table people look things up in was collapsed while forty lines of prose about changing defaults were not. The table and the bundles are now the visible part, and the detail behind it is folded away. Commands and the v1 import get the same treatment, a short everyday list with the full reference collapsed underneath. Restores a short "Before you update" section, led by the blacklist now being enforced on every world, which is the one change worth checking before updating rather than discovering afterwards. Nothing is dropped. Case-insensitive names were stated four times and are now stated once, and the note about needing no permissions plugin went from five places to two. --- README.md | 278 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 157 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index 85d4a53..5fca8ab 100644 --- a/README.md +++ b/README.md @@ -22,114 +22,33 @@ 2. Run `/sethome base` where you are standing. 3. Run `/homes` and click it. -That is genuinely the whole setup. Since 1.1.0 the player-facing permissions default to granted, so your players can create and use homes the moment the plugin loads. If you want to take one of those away, or hand an admin command to a non-operator, you can do it from `config.yml`. A permissions plugin is only needed for per-rank home limits. +That is genuinely the whole setup. Player permissions default to granted, so your players can create and use homes the moment the plugin loads. See [Permissions](#permissions) if you want to change that. -## Managing homes - -![The per-home management menu](docs/img/manage-menu.png) - -Open your homes with `/homes`, or right-click the homes item. Then: - -| Action | What happens | -| --- | --- | -| Left-click a home | Teleports you there | -| Right-click a home | Opens the management menu below | -| Rename | Opens an anvil prompt for the new name | -| Move home here | Repoints the home at where you are standing | -| Set icon to held item | The home's icon becomes whatever you are holding | -| Delete | Asks for confirmation first | - -![Right-clicking a home to rename it](docs/img/rename.gif) - -Home names are unique per player and ignore case, so `base` and `Base` are the same home. Management is controlled by `sh2.manage-homes`, which defaults to granted. - -Changing a home's icon works the same way. Hold the item you want and click **Set icon to held item**: - -![Changing a home's icon to the item being held](docs/img/change-icon.gif) - -## Teleporting - -![The stand-still countdown before a teleport](docs/img/teleport-delay.gif) - -By default players wait three seconds before a teleport fires, and moving cancels it, so a home is not a free escape from a fight. Set `delay: 0` for instant teleports, or `cancelOnMove: false` to let players walk during the countdown. - -![Instant teleport](docs/img/teleport-instant.gif) +## Before you update -Before it drops anyone anywhere, Set Homes Two checks the destination is safe to stand in. If a home has been built over, flooded with lava, or left hanging above a drop, the player is moved to the nearest safe spot instead, or the teleport is cancelled and they are told why. Turn it off with `teleportSafety: false`. +New install? Skip this. These are the changes an existing Set Homes Two server will notice, and the first one is worth checking **before** you update. -## Coming from EssentialsX or Set Homes v1 - -Your players keep their homes. The old plugin does not even need to be running, because the importer reads its data files directly. +- **The world blacklist now works on every world.** Blacklisting always accepted any world name and reported success, but only the first three worlds were ever enforced, so a fourth was quietly ignored. It is enforced now. If you blacklisted a world beyond the first three, run `/blacklist list` first, because homes there will start being refused and existing ones will stop being reachable by players who do not hold `sh2.bypass-blacklist`. +- **`/sethome base stone house` now means a stone icon and the description "house".** A second word naming a real item is taken as the icon. Put `d` in that position to keep the whole phrase: `/sethome base d stone house`. +- **Home names and player names now ignore case everywhere.** `/home Base` always found `base`; `/delhome`, `/uhome` and the admin commands now match it too, so `/delhome Base` deletes `base`. +- **The admin commands cannot find an offline player until that player logs in once.** Set Homes Two learns which name belongs to which account when a player joins, and a database written by an earlier release has none of those names recorded yet. Their homes are safe either way, and it corrects itself on their first login. -- Run `/import-homes essentialsx` (or `/import-homes sethomes`). This is a **preview only**. It reports how many homes it would import and warns about any it would skip, and changes nothing. -- Happy with the numbers? Run it again with `confirm` on the end. -- Existing homes are never overwritten, so re-running it is always safe. Homes in worlds that no longer exist are skipped with a warning naming the world. -- `/import-homes sethomes` also brings across the v1 world blacklist, added to your Set Homes Two blacklist alongside the homes. Re-running never adds a world twice. -- `/import-homes sethomes` also lists any v1 `config.yml` settings that carry over, and the Set Homes Two key to put each one under. Nothing is written to `config.yml` automatically - the table below has the same mapping for pasting in by hand. -- Admin commands (`/get-player-homes`, `/home-of`, `/delhome-of`, `/uhome-of`) work on an imported player immediately, for any player this server has seen before - the importer resolves their name from the server's own player cache, no network lookup involved. A player the server has never seen imports with no name and is picked up automatically on their first join, same as any other offline lookup. +Older releases are in the [changelog](#changelog). -Afterwards, remove the old jar. Set Homes Two provides `/sethome`, `/home` and `/delhome`, and two plugins claiming the same commands will fight over them. - -
-Set Homes v1: what each command and permission became - -| Set Homes v1 | Set Homes Two | -| --- | --- | -| `/sethome [name] [description]` | `/sethome [name] [icon] [description]` | -| `/home [name]` | `/home [name]` | -| `/homes [player]` | `/list-homes` for your own list, `/get-player-homes ` for someone else's. `/homes` now opens the menu instead. | -| `/delhome [name]` | `/delhome ` | -| `/uhome [description]` | `/uhome ` | -| `/home-of [home]` | `/home-of ` | -| `/delhome-of [home]` | `/delhome-of ` | -| `/uhome-of [home]` | `/uhome-of ` | -| `/blacklist ` | `/blacklist ` | -| `/setmax ` | `/set-max-homes ` | +## Commands -| v1 permission | Set Homes Two permission | +| Command | What it does | | --- | --- | -| `homes.home` | `sh2.go-home`, plus `sh2.teleport` to actually arrive | -| `homes.sethome` | `sh2.create-home` | -| `homes.delhome` | `sh2.delete-home` | -| `homes.gethomes` | `sh2.get-player-homes` | -| `homes.home-of` | `sh2.go-player-home` | -| `homes.delhome-of` | `sh2.delete-player-home` | -| `homes.uhome` | `sh2.move-home` | -| `homes.uhome-of` | `sh2.move-player-home` | -| `homes.blacklist_add` | `sh2.add-to-blacklist` | -| `homes.blacklist_remove` | `sh2.remove-from-blacklist` | -| `homes.blacklist_list` | `sh2.get-blacklisted-dimensions` | -| `homes.setmax` | `sh2.set-max-homes` | -| `homes.config_bypass` | `sh2.bypass-max-homes`, `sh2.bypass-blacklist` and `sh2.bypass-teleport-delay` | -| `homes.strike` | Nothing | -| `homes.*` | `sh2.admin` | +| `/sethome [name]` | Creates a home where you stand. | +| `/home [name]` | Teleports you to a home. | +| `/homes` | Opens the homes menu. | +| `/delhome ` | Deletes a home. | +| `/uhome ` | Moves one of your homes to where you are standing. | -Worth knowing before you copy a permissions file across: - -- **`homes.config_bypass` is three nodes now.** In v1 it let a player exceed the home limit, set homes in blacklisted worlds, and skip the teleport delay and cooldown, all at once. Grant all three `sh2.bypass-*` nodes to reproduce that. Nothing is lost on the cooldown, because Set Homes Two has no cooldown feature. -- **Your v1 unnamed home is called `default`.** The importer files it under that name, and a bare `/sethome` or `/home` uses the same name, so both keep working exactly as they did. `/home-of steve default` reaches an imported unnamed home. -- **`/sethome` takes a description straight after the name again**, as it did in v1. Set Homes Two adds an optional icon in between, so a second word naming a real item is read as the icon. `/sethome base d my main base` forces the default icon and keeps the whole phrase. -- **The one-letter aliases are not provided.** v1 registered `/h`, `/sh`, `/dh`, `/lh`, `/ho`, `/dho`, `/uh`, `/uho`, `/bl` and `/sm`. `/h` in particular collides with several other homes plugins, and Bukkit resolves a collision silently by prefixing one of them, which is worse than not having it. If you want them, map them yourself in the server's own `commands.yml`. -- **`/homes` means something different.** In v1 it printed a chat list. In Set Homes Two it opens the homes menu, and `/list-homes` prints the chat list. - -
+Names are optional on `/sethome` and `/home`. Leave the name off and both use a home called `default`. Home names are unique per player and ignore case, so `base` and `Base` are the same home.
-Set Homes v1: config.yml settings and their Set Homes Two equivalent - -| v1 `config.yml` | Set Homes Two `config.yml` | Note | -| --- | --- | --- | -| `tp-delay` | `delay` | direct | -| `tp-cancelOnMove` | `cancelOnMove` | direct | -| `max-homes.` | `maxHomes.` | also set `maxHomesType: groups` and `maxHomeEnabled: true`. A v1 value of `0` means unlimited; leave that group out of `maxHomes` in Set Homes Two rather than setting it to `0`, which would cap it at zero homes instead. | -| `max-homes-msg` | `maxHomesReached` | direct - v1's `§` colour codes paste in unchanged | -| `tp-cancelOnMove-msg` | `movedWhileTeleporting` | direct - v1's `§` colour codes paste in unchanged | -| `tp-cooldown` | none | Set Homes Two has no cooldown feature | -| `tp-cooldown-msg` | none | follows the above | - -
- -## Commands +Every command, with long forms and admin commands | Command | Long form | What it does | | --- | --- | --- | @@ -141,12 +60,9 @@ Worth knowing before you copy a permissions file across: | `/list-homes` | - | Lists your homes in chat. Click a name to teleport. | | `/give-homes-item` | - | Gives you the item that opens the menu. | -Home names ignore case, so `/home Base` and `/delhome Base` both find a home called `base`. - On `/sethome`, a second word that names a real item becomes the icon, and everything after it is the description. So `/sethome base stone house` creates `base` with a stone icon and the description "house". If you wanted the whole phrase as the description, put `d` in the icon position: `/sethome base d stone house`. The reply names the icon it chose, so there is never any guessing. -
-Admin commands +**Admin commands** | Command | What it does | | --- | --- | @@ -160,15 +76,55 @@ On `/sethome`, a second word that names a real item becomes the icon, and everyt | `/blacklist list` (alias `/get-blacklisted-dimensions`) | Shows which worlds are blacklisted. | | `/import-homes [confirm]` | Imports homes from another plugin. Dry-run unless `confirm` is given. | -The three blacklist commands are now one command with three aliases. Nothing you already type changes: `/add-to-blacklist world_nether` still adds that world, and `/get-blacklisted-dimensions` still lists them. Give worlds by the name the server knows them by, in lower case, which on a default setup means `world`, `world_nether` and `world_the_end`. +The three blacklist commands are one command with three aliases. Nothing you already type changes: `/add-to-blacklist world_nether` still adds that world, and `/get-blacklisted-dimensions` still lists them. Give worlds by the name the server knows them by, in lower case, which on a default setup means `world`, `world_nether` and `world_the_end`. -The three commands that take a player accept anyone who has saved homes, whether or not they are online. Tab completion only offers online players, because there is no lookup for every stored name. Both the player name and the home name ignore case. +The three commands that take a player accept anyone who has saved homes, whether or not they are online. Tab completion only offers online players, because there is no lookup for every stored name.
+## Managing homes + +![The per-home management menu](docs/img/manage-menu.png) + +Open your homes with `/homes`, or right-click the homes item. Then: + +| Action | What happens | +| --- | --- | +| Left-click a home | Teleports you there | +| Right-click a home | Opens the management menu below | +| Rename | Opens an anvil prompt for the new name | +| Move home here | Repoints the home at where you are standing | +| Set icon to held item | The home's icon becomes whatever you are holding | +| Delete | Asks for confirmation first | + +![Right-clicking a home to rename it](docs/img/rename.gif) + +The management menu is controlled by `sh2.manage-homes`, which defaults to granted. + +Changing a home's icon works the same way. Hold the item you want and click **Set icon to held item**: + +![Changing a home's icon to the item being held](docs/img/change-icon.gif) + +## Teleporting + +![The stand-still countdown before a teleport](docs/img/teleport-delay.gif) + +By default players wait three seconds before a teleport fires, and moving cancels it, so a home is not a free escape from a fight. Set `delay: 0` for instant teleports, or `cancelOnMove: false` to let players walk during the countdown. + +![Instant teleport](docs/img/teleport-instant.gif) + +Before it drops anyone anywhere, Set Homes Two checks the destination is safe to stand in. If a home has been built over, flooded with lava, or left hanging above a drop, the player is moved to the nearest safe spot instead, or the teleport is cancelled and they are told why. Turn it off with `teleportSafety: false`. + ## Permissions -Nothing here needs a permissions plugin. Every node has a default, and you can change any of those defaults from `config.yml`. See [Changing permissions](#changing-permissions) below. +Nothing here needs a permissions plugin. Out of the box, every player can create, list, teleport to and manage their own homes, and operators get everything else. + +Two bundles group the nodes, so you can move a whole role in one line: + +| Bundle | Default | Contains | +| --- | --- | --- | +| `sh2.player` | everyone | `sh2.create-home`, `sh2.go-home`, `sh2.list-homes`, `sh2.delete-home`, `sh2.teleport`, `sh2.give-homes-item`, `sh2.manage-homes`, `sh2.move-home` | +| `sh2.admin` | OP | `sh2.player`, plus every admin and bypass node |
Full permission list @@ -197,13 +153,6 @@ Nothing here needs a permissions plugin. Every node has a default, and you can c | `sh2.bypass-blacklist` | OP | Creating a home in a blacklisted world, moving a home into one, and teleporting to a home already in one. It also stops `/homes` and `/list-homes` replacing the home's description with "Cannot teleport here: dimension blacklisted" | | `sh2.bypass-teleport-delay` | OP | Teleporting with no countdown, and not being cancelled by moving | -Two bundles group those nodes so you can grant a whole role at once: - -| Bundle | Default | Contains | -| --- | --- | --- | -| `sh2.player` | everyone | `sh2.create-home`, `sh2.go-home`, `sh2.list-homes`, `sh2.delete-home`, `sh2.teleport`, `sh2.give-homes-item`, `sh2.manage-homes`, `sh2.move-home` | -| `sh2.admin` | OP | `sh2.player`, plus every OP node in the table above | - The bundles are what actually grant these nodes. Each individual node is declared off in `plugin.yml`, and `sh2.player` or `sh2.admin` switches its whole set on, which is why denying a bundle takes that whole set away in one line. Granting or denying an individual node still works exactly as the table describes. Note that `sh2.move-home` sits in `sh2.player`, not behind `sh2.manage-homes`. If you took `sh2.manage-homes` away to stop players relocating their homes, deny `sh2.move-home` as well or `/uhome` gives the ability back. @@ -212,7 +161,7 @@ Note that `sh2.move-home` sits in `sh2.player`, not behind `sh2.manage-homes`. I ### Changing permissions -You can change any node's default from `config.yml`, with no permissions plugin involved. Uncomment the `permissions:` block and list the nodes you want to move: +Uncomment the `permissions:` block in `config.yml` and list the nodes you want to move: ```yaml permissions: @@ -221,23 +170,30 @@ permissions: sh2.import-homes: op ``` -Accepted values are `true` (everyone), `false` (nobody), `op` (operators only) and `not-op` (everyone except operators). Bukkit reads these, so case variants such as `OP` and spellings such as `notop` are accepted too, but stick to the four above. A value it cannot read is skipped with a warning in the server log, as is a node name that does not exist, and every override that does apply is written to the log at startup. There is no wildcard form, so list each node. +Accepted values are `true` (everyone), `false` (nobody), `op` (operators only) and `not-op` (everyone except operators). A deny applies to operators too, so use `op` if you want a node gone for everyone except them. -The two bundles are nodes in their own right, so `sh2.player: false` moves the whole player set at once and `sh2.admin: true` hands every admin command to everybody. That last one is rarely what you want. +**This only changes a default.** If you run LuckPerms or similar, an explicit grant or deny there still wins. The config block decides what happens to a player the permissions plugin says nothing about. -**Denying a node takes it away from operators too.** `sh2.admin` contains `sh2.player`, so `sh2.player: false` stops operators creating homes as well, and `sh2.manage-homes: false` applies to them just the same. This is what makes a deny a real deny rather than something operators quietly keep. Every node the config detaches from a bundle is named in the server log at startup, so you can see exactly what moved. If you want a node gone for everyone except operators, set it to `op` instead of `false`. +Take care with `sh2.import-homes`. `/import-homes confirm` writes homes for every player on the server and there is no second check inside the command, so granting it to everyone is a real risk. -**This only changes a default.** If you run LuckPerms or similar, an explicit grant or deny there still wins. The config block decides what happens to a player the permissions plugin says nothing about. +
+More on changing permissions + +**Why a deny reaches operators.** `sh2.admin` contains `sh2.player`, so `sh2.player: false` stops operators creating homes as well, and `sh2.manage-homes: false` applies to them just the same. This is what makes a deny a real deny rather than something operators quietly keep. Every node the config detaches from a bundle is named in the server log at startup, so you can see exactly what moved. + +**Bundles are ordinary nodes.** `sh2.player: false` moves the whole player set at once, and `sh2.admin: true` hands every admin command to everybody. That last one is rarely what you want. -Take care with `sh2.import-homes`. `/import-homes confirm` writes homes for every player on the server and there is no second check inside the command, so granting it to everyone is a real risk. The plugin logs a warning if you move it off `op`. +**What gets logged.** A value the plugin cannot read is skipped with a warning in the server log, as is a node name that does not exist, and every override that does apply is written to the log at startup. Moving `sh2.import-homes` off `op` logs a warning of its own. There is no wildcard form, so list each node. -With LuckPerms, the equivalent one-liner is: +**Spelling.** Bukkit parses the values, so case variants such as `OP` and spellings such as `notop` are accepted, as is any capitalisation of the node name itself. Stick to the four values above. + +**With LuckPerms**, the equivalent one-liner is: ``` /lp group default permission set sh2.player true ``` -If you would rather not touch `config.yml` at all, the server's own `permissions.yml` can wrap the nodes in a rank of your own: +**Without touching `config.yml` at all**, the server's own `permissions.yml` can wrap the nodes in a rank of your own: ```yaml myserver.moderator: @@ -250,6 +206,8 @@ myserver.moderator: Then grant `myserver.moderator` to whoever should have it. +
+ ## Configuration Settings live in **`plugins/SetHomesTwo/config.yml`** on your server, written the first time the plugin starts. Edit it in any text editor, save, then **restart the server**. There is no in-game reload command, so changes do not apply until the server comes back up. @@ -282,6 +240,84 @@ To pick one up, copy the key you want out of [`default-config.yml`](https://gith
+## Coming from EssentialsX or Set Homes v1 + +Your players keep their homes. The old plugin does not even need to be running, because the importer reads its data files directly. + +1. Run `/import-homes essentialsx` (or `/import-homes sethomes`). This is a **preview only**. It reports how many homes it would import and warns about any it would skip, and changes nothing. +2. Happy with the numbers? Run it again with `confirm` on the end. +3. Remove the old jar. Set Homes Two provides `/sethome`, `/home` and `/delhome`, and two plugins claiming the same commands will fight over them. + +Existing homes are never overwritten, so re-running the import is always safe. Homes in worlds that no longer exist are skipped with a warning naming the world. + +
+What else the Set Homes v1 import brings across + +- **The v1 world blacklist**, added to your Set Homes Two blacklist alongside the homes. Re-running never adds a world twice. +- **A report of your v1 `config.yml`**, listing any setting that has an equivalent here and the key to put it under. Nothing is written to `config.yml` automatically. The table further down has the same mapping for pasting in by hand. +- **Player names**, resolved from the server's own player cache, with no network lookup involved. That means `/get-player-homes`, `/home-of`, `/delhome-of` and `/uhome-of` work on an imported player straight away, for any player this server has seen before. A player the server has never seen imports with no name and is picked up automatically on their first join. + +
+ +
+Set Homes v1: what each command and permission became + +| Set Homes v1 | Set Homes Two | +| --- | --- | +| `/sethome [name] [description]` | `/sethome [name] [icon] [description]` | +| `/home [name]` | `/home [name]` | +| `/homes [player]` | `/list-homes` for your own list, `/get-player-homes ` for someone else's. `/homes` now opens the menu instead. | +| `/delhome [name]` | `/delhome ` | +| `/uhome [description]` | `/uhome ` | +| `/home-of [home]` | `/home-of ` | +| `/delhome-of [home]` | `/delhome-of ` | +| `/uhome-of [home]` | `/uhome-of ` | +| `/blacklist ` | `/blacklist ` | +| `/setmax ` | `/setmax`, or the long form `/set-max-homes` | + +| v1 permission | Set Homes Two permission | +| --- | --- | +| `homes.home` | `sh2.go-home`, plus `sh2.teleport` to actually arrive | +| `homes.sethome` | `sh2.create-home` | +| `homes.delhome` | `sh2.delete-home` | +| `homes.gethomes` | `sh2.get-player-homes` | +| `homes.home-of` | `sh2.go-player-home` | +| `homes.delhome-of` | `sh2.delete-player-home` | +| `homes.uhome` | `sh2.move-home` | +| `homes.uhome-of` | `sh2.move-player-home` | +| `homes.blacklist_add` | `sh2.add-to-blacklist` | +| `homes.blacklist_remove` | `sh2.remove-from-blacklist` | +| `homes.blacklist_list` | `sh2.get-blacklisted-dimensions` | +| `homes.setmax` | `sh2.set-max-homes` | +| `homes.config_bypass` | `sh2.bypass-max-homes`, `sh2.bypass-blacklist` and `sh2.bypass-teleport-delay` | +| `homes.strike` | Nothing | +| `homes.*` | `sh2.admin` | + +Worth knowing before you copy a permissions file across: + +- **`homes.config_bypass` is three nodes now.** In v1 it let a player exceed the home limit, set homes in blacklisted worlds, and skip the teleport delay and cooldown, all at once. Grant all three `sh2.bypass-*` nodes to reproduce that. Nothing is lost on the cooldown, because Set Homes Two has no cooldown feature. +- **Your v1 unnamed home is called `default`.** The importer files it under that name, and a bare `/sethome` or `/home` uses the same name, so both keep working exactly as they did. `/home-of steve default` reaches an imported unnamed home. +- **`/sethome` takes a description straight after the name again**, as it did in v1. Set Homes Two adds an optional icon in between, so a second word naming a real item is read as the icon. `/sethome base d my main base` forces the default icon and keeps the whole phrase. +- **The one-letter aliases are not provided.** v1 registered `/h`, `/sh`, `/dh`, `/lh`, `/ho`, `/dho`, `/uh`, `/uho`, `/bl` and `/sm`. `/h` in particular collides with several other homes plugins, and Bukkit resolves a collision silently by prefixing one of them, which is worse than not having it. If you want them, map them yourself in the server's own `commands.yml`. +- **`/homes` means something different.** In v1 it printed a chat list. In Set Homes Two it opens the homes menu, and `/list-homes` prints the chat list. + +
+ +
+Set Homes v1: config.yml settings and their Set Homes Two equivalent + +| v1 `config.yml` | Set Homes Two `config.yml` | Note | +| --- | --- | --- | +| `tp-delay` | `delay` | direct | +| `tp-cancelOnMove` | `cancelOnMove` | direct | +| `max-homes.` | `maxHomes.` | also set `maxHomesType: groups` and `maxHomeEnabled: true`. A v1 value of `0` means unlimited; leave that group out of `maxHomes` in Set Homes Two rather than setting it to `0`, which would cap it at zero homes instead. | +| `max-homes-msg` | `maxHomesReached` | direct. v1's `§` colour codes paste in unchanged | +| `tp-cancelOnMove-msg` | `movedWhileTeleporting` | direct. v1's `§` colour codes paste in unchanged | +| `tp-cooldown` | none | Set Homes Two has no cooldown feature | +| `tp-cooldown-msg` | none | follows the above | + +
+ ## FAQ
@@ -301,7 +337,7 @@ Update to 1.1.0 or later. On older versions every permission defaulted to OP; th
How do I turn a permission off without installing a permissions plugin? -Uncomment the `permissions:` block in `config.yml` and set the node to `false`, `op` or `not-op`. See [Changing permissions](#changing-permissions). +See [Changing permissions](#changing-permissions).
From 3399b5d15977609ee984857fc8245fa5ef5a928b Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 23:25:51 -0400 Subject: [PATCH 41/75] docs: cut the README back to what a server owner can act on Drops the "Before you update" section, and trims the passages that explained how something works rather than what to do about it. The collapsed permissions block was 246 words, most of it mechanism: why a deny reaches operators, how the bundles are wired, which alternative spellings Bukkit also accepts and should not be used anyway. What survives is the part a reader can act on, that a bundle can be named in the config like any other node, and that the startup log names anything the plugin could not read. The LuckPerms and permissions.yml recipes stay, folded away. Same treatment elsewhere: the bypass-blacklist row no longer describes how the home description is substituted, the one-letter alias note lists the aliases without defending their absence, and the import note drops a reassurance about network lookups that only makes sense if you were already worried about one. --- README.md | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 5fca8ab..0ee39b3 100644 --- a/README.md +++ b/README.md @@ -24,17 +24,6 @@ That is genuinely the whole setup. Player permissions default to granted, so your players can create and use homes the moment the plugin loads. See [Permissions](#permissions) if you want to change that. -## Before you update - -New install? Skip this. These are the changes an existing Set Homes Two server will notice, and the first one is worth checking **before** you update. - -- **The world blacklist now works on every world.** Blacklisting always accepted any world name and reported success, but only the first three worlds were ever enforced, so a fourth was quietly ignored. It is enforced now. If you blacklisted a world beyond the first three, run `/blacklist list` first, because homes there will start being refused and existing ones will stop being reachable by players who do not hold `sh2.bypass-blacklist`. -- **`/sethome base stone house` now means a stone icon and the description "house".** A second word naming a real item is taken as the icon. Put `d` in that position to keep the whole phrase: `/sethome base d stone house`. -- **Home names and player names now ignore case everywhere.** `/home Base` always found `base`; `/delhome`, `/uhome` and the admin commands now match it too, so `/delhome Base` deletes `base`. -- **The admin commands cannot find an offline player until that player logs in once.** Set Homes Two learns which name belongs to which account when a player joins, and a database written by an earlier release has none of those names recorded yet. Their homes are safe either way, and it corrects itself on their first login. - -Older releases are in the [changelog](#changelog). - ## Commands | Command | What it does | @@ -150,10 +139,10 @@ Two bundles group the nodes, so you can move a whole role in one line: | `sh2.import-homes` | OP | Importing from another plugin | | `sh2.update-notify` | OP | Being told on join that a newer release exists | | `sh2.bypass-max-homes` | OP | Creating homes past the configured maximum, whether the limit is server-wide or per group | -| `sh2.bypass-blacklist` | OP | Creating a home in a blacklisted world, moving a home into one, and teleporting to a home already in one. It also stops `/homes` and `/list-homes` replacing the home's description with "Cannot teleport here: dimension blacklisted" | +| `sh2.bypass-blacklist` | OP | Creating a home in a blacklisted world, moving a home into one, and teleporting to a home already in one | | `sh2.bypass-teleport-delay` | OP | Teleporting with no countdown, and not being cancelled by moving | -The bundles are what actually grant these nodes. Each individual node is declared off in `plugin.yml`, and `sh2.player` or `sh2.admin` switches its whole set on, which is why denying a bundle takes that whole set away in one line. Granting or denying an individual node still works exactly as the table describes. +These nodes are granted by the bundles, which is why denying a bundle takes its whole set away at once. Granting or denying an individual node works exactly as the table describes. Note that `sh2.move-home` sits in `sh2.player`, not behind `sh2.manage-homes`. If you took `sh2.manage-homes` away to stop players relocating their homes, deny `sh2.move-home` as well or `/uhome` gives the ability back. @@ -172,20 +161,14 @@ permissions: Accepted values are `true` (everyone), `false` (nobody), `op` (operators only) and `not-op` (everyone except operators). A deny applies to operators too, so use `op` if you want a node gone for everyone except them. +You can name a bundle here as well as a single node, so `sh2.player: false` moves all eight player nodes in one line. There is no wildcard, so list whatever you want changed. If a line does not seem to take effect, check the server log at startup: anything the plugin could not read is named there. + **This only changes a default.** If you run LuckPerms or similar, an explicit grant or deny there still wins. The config block decides what happens to a player the permissions plugin says nothing about. -Take care with `sh2.import-homes`. `/import-homes confirm` writes homes for every player on the server and there is no second check inside the command, so granting it to everyone is a real risk. +Take care with `sh2.import-homes`. Anyone who can run `/import-homes confirm` can write homes for every player on the server, so granting it to everyone is a real risk.
-More on changing permissions - -**Why a deny reaches operators.** `sh2.admin` contains `sh2.player`, so `sh2.player: false` stops operators creating homes as well, and `sh2.manage-homes: false` applies to them just the same. This is what makes a deny a real deny rather than something operators quietly keep. Every node the config detaches from a bundle is named in the server log at startup, so you can see exactly what moved. - -**Bundles are ordinary nodes.** `sh2.player: false` moves the whole player set at once, and `sh2.admin: true` hands every admin command to everybody. That last one is rarely what you want. - -**What gets logged.** A value the plugin cannot read is skipped with a warning in the server log, as is a node name that does not exist, and every override that does apply is written to the log at startup. Moving `sh2.import-homes` off `op` logs a warning of its own. There is no wildcard form, so list each node. - -**Spelling.** Bukkit parses the values, so case variants such as `OP` and spellings such as `notop` are accepted, as is any capitalisation of the node name itself. Stick to the four values above. +Other ways to set permissions **With LuckPerms**, the equivalent one-liner is: @@ -255,7 +238,7 @@ Existing homes are never overwritten, so re-running the import is always safe. H - **The v1 world blacklist**, added to your Set Homes Two blacklist alongside the homes. Re-running never adds a world twice. - **A report of your v1 `config.yml`**, listing any setting that has an equivalent here and the key to put it under. Nothing is written to `config.yml` automatically. The table further down has the same mapping for pasting in by hand. -- **Player names**, resolved from the server's own player cache, with no network lookup involved. That means `/get-player-homes`, `/home-of`, `/delhome-of` and `/uhome-of` work on an imported player straight away, for any player this server has seen before. A player the server has never seen imports with no name and is picked up automatically on their first join. +- **Player names**, read from the server's own player list. That means `/get-player-homes`, `/home-of`, `/delhome-of` and `/uhome-of` work on an imported player straight away, for anyone this server has seen before. A player the server has never seen imports with no name and is picked up automatically on their first join.
@@ -298,7 +281,7 @@ Worth knowing before you copy a permissions file across: - **`homes.config_bypass` is three nodes now.** In v1 it let a player exceed the home limit, set homes in blacklisted worlds, and skip the teleport delay and cooldown, all at once. Grant all three `sh2.bypass-*` nodes to reproduce that. Nothing is lost on the cooldown, because Set Homes Two has no cooldown feature. - **Your v1 unnamed home is called `default`.** The importer files it under that name, and a bare `/sethome` or `/home` uses the same name, so both keep working exactly as they did. `/home-of steve default` reaches an imported unnamed home. - **`/sethome` takes a description straight after the name again**, as it did in v1. Set Homes Two adds an optional icon in between, so a second word naming a real item is read as the icon. `/sethome base d my main base` forces the default icon and keeps the whole phrase. -- **The one-letter aliases are not provided.** v1 registered `/h`, `/sh`, `/dh`, `/lh`, `/ho`, `/dho`, `/uh`, `/uho`, `/bl` and `/sm`. `/h` in particular collides with several other homes plugins, and Bukkit resolves a collision silently by prefixing one of them, which is worse than not having it. If you want them, map them yourself in the server's own `commands.yml`. +- **The one-letter aliases are not provided.** v1 registered `/h`, `/sh`, `/dh`, `/lh`, `/ho`, `/dho`, `/uh`, `/uho`, `/bl` and `/sm`. If your players are used to them, map them yourself in the server's own `commands.yml`. - **`/homes` means something different.** In v1 it printed a chat list. In Set Homes Two it opens the homes menu, and `/list-homes` prints the chat list.
From f40970e284ff92c51ddf8803470ef7cdc30a0af3 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 23:32:36 -0400 Subject: [PATCH 42/75] docs: bold the config.yml upgrade warning so it reads as a caution --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ee39b3..b2cf4d6 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ Per-rank limits need [LuckPerms](https://luckperms.net/download) and `maxHomesTy That table is only the common settings. For the complete list, see [`default-config.yml`](https://github.com/Blockframe-Studios/SetHomesTwo/blob/master/src/main/resources/default-config.yml), the file your `config.yml` is first written from. Every setting the plugin has is in there, commented in place.
-Upgrading? Your existing config.yml will not gain the new settings +Upgrading? Your existing config.yml will not gain the new settings Set Homes Two never touches a `config.yml` that already exists, so settings added in a later release do not appear in a file written by an earlier one. Any missing setting quietly falls back to its default, so nothing breaks, but you cannot change a setting you cannot see. From f3745df751a4509d533a1d5159872c2e4bb236d0 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sat, 15 Aug 2026 23:46:42 -0400 Subject: [PATCH 43/75] update image size --- docs/img/homes-menu.png | Bin 99625 -> 76429 bytes docs/img/manage-menu.png | Bin 96263 -> 38165 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/img/homes-menu.png b/docs/img/homes-menu.png index e483fa2d4ac61a796e046612cf734e5734d364f2..c2fe0954619027069dae56f992dac8dc67470cc0 100644 GIT binary patch literal 76429 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

~Dsv(CDY7O^x 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 Q3;a2+e?bp_=ElAM0LtWnUjP6A literal 99625 zcmYIv1yq#J_xDoLuqXlwNJ~pgcL@qeml9Ia-Q6WAAuS*vxe_Z4(hbs*OE2B9^u7=J z{k{Kl_HYjKOx%0t=4WOi-zv-F<2=Oyfk5~QuVmCfAhb;o2(K5 zUp#Y`Q8w0jIE`+|ezeJubN(@k52oNG(9udMn@`j$AFe!(Vcct;zV4}Lc>i8=;XP%E zX1N|Ljz3V-)U{*;kt(z^NrkEkcA}+xd#~o)8Lir?u&PD zvN`D%^RUeWBl!k^&j!17kMKI$vy63yoRC0c!hom|8Oc6ghm~8_Sx$jS$B*`PWzPHQ z>c`-$c!wEj~$U|Y+xglYWA;Tqsswai2uhqd+i zFG`)MmXE;y*#$4|C;Z#ND*Jk{h`9EiAwXQ5Wi}j*rY4djivPc11JY~vC;qsa6k1I(s0@}ZpFV- z-|Irhk8}&2;{Q)fhbI1e;m6tg+?w+6RVfC(^E5;to%QUq`efYyHmU z{+lehtLeJi%(1y;tu=Qlk1tj3_amZsBvOn`)4JUEZtW!Ah(9v(gHEHMZx z|0RU4eCD^OW{K3p$h96Mk!}0G6|o+p8?R)ZA$bd_EvEhHMHbIzYYoK&TV~@Dk{af+ ztw)qlX;A~fw`bcKt#IhDpd&`%!M|0Er_f|VB668WZJk=i!(q`qjdDlyky78`;~Mh}%vXk(8uLOattl&oI}&@uY0? zpthZzi`^V-Ej1LvDlR!(o+tmHhV`Ssk*Qcka z%$9&%{oOLs{@vX&XpzV)_FGIE6f7B#!*HGJlhP6rX1KvPO?><#frfW`3=h=`v&_?q zKv-C6L$PrLEvD10^dN=;==s$S1QW!}kYn{9JE6;&n5d{|OhhabUeH!yT9$}a zISM*8&Z-rdVTW^Muz!o1qgpp3!Uas}v9D@Ku1PHrlyjMe@Li*$dVj1?&X?ox-)vo2 zh&aE~d^24fnqv{zPun?!&xpJDaW7i)2%u*jQ`UCF)x%yVX}TPq^OOl{PeUb$^@^+5 z#&`#+*hWs6q9R^lwHX)xZ!+hV&PcX_5OB*IFcA(-NK$M>=4Zun6@-nf1t({Y#Lb|95vp)A9MKMCF0d-5yvIP1WNam2aR8QA`N z-TU%5)SFn=|03M~kIla= zs2SGyp_ZM&I%-v(bew(UBgg%Zbc=`kip2u4168N(3p=g{j3(tnCz&9yDwZU=jPfg} zGTy>=7cYKuV(Gu`c+SteHc1Zc&wa{AQ`JQo*xHaVAxn|DqI-u0{5Eg`{yN5cF5jWN zE3V+~{(||;!>Cf;o1=*EAMYJD#LLQ{+^{C}*E{@gq41QoWtsT@1R3^dAM0(I{Ukwj z9_Bw@$u?x;babKQoak&(|1Dyk^);pRd_Krz@;)Bj|8}q|b*gY7vy_Cm|E*ERp`ikW zKLGu5yrum|T9|H5J0;QaLkbiW@818$4K@((i%t@d>p&kRaG5*CJ}nrg>u82f2~M-d zvlgsu3_tQ5|QJKS3!WAFt1f z$YT=nI9#H1pI0r{%5v`hqE~)i;ofY`C-qxTA2Qu~(ZwNldpbU8^%$gAK4ooil0IkO z+B{>U$b4DTxF!Hkq`xd}y(*22jHJd20S}pdq{?Y!r8Fj~40LOt*J z*wBN>+Uvk<)C#&^56@m+Ha2;qN-Qlcec*>^jNHs@&x#15_PD!*A2cHt0|QZLI@i|L zrpon$sW%0kIIX$NRKzaVT70h)=~We#-n?nrcZH1WoqPcz6DQsYEqK#{c%Sr=oC7s? zE!4>0-@ja|dWu9=wRBR810|A4A@T0+&Z{{#!b~^JxEIH%yr>Ld^z5w5k*$C&DoFD_ zqIPg{Jdg2PZuh18=p{oe1x^5?62)+Jf60b*CF)EXn116bWP6QdN6HfBlv&5+}Ec-TQy~_8x}!FN0+5s1pl`9k(UI{xpi=$tw^M*o3gsP zDyy0);Mfi?NXyJz>(%&YSfo$HY_D*wvotw+AuG7G>be!P(RpN)l)fY0i;rXzYL_04 z_K4pdWiMw&v>?1&wXOG*npBB>*I5nIva%-iu3POE8qS&zd?B@sMV4L*uG5un_W_$M z%yTqgM*`Yv)b7itcISkHSYBSHx*er*9VSvG3w!kFk@)Sn`1@iFpD=m&e8%EwAvKl} z&C@e$!)rYb?&FK2RbYEtucm5ST)n(zEgax3@qg8blS6FzE-2sQxmN|Z=zD-gIXSr% zfqiU5m)*{yrn!fQK&;6Di6L=UXQxd5=;4Qj>&BB`|5-lUJV&z|V!24H;}c|e^~hcb zzDrAjoD^gXIfUt%t+-k5ZO?6(7>t~1!+v&A3PMDK{d*k>VM6e>k;R1SS|26dvE%h| zU#@(YVdt$~O!4c{VRxJt76Tj`OQe_jf64pdAfCUQk&!WQ(q!QXnci`v(6^4^LPJAa zuyc2FdpY7pH*Hy#YfiUGDVeEG+2!nD#@yXB*5s5D%ta` z{;lr&2n;j(y5$G{4v>@mhMkJXdxeYFn_=X3hs8%6)mDT-OT7tn;e=EMm3S$H94(g{ z8QRswhw^jH-nDVEV(_FaU!0qtdx4nGn4&&h)?IEH+!AM2uj}pfQ294aT4kL`#^*o) z%iDpp`te}|-?p`$WF5FPF5of|`WUGHy0c6phDyfj$Xpy^b(=~VHE zSy9@hy4{qAH0}%8XA?`+%?=y{V3lj#jaeA}7NM>}RUhgQ;$es%PE} zF=>`ga`+uZBNn~w{r!=G?}LZMFz(PVIvK^d`1qhc^EFd&J`A{xC-Bii>?CcW(OtWA zvRZ)P^42te)Z=o%@Q$_hnAI{p`((+$(|=n%XSUYfkR!}HIrD({uS7fAZ*6851mVf* zDpq&|1kV)!NlJoi^vWY6v2%_V8oMB4#?y1}0QR)$+>_KI>hfmligJRe0cS(`^l8=9 za|Lcr&L3XW81Q$7hB@}1?EN{O)iMs>v}HLvQXtOG*pF(T0sK~a8q7e(6jl>gGhZim z*3Oevn>tmqkyB8JKvYYnck=V|p?Z6}>R}P^OjO#Z6cp$I>#G~<$AB5I!1-fWs}b5* zL}AOGl$j9^9z5{DLwF7yW4uB6>xkLrM`+SX2c8mteYi@Q~ z(js0gEk&C^M_$))FXOH)&{exG7iJNI6-_A&)iGM+vXA#hAEZ@o6Y`~Gbrec#D^Pa>*B zTd(zwXTZH|}UQ{5DHF+F10maIkletFv~oOc$f-xlm@nWE?wZuadi+fw}* zMx3q7N37RY`B;+`(+fdMIUyIZ0`QwQ*5s(1pvQ#IdK~S&-xg~`ly^jKW03x!<&r;$CUuAb6E-$5{-h{)URsYauDcw8E|Bj__o)#cDG*xAR$R zNKD>lrc+PjCZIv0-sj&exABocW^v|q>%+d2`i;&rTO4_FvdA%mu!aw3v0imx(S(JC z6&4jC_7^hX_4b!bOGX81N3>^SC2j>1Ku|v!5r32x;DTPVGa>ZvMciWJ=6^^@= z-QC?8ePtPi#NCBP1gYOLDPWRKaf%c@ttY~*)p^ARih}(7h#cgJf6Ly_Ilb}}(xZD1 z2}9l?02z5K-=T>gqP5UTMF3gVgu03^Kr7j^XXh{807L?g?stTD8JjD4 zT6J~x*4w#O-;*K!93HVh@>OCNOTpp@XK}cSMl0H7Iqa(30N;qF)BbM5-duMB(-Tm{ z4q(CIEa7HZB^b`NOM;?Dt-p#*I}PWyeKv(&VMKYCO8$+C ziZ^!S+GpJqSD~!Hz(61Yxb;ctb{Kq|;pd;WtMz@RX73JU9tI061wQgO>d}Cq88+wplxyibN#xKQFt6tvezO!3 z7S1T#nt?){hXvs#a<`Fs#9hZ$XlGucc+xv~?#<^rQc-@Z0y$o4 z&YEK({8#MYx^G~B_nU1ytmrK{=%SD}dbkLK#FRmcTg^*VL>*4mzj z>x}5ilx!DP7gT?TG zAYaj~*^Nn+b8F&AflJe5fOsYYq-=Z@+>2C^jG^~30D!Di=_+@jteuL+{np!By1^84t=%xculXdvZ) z>fHv5Uj$EDo5_?-T3IO0Y-L`r8{U~AXED){#~jGxwqQ(c^@ZXYo0e@&$W@o&P1odv zkJx|~B=n2&G0dbh~( zX%AOp|Gy*<0Gic~8h#c{G(T@43*4zLHw-uxFQNz!ZLcV;tn-e`;>2stc>_kzBu@Od zC`6sFFzGn4D>Z_@`y%QHPAq4pd)0p2LfedlRa4Asmm{LKGcgT^dR{(PQxq?0v5DvR z&T;NvzABV#_(alV)GvO%#15aDioJiC0(uc}fr z>+9>ytBaxMuEmM;Po6%-={Q7PQTJ^t>oTMuBRg#2g0s$JK6ud1nVFess{Id?SZ+}! zk^++I`y6)`a+{vot5~d6diJ{Y^tCZrHqZR;aL}UP_1=L;kFd`bfyv#{^3}HgR08%V46hSYwg=p!H(*pdtK8+#WW{yB z=&t7BV(!DLXBjnsbA;Ut!FCJuiKO0TOKB`7-r}|%;))qf=w;CNX}4ZYs`wp$mIP;} z&Yp(@Ng;As{3aYoa_VV9E|xIGJy3M*ZsP9?S81xNw?*R#YC`ZJ>R01QBSL@kt>u>3 z!(_O9{W=L`aYaDP=i+cRPTT-8J)*mzZY|2ibrkE~wEA<-4k+{I0jA;1X;wF6tt~GPkSw&GMgzJ#~ETMV_Ulfbi z?GRE}2kd)vxYL>v%ZV4GC+P`YM9nfaO@_^?doeeHl=H^9GMj=$b)EiC#pvr5n2_i3+J{<`gLI$cH3iU*sh@Lufbd=O8o-Wrc6Pu}g@*+b zeV-XeG*~v-)R*KfWtUrI++SuWiI>1@>f9R_yqf(|E~R9snzz+ym6eq%7Ok83Tc>8L z*nCTRs(rMvmgTqF--LJohADD^VmqVh> zI)uq)x}}qgh!%up`ABg=lglCi4zIl7wjk99G7#|-9`Q%$ZI=&Zu3)oJYwZ!nqPe~O zS?CTL)rADH>|$RL1Ol1cYgsxT;6KCkKg858PpM6}(EVxB6FXUBdqjLH*T*{XxJVjn zbjxM&W`n8?Ziqn&{L?);0xI~yPAXDog2K4_&#HktpFI%QHgf*b`X!>3&D%T(k0YF8_`UWzaUQgZP&>B z{QN^rmo}VkL4F|dHSqE}t0115JrTM%hHWdb+OG^%*&H2B$CbXxAFW%ur@`VV<;c6} zWUR$PFx~yQekq0AL<{EB+R^l7X_W!faP0Ubi(6~&zxkaUELXXwU-UbBLI=*AYh3y0 z;AdyAO9eOz*Wto+J4Ne%`@IvfK{NR;g8x1B``h~OK$os4*_@{lNVeVX8Gv| zq0PJwK-mfKdNBE^@pIY~GeKV7)AQO^Q6+Vn5O5ybfbf2u2_3E$U_dt|r4?7C$Q8qy zIa`{LsfN*yKFhd@kXnFJ1`f|Y00W)XDrE?X$lC;9FvHtzZT@51SBf)swT6g> z=>?yYp)6%g5CAugtBCs|766<^(|RwAGDW%Qgaf(WI^W{!4OnYGuaf)Ttm_cp^hH5f z=yf!OwQY)-tg4Oaa<*C8`(J_<7paDX4p zk87*5{37JANKqvjfV7Fb(%I^qcy)^dpP&jKFLFegfQA)tfQmKRELD4iEW-gkG)eb1Ad819mY0Xs)U;4k_HLn zRT2ik^zkFH{l&*n_b1eOT;@l@JG-<5U-{hvB)Zg?{c}4K&2)>#RTVv{+{FoiEKa;_ zKA^oB*h`;z(P)mm0dcm)o?n8P0iZQ}*VJ)}^-Q`v<)Z38J-&`;;7X<_{IgD&`wWCV z+IK$HXHZzz_PcRkRoUDg03T+@c>@?uRYRfWRnYuh1QyCiN%HqUi;s5pR*WmB3N0Hv zbRss72?uba5!go^E57r$3>_US2g6y)J=gb<=!{iWrw+1Jl?=bUxVUilopyiUa`6DQ z3WVx1d5Q55rT-@glZit;%AeT`fcOBwzuI>0rFA*ibG#0b5h}0ET)pxKAfOBa@D+&9 zI;(J2Y^^ASoWcm*8HPFznh3yp8G2PpGmn$>-ZV^|itP*tQMC|ATtt5rUdJH-W_Tm^ zV^+UJG%W11*Umew#_-$Kd?cgwLSDllNvHw$=YSrYx#7>BKX2zGmU0ownr_jV%lTCW!lUUpy9_nzDg3m=?9Zj~?C?LA*p^pIP;rVII4JI4?6IT40k z4p*_=ZaD&I$cXR0vmZs1<{75n66T8{yK`d^sGbtbdanXarqkwpLE4Ai3xuYFuI1xEl6j=OBu&Ay9+-o6m)nm6bql zyqJ-!SYW}+kjIG!AUeU+hfcN&Mw0XrI5;2~Fjm;tR-8U?-HTtcLtnwPcpU)lB@pXv zA7=cAoW$Ceq9+w@qidhqx4F5QXjW6JrDt6_xec5Gq}!b|ak~v)`fglY{8?}pJ%s06 z&$Zp%$yNJy;7{Lmsvr*sPv12zBuU}aZyrcv1mOP3S?Xpu%7OGcI=lb^ zu|HbSWrj}e>KcYHlxXxHe5ftR}Jk}BA+v|`&-}O5ezC}9&%74Smw6rZ3A*qP2 z#%L&b-;aVsAMh2xv0#pc`6`w>%q)lIn}wsHqt45ud&lh$UiOVj@fn2E>8WF{P?}iz zp3xWITpl1LOJ@WecRgnJg20c9_b=zOU@fb-5bqtt;zdd7NOmUMxCRr{P3gEccX4I~ zrt4Q}^PRO9=IQNjfz*((ZeC04NRLiA6J;~_>;YqOOP;AKGmhsDWL0H~>b4Q0LHLxZ zFl|0ll?sgQyY9yd**CUjr@U#5;>HbX{@GP*Fg3K^?N&lIzMkoc2pkYtCM=&AtT- z(15_+NTL@v)$j}D{R!(%&4%A>PhwmJ*rpn~#Jy9@)c^L%5mbCJIt z9`U)IVLX2(?&xD~ha-VMA=mQ&l4bi$P_WS+d3UP~hg^)u&8rs6iXX4Srnd^6tDPh2 zkk6o#LNbmy6$xiVs%bEVbNPrgtlF zb8|*uMG`#yJibtK{Y$&`?l*s{>)ei^`vAEC4?mZtX_|%=`#Wwe5zPx5K}fY(2f;uT zE-nCAsl9RwmQt^paUs|er7F#J*}GNz`1Y~9%~_NO5;PzVX$%= zp384F*3V(nCk#)!pqIbY{iiEWkkk%R&%05RAB3QzbIO6)wWt@i)v~2XF$=_oOwx$eDmIV_$!@<h&q%EJ!*ook`i0wnqu5>{cOx9z$huWBb9#TO?cxXCDH#zM3tRP3a61ty zDp3Tu3N3ygjR+M~hZ;omy$Sstfp%HQnR3jK`Od!g0e;YL?*joRo*0q}7iFwS%XHt7 z9YcOzfw%9{9N$j~St~Ug0EcLIJFOR|fuFqrQu@kcviKvZT3%cPGA_Ug$EArT$Malm zoa})MmA?9ovbenjQ`b2{E^(FbLk^{>ZaArLcSmkNIor9J6J*Ax;|}wXuZx)~(tV?N zM&5UF!y$IwgX)MdiA(LC*g0d0qAjMSVNXzrWd~CxRBN$9U43{8W=>D8;pP?xzDsxH z_I`bP_TB@#YZ|j#KYT2{km}Vz+TCr-eebrqG<*|&JxQWrST!?o_gKSy%;I3@{`Dcw z0}4Th6cA{H{ug${+YtH>RtMT%NR zv?3)QG7js?)C^OKanU%0W_;Jko3Ux#Sm*ex)?Q`mvqNE#<@!?#LV!hu& zDNi+UyiatoDRDfLI^GI1UKC=n_xW?T&a*ubIXmCC@iYtDWT^0NFQVk(gc}~rqsp@; zOIhUVk2`4Tk^k`%+N&ePjXvI3eczZ+e$ssJS-=<4dLu^eZl+?no*w=3DZ`s(DqNap z{ujcr-_=TEdirc;`KGLh%S9lvs`Ldk@Cf3sNEQ8Qu0n}vQMc31K~rllx9!7(1ojNr z;d$-LLW^-_eVg5<3WIV@WnOw`v}@Z8^*i5RpS^ZDGI8pHBuhLnTT65lzSeQrWmT9- zj449ZtX__12sc&19(UeV$Lb5hQ(&ABB>VF7N7@%Z0g5K}gI2zgqHja!EM8o=gq_rH zlE(Zpu|S$z*U=F{pPZLIImTlB`YP-zg8eq|m4u80dO$#1^b-1suSyM`Na!h55Znwwte|&HKZoViraE5H zDHkk^=?4ChB*V&k#f1JlKuP~+gQu0!av6!!z#sG8W5#`NBZu*e!A?`;?F zt$f(SV8zg6BhtY_M>hqOk-{-cy2R#znfl9Y0_(=8+*Gs6PrStv3La)JGGt`dZa!n8 z9>-aw&Rd{(>AHiA7o#a}mj;nBz5W3d@fZ##PxyT$wwuNbyJRO=7iAn2a0K7a|M(qs zFhVLy>x4^nZ2v+m)eCYf??bzmopJ5;LfAfrA;8Np5AE!XLsUf6{dm9e;K2NI!akv9 z0jFTR;#xe9Ia$R54u}1(%IOFsCo6$itUuu&n4A0wt+1zuyYX1@hRmO0+wNNI2|h$( z9T^dd9_NT}scPugsO0>sN8%Uui-JN`o*$fo)~_EV>`u+T4REFPaA`ces!;=;0aV$=|f2^iWDj^up2%8U|~@t zNE6k03>Nfgn7GF)_YO;D*|3KjB#=6{yFf%Pm1Pty3#4f=1F)7d*`vl;Ivzj3{>`p4 zKadA!q3h2hMorcl7DCN^i=_zOL3O4^)y#j9XgNF*uOyi>pdhEUVTgsI*z}f>jR-^X zt9HN9D;Y_$pL3Kl@u-wES1v+DiEt1|&tyZ|Ba11jDUtrx*Vi(I1~uuL+;L8uiTDRQEP7|dgilDM(9e)PPHKYgJ_fL>_6nCkX6%3J5Yd_~S zxwa;wnxuyP3qJ|l15LpVUZOCj4T0VM?vPi>j9C3XzPIQG6g+zkZeDILl*U=#w~V|# z=lmnma&{PqmUGu>xS{Jey#T`reYQ_kf)4CwLKMA#KMaK_bEuDAO#z)!A&@h<$XPZP z6gDpbjtNzuPNq|n`c^11W80;Msn~^vmK!SHaz1nyh@wqaFj)$_OQ9V|cgB|?W`m`X zLgfx_5$$(H&7;<7up3}=g{PJ|~Z9qlV&-Nhl_DYD4I&wImzD1pOBP3B_E z7HdDUzJP5smSJ#O6o^1CZg}K#{BO&k>qt^XD3z$2jgysCsE`A?TAxzQe?C7!A%Ur!f=>TbLidqM{NlL0iTyW( zyYWv{G^|mYM)*tDIW|hAV(HC&4sjHW0JL~&)kS|;FzELic%t9gdx-z)hHd^{U;ASz zmZJ~cZFXyn*mo$JP_<%BR;14XT9z_GSVhcptuvB7YIb4&bYeFfj(87s9e8cu;^}5r zkzuB2;Au=~P$jR*DCl}ru# z^-3|4cE|0Lih~^S4hT$JKvztm8bCX$h~gzFo>8cvTEbL}kB<`s@x2@VW{|$hg|!_?SW)D%~7XsceNa*wf;t@i%#JgIBW zEzAp>SKpQ`gi_{8KB7dgw!Ns}pKi6*N8@$E@7 zg50{*aayz|_8HY2uh>hGmCT)k9hm#!AsRy5WzM#iLR?T#i1*@H-#*RyaL>*pfMJRam2#e-shfJm@BfG@3GyJr}(O1Rawl~UV$Cb%olY8Ja z^ECLBhU*vfKoT_^*1CmzZ)AaM-|R_ZuD zW#-Qj&6&$ zcA7wOxMOIqLU9na0r&GzN#Y_WvlQ8S>iIr};|lIOG*rn=bQ&3SiDg{A0L!2~)c(+1 z;T?zGe&*$hW6~+jLcL;og{mM2c_w_by*IQkzSUsrttF_)6telEKO;hsWKc)~(R@pK z7@)s8UXWk6UQWvi9I+CG|Vlu(etg6|%tK=b%yJO)7pptEKY6!fLr1YK+bKQg^w|Z`v!rocj$O~$Q23Sj?yCk{febU6D4Bc#v?s$E>oPlIBI@wM$&9co z*#k)l3I3*YO|@dX>h(+$81N!!`40898`0RQGz~cW!fRJlbH#BLuc19WGh6Y|TC)0x zgP&sTDsxQBx1ghTaZAaboUywidYgl{=oK;&5&TcaL(%;8m0s$i(|^s?-gsenaGVv9 zS-qA?m`m@(iG5Kwp1o2uDIr1M&FU^F*jW~LtEcUG#~ zc(Y4-hXM+EMSHIF#F_lAclel5WwR*fGt5B}TiMjC_UW1B<3d?sf^GB)7v=_mP964ZL+U41+DF#7AN)R+9r^3^v5X|WqcMDEJn?3 z_VT&CPx%G&a;Nrk?eVwUvX<2gwCm;U<0V#t1&5RK(W8V6`vyz9pYKiS*B>v5RLfMc z7Wh=UFc;J|UMn4ZxV+0#_%-)=7xcDV;~OzT{N_9Oq6^~Z`*U=@%4|HAQd@Hy%06wg zRU;&;g&N|Sj{oTtm$|HR`knXCo9d%TmD`20MmG|(++@G5@>-XM1-tZ$S|dC0B1ej8 z`D-`v^Ug92R3a(Dq>O`^=RiQdX$Mv%RFf@{`>L0uyGt;Wh?=1tK8b5#CNy%*VJ&?_8OLTTv< zmTEFn-;b}XeUVv)&*dYeSUvX!>~Y(_&wLcHp1HdBK^zcM%(sspCYnFPSN&DVp?$1N zL@@JlFkVu7<>7jbP7Zr*KHXAOOVR6=y3<>Ab3~~BiwGa>JaCdtm8l~VHW7rvN*^EG zmbx*Vw?z#q^M^l?2rLPTdW$6!X+&C8Fx=QrCNGd9lRVEoy8MatHHTsddw+b$@5i*b zOt(BAOcqw=S*6&{O`VpiJfnq~92WSLf{DYHqpW@jrqU-&;h=OTz8?s3=sEe6?b1!A zjw2cMoh?dYM-RFUU?!-QOiGpKcl$DHYzp{j!ON!_Cl&UhT%w}ammEGMpgbmY(mGUS z4efHU$@sPLYvCCHkfScfnJeA!a^jV}j;)iI>%w=f z-;%poq)JBah3vFU`z;Ub^M?)8QWQw9A3cu|Pc|m+ia@2@iE@0dWWhe$%0u;iJ0W#B zPNV?23>M)I(mo(-u&uh>l_rECes>TyFEfa147+Umx(vO3FdI{r;Wk~48QMzha}q=y zujUoFKaNk70G3iLh61-=$)sh>biI6$EAa;{MNq5cfR>X#pQR#3e3oA?@GexjI+q(v zt5PRt3UD(dD>r&t|71?4|1}oR{>I?X_mp0Vy{7YYt)gpr|6@|^_szjr;N=32dclzs z<@6}m!*4+mnNZb44QuE^oao|K4OPQ+OL&f-lR$|L`BV0Rhh^l&^!3&C(5GAvmPy8( zutSy=hY~aPf1)h?+WkbgjhipgABDr42hvar`n9EOQ8G6wHI&xx(%N}j$85+fAi^2z2-&Bm%1%H zz_sxKni(QVO)R2~Y`nK)2^<Gmc z4PDZ}_(|nZUV$qk;U9L{%!iV-q-sN~L zd7M?xMQb+~+^jR7ol;(0cYJ_DPrDJnsrcF-wOZe)(2SzNt$ zKI!|x2e*k=f{eTNCnX--03H@Ul{LT(*3dxw3VoupfB zSJfivo_#1wHv*PrVjy}f@emB6rv(kV2*=J>nxmnVNJ<0*JK}+!2+F^T4YQu4$0MSC z0d=kT@$<(^(yEf+dbX@=B~~Gda*bp#l(R;r01ALj383FXpsxa0Afu|uhY9=XO9#je zdtoELk{iUWFH}j`eF_s(Xefm*Y+V?5Mr+9W=tspv-H&3AsN36GA!>nb0iP>rf=wOz z_D+XhsOFE1*3 z=0#gf1mfuHyOF|2#t67?mM4ywdNjhm4#amyN{=<+M5TNsvr^WKyY^u&G z);n;RJRzlM)xk0+9?Dm?fxEz~KfUKwOic=l#OtG5pTBoX33;GoETU0^utEYjF^I}X zMRk~6k->y+LX^c5RXhaf5_Xj}Uot*)rM2_qSPrO&z;}Odeo^6;mN6z1JDabD-AvWG zlHqj9m0m7D{P%=g56V5WA)!thBy1L-0$?=9}@1FA)Sxy(nIJrB5vX1-j<+}eWdxY8^L#`|@i0p#E$rJ3q_(Ij`U82=ar61Jmz*9q)11 zX+8KDZ?L9eKx#>bDuul2>ELG-+9=5UQN0BLC0WG;r+uJ?D)Eh%mSh-P2E{0VP0b*s07^GG>sHPvTpp=4mtdXo zf#o$F^L3s;VhIP{K(?v8i%bL=3p?0)GRk>kPz@w7pXrLO7g3ojNwd~rc+-=&;CD-i zG;;lDbn?@7GVrcu(m>4@xOM{Eda)&W9FUkkV(H|t1hEEvy{7z17j1O(=FCx9Ka#{O zu`SzsfEJ$SX-RN0>85;H#WAq!w@p=aJ;;`F7fmR&wy}O4kI{acQ-;zWOJ$uG(jSXj z<{`O*o8_4kqPW>mYjqip*32DlP?u_+dk7}VHXtNM`NSowDzxfj_l%YySZv}ch@R~2 zW9bB%$N;R5_X$X+dK|Bfh2QY2fUUCP(l?3pi*Fm=)v?li*C*5N=MnuZI0EDD&J+@l z%%djTJov149S|rGHT5b$=>E;o+S)`yzC4xhW+Vi8qHXf0h0eoz>&=(<{P@?~9w+EN zrG$1ZfSD$+8dsE_i@g_3X)dG)T=#CZ_n00&$dgj6ayt2zc7@{eYE2CF#0JNjf(n&LeOOmxOMq^YDn1c z^!?aVcOdOdAKJjx+-p2h{;j!lLtj09RF>4n06MAbPx;<*XK=2TeDhsUA7lDV)i}B|Mx?_@4l|{ zbnDpDRwviE zWn_43#;3OM1vb0I697MM_!Cf_Aiy{tDT*|x?76DnS&Pxc$4b7Px@Yy0bC=sCK&T4! znb~_asdu4G7u-+ruf%vEH~<(`|A$J6%&Ow;0t;%xUs}V)*biHNlf8HS@9Q?)60W3C z{IA5e+I%xNnGKI9y2xmpXy8&*L?L+eVUw}AlYkZrA1-LISN@eGT>YNH2n(R~M*n+P5rO_i$07ys& z6sDxl`I~mOy7Y5}es89C@MRJItT}O?ne_Ei8C_Z-vuyIsk?fnR`u>kL4OtsJtfw$* z^A^FZ6-?F7<>uZyRl#Pye7UIXtlYlsdP_Cs-EG#_@P}$6sy-4;KRTNyG`=Tz6|%n>^1M`sHzew`Cs8_zsUyI^sm+EhADghic2_q28yC(7Y%K1+M-QFQ zk=7^_v~b*^)At1rlI3{83ORpzX|J{tAA|IH(tMY zyw!ggm)r|ue~>sXrvJ~QAB7Q@kMS1Mi>*r}uzL-+i$m{CrW_5JDE3PB{0?`PuhciW8bj_EDno9aZd{W7`|C~xPBa0WY|v=@TC}8EM2=Q<7VnOJk~{I-V9)PI+*+4Id)r)hnbc>}GjS)`yTnw% zctr0?NO=Np5@bK<-%*ngbSCdWiyw;-d_;oiE~$yv6rn)0^vG{sR!$BScJ5;|Y&;kb z2=<}{g|64W#6HZJ(P9mEjDHp2^qh$T1y~}kWBbLowWr6WU^i}Nz4`X`oZeKpNnc9HSmdJQfaF5sOVf7;cWkurdVLX;&s!B)mLb}BpB3i# zj1F}yn%y}@o2IPKZ#ryZ?dBI$((KR>Y*pCaCe*WM~LzxTvG! z&Goqp9`o6P*?nT)en`>6;2^iQrG14i+!E3+!I(rU42d^>#ve8+p>MRMA||<0E@%Gx zp;qp&1P_Ik+mzeA=0i}7JHz!4w#dKtXW70Sr6RGh?4>n971%LaC8FYH>Hcc@IbTC; z47CCou{hscR<}Q!YCMV^R{pMCOc}eC_>>@|g+ybK2^d@j9+KPrR#_0CMU6E83Q_l! z%tJ6+BJ#Zfvd0g8 zAzCw0E5W5BoA!c2b*X7_@0{(u`t(Ub0Q)|vVsWkjCLLlvKKe@O z9A^%PCX@hj@mhYo2uZI#^{Nj}cQdy!FmR4M2)@hQxg)~(8ogNM`#13Ko|5>ecqyDu z4o|Z-^9Mp|`dfjqj~S6H#mkMF2|k-Y z7D}Q%)E?E!gvp~?l`1@OBgnPLXGCLnYcPQEhNFzm@Z%p`pqL>#q-6Td!-SXM4@C^K zpX;UwS@9wg@^+Jt`x^UG;nb(!|4yBk5{ka-8ma%-gi#OqL-*nI>p#yrB5ntzt0 z%`1=3;vjSUPO|#PAYY36Y*5Ck3{!kYcJ}`6Bq6b zMtXH9>~7^?iUSDmHKHNFvo6Sp;ZMD140-bYZTbyRL2Es8q1oGsLP0^RoPF;0ImXC1 z#nvz>%SX`xUGIY#85xeS{H*iIwRoreWM~pIVw{Zl5ckYy)Yh=!S)$wH1PUfpzTTI` z{kn^N+0%isU;I*H-)_rXU@z*tooCe+Hj0d9e|_EC>Lc}It|WR1K}Z}%qd8)yJ>FKe zip_C_#^ZuA-GQXw2%YGH=q`9{zs|OD3T1|`YetsCCFPCA{q~feyv_yw2@7s1ECP{C5FAJjJ7WL+IePxZ(C;)8c|=J;l8?YCh?pvK%voAqeE zISV8y7gge9M?Z<@8A7lkZy2jQDS=W~0PK9_^AU9wD0vvq%2c6~sS^~JSs;dJ>oMh01gPX5&H zvw2?=CuQIYeEimGcNt(SGyfRbNlAT>^E+_DUP$I1UB%*FSA+ZN@A?V|ybHgkG4f+_ zEA6A3Fp?CLk`NMjMVy@dkwNzEK4>?t8v@LAP1$P>=+n6S87GvU+Djsm!=G*9pbaZk zm>!@VqiZcDpH}?f`z+(FNKz!~95)OQ3wT$DhojOj^ol35gl!D~U6(I;8~s-FU9Z!s ztZO}ARkQ_+l;K66Z?~-XN-2IY5zzz$2P&V@w_0(%oYHQvR^UH4P6^Dvc-yGLji?T4;UMB_#!vSt3#Ttjb%vd1<9xPXLfL!oI`j4_JLhpz@kWO9{aR$B2olVIVbCW~sNjGT?E-m@=&{+jl6j4ml z=UeMTF73IuZiO}wsF?mM&9<>ye)4w1+c* zCvwfrVcoLavXl9dYyT*4nLvvBlzq!Na~sUYcU}jPf!}eVNC=LSyhScEBT4}=FTfSd z4tgL*r+5p}nePg%uKZi*DPL}>M^n`lD9m8-{W~F9MK!E$uh|8g)HdNG=%^j#qm?m6 z-+#qmTW_yxBT;uW(Vo1%{!iZHy~Fci?7{(DU0qY%H;+;KW05Bg6}|^cSw*Gyhqfod z7o6hb%`^K?zB~&!tsZ;3^(CXX&u6ds0Z@-%yQof~m@A3slM$F4z+r00uij0o7%`K_ zd-5x#x2io_T_q90oX?M+4@U6D^2vcul3`?kL2f+c-2@{=$?KUF&HLq`^Ape7*!zF` z42x1SMae|n<934`I$X*_6ul#XSC?QL4qKD}c^7%|%6x-!iUJEv!AUV3fSK@`*J5|H zgu%!s-q^{5K|dXQ;rT|a{yy7{v&?_%7p?ZFoo|yip>>DUtXnYtS`FMhZgL;Zse{5Crkn~ z0fr6rg2M_wJq*6(EDEV&&pddY-}ehrhCdDt3BSi>34<7zEqJ)oZ69|;{dskvZ7$U| zyv?!q_JPyw)*cX$SVAxU(%&)TV&ttv=L-s5zBwF#{bIx+pU+uu^1gA~^uSU`(X8+6 z0`sav2CIbh-o|g#jDNw?@2!GFL=YS&=A<#5D+!Pe1u)yHA0O?(e>Z!JBn@cy zbzGJ07FQ)!HPf2Se(igt*HQ2E_hER$M7o&F$uQ|rGXuCIL33uOe5B`!vlJOCPRE;mauJt0w{1ZYfqda;t^Xt#1lccASP>g$6d(_Ad+X+ z^%XR)fjCrS@!BtUdU^TsPg+yc#ZCVs>^?_8#?D$1N7wfEdpwd}d%oP5*cuQoFLYd< zV5hHp&E08(nULpXWGcP(S^)vAT=#S)Mq>ApczAm3O(p=iEP&*7@QOj>9|<*9G*}4C z23JxX6(a!f=RzmcPM&mvrl`YTY6*PuF;dunqv9xYt}`9hpQthGSe zzCEWC8Krj;My^;`KS1F!0&?pxyqwSaUkuN!Z_aV=`%$UPy%%k{h%tkr5Cu}{GKL6$ z-5_I@FVZ{z$OSdV4(M{JMWdqc8)_26rZQ~;M3wSw_s63VFM0ijKm-_xsRr!}wR=;R zPX^OLH=E7Rmhpwvyq1hmR8|Ioh@d8g94fQ&5@zZfo0R=;hc`Pqkr7cv-$9q34KtAWG@#4P-xyG1LsJj4fOR>jXTh}9?Wg-W8bgI4yC`FKmOTB*%45DhyIvR z_M=-nmYMlmeF}-R@48t=%k@fJnU75hsSL4s*mJs~KGBEfvIg`E)mzQ>u2TcdWA7#W zv;Ga}cg4}am#+2tcQJCo39=PMap?k{(Eq#x+^qV1^Ikr>?u$C8TG3N+p#JDoeh?8y zWwp%`#GL<~ls>DM00;y=0Lhcld=fpT;i<2`oZs@UGVFeF`uyCVhSR3PY@x~1&bnc> z|MK5Mv`)*!{?px^I?0b4E~|(^?h!1%sKIrCw$I%#1u=;2F)uH7U`q{;+0;)jF0M0g z?;7hc*q$nu{X`+p-Ep(wayg22X!hJ3g@CZ26hTSJo%*}Rz&Fc}1I$hZ`|NA+Wv_U3wi|q`XcYZ77xoO5DP?1Wr6)9Ioqa5y>qMBZ)ZtQ*>J}z6zgNz#YsFa{gVyI!pbWJObaCR%KIcE8(dE)5EFx*%XmWFq}(Y(&jTOGt$ zZ>I^pX;r>Wb?SM_`haRkaikb81|Rei{o*_0$%_8sfvaGZpY7pGjq%hw&EOCQ=VzJ= zl3PL`x~IR?p)hsqWybfA=g7@fYyEDG>A&-zjy8#dCtvF&mC-)RdSN~q{gFJ+Qv{S~ zB*dbX0{xa^sFURhFNLE$0`_nxjj$c!p}?}~D@VY%W8IBY4ttSN6%k{4Y!R2KWz=L6 zVqmQ=jzP@LVh2;9ZytPxg>XEgmssaYpqB{}i^~80U5?|=&!3+gTi(pq4T$?5uMbd) zX0{dU8m8?1tX{*S%ltOd?66g~oBiL3aqp^qN;>YVWKJ%8mqYH0MvPT0hbEy8g99Ez z!p0vOVkE*$8_fb&wr=GpB9b``?fN+?02Yys1ORk0kBi~P_!lKL7VMI{;s%Z;jnYDa z5DGZQUO6kI6diK66=%3%yX3&zjxakn6(tURSJiYENsyf-=0Tn?7?#gc20z*ixl#Pn zvK)N9V=g;uZ(`#NAp0Rt)an0NIIOneMK3vHE^}1j>18gs69;X9ZvnC&aA2&2Up<@~ z$X5XpNInGUumdcd5GT?OMzvzh%w!xDxE#S!J35%OeV_WI(4>zh`R_u-Mk{zKlI4N& z<6(OyOr$g0GGHn|wv2nZ>4uCR0S@=v|0uhqNSfy zU}d3~@l9eGt-D1Eg^~o%*;U$?FWGx{ip6)n)q^>2Xvjjb*VxGnL3Kwx1{fwoV=*JX z!4T;(9~igFl&ogU>znjMDp znlJ}9-?40*tLdHk`_r|9L29~EK4*u+QXOh_I5mn0Kg+sD*0m?Z7ECv{NxIB{AH1;o z5OCPDa?iw0RtIz$_;~J;j8G{JFy_E(>iLzJHug789xf~%c({e<>qLVKNdy3JIK$I{ zF4d}uZ(*xb><@d_5K8E-$wGi1?1l&2^OMiSAU286nt)U#0@>|l`lMI;d-f&0aK2}6 zi>}E$<;WNGhH5SM;FqeVfTrjO{k=BPwjkNF7vY_T`lUKa2rNtl&2ih4U%&6u()W>m z$4f#6uT*ps%L%}?II)b1>S}(c3jO;p2`lngcxTznHglYmRZPookjbQV)6@Cd_xyUt@j*O~G#C>sltB_CX7TFzvEr%!Pqq zb+|dWTya+acB4=`sXbLoHiT*i|5!t#IB(mc(*rix1k?#mT zkH}*b7*&RuAI}kd7T3A zNuHYY;t*zb9m39oRoNDu%lACtmd%7n~ z65^gr>JeX# z{{{`L^-qd1MvTtttpPZKBT8UQ7aXoUnd$l~aMM&G3Y$=IHr&958U@OxTU$&6Uq1lk zJJJJc)Tui;C~+$r1|N|rF?yBgyQ!H7%#$D74&uF)NYWcARd#~IV2t(^uW*6T4Z7@f zcI=>fGf3uD2sJ;<@hwkx*sfPLug&-HJNA8g9^-XcWvDQ{GUIVR7&+y@R{Lrts3%&p zRLLR2UZW9@EM8tKnjFo7V_=sf<8Q(X|L}*-F4oelm6qPzZz1D%r~zv`MNibA8KHE- zo^1C19EG_gg)<+-eEcWZv}%MP+eZ`bKdH@KcuWvNTqF#0-ta7-;kLp6gG(0Z#8jba zv0f)iO*i1LV2im5qoSImHIoJ{^Bo>iNWIQd>uTTWHBV^S{kv-xBCU)PnONZV+FX&r z1-k9+C%Ll%ASJhb4DHWkzvkRAq8}guZjvjertTKF2XgUuIEi>FP;v@@#0${x+`yIY zxeh=5Q^gLhWu15KiqA)A-K794s9*a97mKZD-WPk9&JKbA7P~-fuDHlwl4rBbia4L8 z2G#XBf6asrW6^#UgNLmD5(+`@`m|_BU+U2HVUvWh9I;ibUOqEREzR{9Y?;tM+s^Pg z8cVtAeRA+`$;Fr8>Or%duaB!9mRe|Ap%*yw#mqK_hK2?Uq&3;kp1tmAZ*TvP3%l46 z5&{NlQCx19Iekl3guHw%N}*MiUB@#t`YUr%LX}*b;?svXPL40{ZbyH%PSMgbd=`_4 zKouWgCO-eJTh&E4+7i9|m??asF0iEWz!n@KPt>7w^}B{MepD_ge8GoA#^b`3o}I~2 zee4{ww=rGlO+}r0;0DB)ma$Ti*+mC2?T|XKMnI9%hEPUoM;OG)Qni?y3B3k!gHe_I zQFTz28zoqRSmbUT95>akhFy^2Ond!BHvluaYJYv!&bxscv9sE)mdjIu65gG}ZPg`F z(qEB?EK13af0oNPi;t^yO{fS5=hmfAE%p#OsQ^?+=N7UoD=k*rmm2kp9WJzI;;4pP z$$@$x@hk@Mw1cNa4RQFh6iE_B=`^o}^;BUd|MC>b=};if{QI=?#+*0H3|9FszUrLU zFk~N8m8*F2oqF_V))Jf~loH>cwa;9;9t7u8`Uuyt*W;Mh^<)`t4JJI;brTTK45oQv zYZ{44h^eG)cCVBaUzA$!X#_~ehfpIWBbNbgfN%(CSp7;P{k>0T3zaEBSM#9cOvwC) z`@`^SIzX%Z%{6UI@Ko>%WE9XgYbTb0bt=O20Q{Rc*A8?Mj!u z8mrkI=*z*Pa^Jii!ru8GIv%SQv*K`+Vo8r}a`a>oSo*W4tE)O!Me+jxW4I?TQ9(jp z0=(YiOjtkFc5Bh2wL_9jO$0W>)NNPF8+-DdJDun5q!~>{a)*yOn54UL3A% zW(qG|J6WcI5rm~#c-kB5<&${~+vOQfSV~O?euUrUQ`hF5=!?A1=H^D9*n%rWOS!>7>Y#%S28e~5;>GJTK@vVp-<<5qTHZ{gE|+1s1q57QqnP#yeCEg`!FVP0RHA;0gsYA28b{cSgU5U%XBxYFNQnbD9VCgHt) zX{|*E4izj~z|zpW;XM?u53B>VP=FF<;J+_`U}&MY2~}y_D`ADwm!^ej94b&2 zCvQ4DXj&0V$#BbhCJ`>~3)^-#!cW!5ksoq$#NQ%7t3&I;#m@oA4e5w}BT+GlLcC{z( z5BIr~V&|`v6p$GDKjBBmF4YfDZb_LmW+rS8Y+C4X?D;_RvpKp&o#slsH?Tfdt5a3k z^#Tpn5*Vq}(#>(Cb5#`t&j6GZJ%lPZ19P+78NIGG$bV1p2n9ca_kb~OJzqv|OyTm`5FdGyR_@lxZuKvRMMWkHW`6ws%_ z0U6mk!h2QfKf(!YKNdcKF}A8zPwfFM|Aeu&P>5*~Q!NJM&2h}y|3#JjZJ_QD3_ec! z)a>05Vz2apk>^6`mDfjko(E1iAVR1-iu;`qYdylF^F-&Vx;)mIq4v)aYQ6T2`NQ+D z{%EfJmpxkFwBwgzmK8Ox2%hQ3{mR#^+x&;e;>p&07Qi6aHj~{@##Rt@0(6mXyhE^AAy zf}&o&0Im1qg7f1`f_m9Gk}1idN|Qv`$7)ov-vRQ~g6ifmuOf|>`OojOytlog?Tq2y zZqFC~%WG!;c}kYyIa0$O*dnao%(KuyLPFw4-1bO?o`yd1(?2W%wnBY~&C_dCm^K!> zxUrNj({V-)`h?~wD)Ha? zL|XX_jDzgEc&bPSyX|M4&h!E1KC)a9AppRz7!Sxvf};hmD>|g|1!&6UdaW>|=R*&E z<#1wbu)a0 zG_v{f|M>08omq$dEn={%fVg7gcO0Kuj5 z4Q(iX4IozsqPb5O*aO=+GzbnXcR~x|(kkUE^o8a=%#j56y7nG;+Qd!CS`nzt)0LWe zFPw{dM?VLXcOLbg1hO%VOK`$Cp}NOTAm;~xgyoZRBfE~1o=YrEVh>f4*nwcC@lL;-)N9W6@Uj2BX49XRk zR)H;_1y?1&jjYk+W3rx1LvUDKuia;@lJV=_G^w#DTrxC5z5fNGK46{Bp+d4^^t;`o zE0;LW24TTJJEOSy_3EG)PD)1n{CGH=HC=F|Mo1;~Sj8jo6%L3wIi13)Xkj!)OCHoY z+0-=$?T4}xjFK5LMY59-B9Yv3vrk|#)RX7Fn+oSQf+mVop!Zz5P2i|)(ZDp}QjV9U zoR;CZeYkDGZilch{`Ni+CWoKoP7~=JBGj&l4!}rHUM(ZSfp#p9t}8fJwbLwp?5Qvi z)ni3Zh4z-Mok+kc%*Z8n21V2kT$ZU(gAMCc#OQ*_7Mq-gc}B39e6a#uNfg2XpY;c|H_yDx!pJCuHZ|(lh(G zu%4_>OO2V(L+4v2^8K}ssxYa|ivmWvtqxiM7??n;qqMFr@`zF)F!xOcSsEG<{Ujbf z>kjzzPne=BJ%wD({M+1<>%ZSaDuHJeFI%3^E9QS~aQVmjK|@C2FP#A)Ss0)$*bDCo z1;r`3P6{uuy*`}WH_kAQWOSOR`d_dXSlo%*;6U^v zpu|gQG{t`-W+2NOLuI(+QU}oFaEWyoQ^zB+tLS(vX6?0wI7I*fI8nQ^Q_D-LIDLX z11`OkDMWzNG#ffG2$2n)#qrASvR%<)eeC3)P!bB@+sp15;p>>hTYUY!_1s2!rod5` zaNqXbnFV7yzh>Io@u9JAySG!Mf0o@{W$bVPCarE4MT3-f_~uA|195j@u)o{`c|ZL$ z751TM7Hr*`~TJyEZ zQ)uRAh~Ql(=|VEuQOln z6fl9^NU=!6Gh8N!{5Yv9`Gs;GS^PL*Jcmf*1eeGJ&sb+FPZGAXXrJe0VYSOul3BDx z?55RHcicAPNH&30PP~cnzkO=VC&ni1rx?;C+Y;?@@nKy2RoRn+tib=p%xb>Rj@u;Xr@9a z#(<-q*`px|u7YkJ!R!u*5aEP3a5qJ zcEeZzkEQgDK44`v(lOWFg<6GKOtT0a=xbHiSPG`3`NVG9b5e-i0L`X6V{#HyXNpn0 zIM^~8-wA~e*MZcJv&*C78fgh=r_!ny^Pxzkl1T%3H>eVEvR>qd%OL=;qOHtp=0#rW z_C5cgb3Wn<&lxMK!DdS3dUIUivL{5b#6@04tvCR12>?>NhdXt<_<^OrVxj^#DlZ4i zD`TI2G3x9A{=UiYP*hAcCf77u(*Vh@L(4avfC~R%|6TV1esi>2@=@IM!rpnQ z8hyTG@EGgp!HQo%cFRfKw*o*C*Akmnzd)@*V>H~~*huTM3VM+&JhV$ruB*OwL)Y(? z>S*$MRr^76zQ`QPuN8m7psW4Q(IGD>n z6_2ikbC6ZfIZJHr-<>ZSrY{BzRbVX+Sf=8fy~efB_$g!l$IPGhiVRP7#7b#DKJl90H!3SQoStZQXMp{@#b-z%IH~;N&s`v zUFvyRBQLCoT_E=bNFI&KEenH{Os5xdkO2q>jMg&c$sEj|hJLj>27Fyd<$8_Ufo9|l z@>{uaAeuyAa!N=p6%*phuVrr74sNHi$t_;rZ#}q~x~qI1!6Q-BaN53mvh&P}eZ5tI z0gQ~Pm+7Mo#2Os2Sm0LwOa)$l>zf)1-V@P`%hT@SS*43AR!6{gx_IcZ=GBw1Z=6lK zptHkYc3HKxJ^U)KqS=!Yd^^o5`}GUvXVYv5AZ$8`>LGn~kB~9o{v-+#2Y#xTTTnkm zPpvqvrfOU=9F?7C{%`eumu^?<$2!Qe!V2r1)vXgg8Ex^SJb6odt&KmtrrIbBe1Dp|y*$vk0?(76&l@W=XO^udMt=sQnL(GyU z80Jf>Vc(kZjIdGhx38qz%6%2)O%JW`owD|X3JFsPiu1vSX3leUTidB^QbV3FlCYJ3 zk&l;KuPfn+nN#9-yC5)rA@lyc@v14k?}~qGvD@B=#{$>l1%2(i4~GL}jnX}9XQz~1 zO2)E|rGNWeMw@*kj8nEOTm*9gP$q=LU}=_|HAz*RLP(p0&Zq7bItV*G3D8B5(u+V3 zB0$nPwLB3(jv_C~pG~^q{x8OZn4H+N{ zHA18fy%LQ+mkfm&J4=4A$b%gcI$qVi}ct=NOkdY>kG*5lQ73&U5W+}&C zUENT1gs-s+qG3AhFRT+U;$%7ggtIAkEeAX9>tAaM(`%#nNz%=47Bu zW%@g98Z5|m9(6(aEQZe~F39|!<(qOj=6CrFpxHMHiF~=(XE(^K-6OY|NtHkz zkY-~ZQGARDhDRr_O8D_ABPTa%ZLcBQM7YUK)%BL0Jb;J;2$Lh4JFjYCGc(KK4?L4| zu+2s6w7M7u2C|Q}9zPC62&m^nSK(cz5G$SK{H#Y6CQ?fCYh?bElb`r%tQEy3lSWLdy z$Z>SiZuG{&D@_gLt6%@9E^2`9naZ6hXN_m^;}us_j8_63bWKD)#I+6pJ>Sm z3&?*9dnCKBqx$1Gr1Og>twKM%gVBX)lM?2 zGK0ta3Cm1jzlR4)1OE0Kjn{MBWqz)VwAd-$Nu)yM&_uOfuxe6_qjFw0({G* z$b##*0cJ!g1fFkw9hWWUR=2R7VrZ$B=Dh&5h{gpEgihAHvFT((C$l5jHMB#!VB0zq zA0mddL5zh;lBM;SFOw=_GFT;ME%fbEhe(u(9yx#=9Eu>1FN{tatl0aEU^3c7l_W?; z8W75>=pa2uIT*3&^>M|TjRqj#;O!d9$X{a7WyF?-F&x1`Av1hC?Eok?nY}fwa%-41 zG>u>&NAVw1g3Lk_x8eA!@>bUFUPQuwkrB_|)zeTjJ{Wh5) zY|E+i{PtUVT3mqidsYB@p7&pKrBW}jY0UQ+6c$!W<;(Yr?^x!onQ$J}0r)UU@*bY^ zxJz7ry{R$ps>qB}C6CV@4j?2UZT=G1Wo_=!q4(H&cEXrDIq)chZ6yQvLcZ!l{_A9% z+>GIc94&LsY86`)k`skLn6weopJ1Ws%-=MkFi%Ys%7jp6d!s587cTL!AfVQ>hP?#= z#3Tb*G|3XY(MiqE9{v%T;oR*c0XX*>v;I}S&cuKDmt@0u)s}ze!|HtTFY1RBd6k@$ z>@=@!heB}xES3yl76ZcBc1_HwEQDm@>0>{|1O?adG;$VB zeqrT2jjoGWNk%PqbiO@SHHVcW{aC)+Tc)+n4nCh_kHXs4Qg_X61DS)zj9(8t7)$k% zco%B1r~2F?Mi`wYA-FuNt|pGs5>qulmHFb{s%i354=t zZV9y>$^$0luQDI{v{MW?JYbQqI|lw|tuc2jIC5Cp8=3)=jTC1X{=2c7k*QjwZc?~< zq@)vyN>#a^>N3-Z|NF)+x({E7v^6(W6Q2}bI?PN=@C%6l8|7lmBpwN%hP7R2A2&gQ zt!S9sM(sWWHXj%I7o#I4<*}OGc+Y9cQTYYfK@X>jZC2l+{@~P^7a{S4DK*2Ffaj%q zAH2R#q@@HIpe~(Rq8V^a%BJ3RI+UnSu8nulOF8W{;(3!lZnKnVM8OCo#=FT&`u6BZi=d5zVddikH zJ~I00bs563L-=r}oj^xZM$Lyhy#R$0rueY_=UTFWnP_+X380KI)NXi@!a5jS6_rZ_ zahw46%R?dIUR~3J)%>m1IEqi*hBcD%hPzHU3PFh&=MTGM{s)3tXc|bkzX#7tqb$|Hietgddd!?P`ST7+aW?yl( zjKB(q5{pWA{yaB7+6@TRd&qQ{`Z@(I%fp3h#LTL zskbY(ZL1<@jpVQ71Vnt!$P|f)%}^CFC2>CrcM{?`Mu`tG zwXc633Z6^Nm>JV<=2_M%Ki#+0-|PLE&mO^rQ^KvPHQxobR3c)BbM86tupsNc8fDjf zbD~rtT<;_cr;YCNni7a7XZ2}DCmJZ%fdQz{F7}u7xxUFqgbXubEiDxCc~en!+tO3H z+%DKn$zi_r$q*CMdpV0(GQ_{c6>LTRw~h#%!0El+K}P{gN2R33;nMZeSh$M5q2W^Q z3eo$+6d)9rg`FG%n@#$eR3K&S(r~fssxh`ZoPBA19=BM#xx4RG<)i00y||r1{3>i+D6gw^r_vEEgS7eDKrG$<5oOTo$x zYK6Sh!7ssQ7XFmk_t`t%zaoXj@*&3-ab+P81o76hHB~<2{k0+f9*w3xoPQEGD>&_e z&n}*DQtYrD){56r-{##0T`wPo{1dr1lawe6GD(-p|8d=Jo%*-=;yIT-p+hU-B!%!4 z{;QH69-!CKhV3J=PbD~=4FV*sf2({nh5fHdTd6`N4%08p98BY0xAwiQetl!HH0Ij< zvgrU9DW6ruvpJOStgqPu>#+%7#GK{BCPp+`(skGXw&Kk2I=y=j62kccX%b5+|ymF1UTrNJL> z&ck=o(t2|R4|mDPetkuK;)pIQzX-U@T~MBU)izT%${-Q+RZk^5_&7p<&-%5EkgfyP zLwKR8%a~Z-&Oi0DK`8anV9i%f1V-Xe1dDB1nsf9=KQy`@yPv(qjP1{-L4NbyUW}b# zh^7s69|WQHm|@}3xr@XHa37o4i}oVsilmdb+$|phTUQNEQ>1Yn^Yt+29oVl2eQtI%{epvot=$0EdD*V++C+0v6iwkXSn_C%|2&v+UaX~ znA7b|m#R?hlyz);?9l?XV<1(D02z6}*jae_=227VbE(!w;U@4iV;;YC5k)V8i!pkT zFdYu4dXejXf1!^ofhMVIKiBahMz5a)7O(GDRz>lUXSe)KBQq25`GZ%5PL(Z^EX9e4 z+qZmCpBRP}l~BE(4?2z=QL=;#2QTBKMMzJkjqYd6gE_c76w=cuwcf<*_PT8SQG^~W zrcFb|%FRFL%pDWI>MAQEWoWOQNhb{Euka>b*hn9F?@0=X?9|!+cQgy02&^2mdGUAZ zim3R_3YVP?EElKp?O4b&kmFIS9r1&Db=rED-jpaE=5*8jj)3V~JTTavU<2o-xN(w= zXg_|Ird-5QOFLjXC@|R^!~6VW5aRVTMn)K;^BRdH>ygPEd-UkB4s+VcO8J)>M&+stM@M-?{YUZKQr{^)9aIGr4-(Kgk zcfB2h^Ftl~RP?_O9^9TEWV8GbsEXkS(>)Z5Vr~X)k`MpV(Ry0)RV2v<$L=w@AR*`x z8J}hLQU7S|$&v&CU_|U_I8V2yQ%7ujL`Q-cP&z4R9B2CPDQ)h1tF}*Cyxq@fNQn3+ z<^@~a&qc`iHB#PO6g!PVa42Mch?66IS16J4k>39;+En?rH{4S)AH(NvaF?1R$En`b zc=wM4Xtn2ct^E+eWixjp75$sB?dki`SnzDlFXM=x@hL&HV4c{zzKcmvKf%fobF(*9ivQ z!6=+aN&l@lkvKiBWkZPm^6&oU>6o%<)F=Cos;9}CO~0I9PRju;d-Ic^FKcp2=(czH zuA)|I?|!B9aF{D&_z^14mFppkwePGq1tR9_%5!zY9nE zewkQRCU@QfK#z|f;lG2(&eoQ!bD8N{R{~(D>v}2YoDDZ_9LH5zc@8lN%$_&YXB_rA zjiOeowYIhvBs(H%r2s%{O-lJu<&*G(Q50Qn^DQ$6t%#%mK`DiRrYsnI)*GPG&N*Oc zwi=;&3Iaw+Y%4=VWm#yewBwV@tMd!9d#~7?8*hxQ1{r&=B5jINYprxVJ3l)&-|Tk> zop#T83(f;zQcEi1Vk|%Q=(hL2Gum#Z*5FoIM67L-l1&b@C zFSy4&CaNiI6&mMqYr@=EI+rLFDHS?og_c?h2m!5gth6FB-UGl)k~+`fxUX+300_@} zZy2Di^*A1e0SGV}`40%udG>I+dW>f##8_!WOh{Q~>kYfG{~-o;;QVRt)o{#K-U5K} z&NCB1LQ13303@;Qf*`{EVlN@<3JGjd+hdusxrjS#Rr>wRf$|&uogu>EED{`XqiWyq z%w^(BLj-Gm1;#QFvaxK8HkOPXmSzWlbbTYw()dZ?9%&?hIv~C>jtcKRuxCV2g!Nh! zM;e$3k->ZCiI4#_K^$YFrXxKrN;dw2m>Gz;EC*xZUgp^w43&wah(-m7lv3SJXZVK9 zWm#J1LtVB|&*6-nqRtO%w{AsG%+7OJmU*5TQ-$2qLBHQgB5Mo-8dFwf6-5ydF@sY2 zf`&|>2QyS(VCzw%7PpKomRfsyMd#GuI4M=979!2p77@sUbN!hPeD{0&7XZK>_S5Zq z;2qy}e_Fu+Ng*Qw0zuR4*zON2uPxJS@TA3#h8Vyycwj)JW^JKzh4pTjJ_stZ^9+o> ziJ)VupXBEN_!PXvUw)L; zB01MLG<0KT>n#98YMrW%;^_3G#Am$6$9>4tx_PUI3*d z@4dA~C#jATX7%9KUFmql?@l!#EDhMm*=IZslss>d_NX${?T&zf;GVDUNnPEIS zqSD$s=bbC9RYa{QaqOM<2#`4o0EG|Ll^6h09cjei4@V?MuDlzC-wazxYo%hP5m*B; zgJX8!*@GKp5m{$FgfNjhOajK&PPt{2t}lVJ=*q!cNo!jt2?)w49rI-_M6 z5K(Ehp-g*t2O?5Lp`^F>iin&W7XCw`&0PpC=t7Dfss`)Ct8IG6Fr%Uuf`R1GevJUib3gAG~*(5&{AM0u35k zX326R$Ee3o8SDUhU{$B>QO(w)*}iEjqR_ky0b{?NAo5U^>i9|So`mPZ<&awBGF7}R@l!}rRi9(H?s8%lreIiYY zOj#g8*3UxrB@*t9qa;mWoVbq|MLHYg3{d5Twaz)mjLv#Mw8jFIPNB6GAt|i@N3luP znhF4uqKa#c9Mc<0$aw9Lf8qh6{iX5dK_m} zHL#Te^sEUzB5Il`2wf@6+J=_n>dO^VrD=YrqLGdS>wNJ!o}KMdW$p?@9NvG<+`(St;^FITj|0k=P`3-j58)we$bwyI114$%pAw@px^Iw zIw8D45mm+vqel^dLx$VBWN7Ek&`O<|E%&_(=2B2@D0)PR$tSPC~$w5^BU=4P9bT!^YPj92(2xBBgU1VZ)*o7ih5Df(f+c*3<2fhz z-7kp$o0*X)1ja;B#s5&*W|Gr+)dedKm@SX5|Rft-T^@6U6{=hr2rr$U9z*Ics@RzO)Ch(>+&H_ z2zR_#UbnZKb{{#&Pd~kR@jhJ*a>BEFcDMTGL&HrCS>fz2*!;51D{IKwyri~6ZMx5U zmjfsS4;a0A>cp!q1FVQ3E8KMlnpfE}+-Fr;L1m^#nLSF*_#;=Ru47=jq;XEs}A6kNa+H-xS4+`d?qWhhdKh$m01 zjK6E8^i4P4Tvb&lqn@Vex4!l5)zvi}X<$agvNFcli6%QbiK8tm`g`x=IJ)cZJI8_> z0ov{ExTQ{2l{dgfTKywCp&3soQ08n-QE1R8)|MkZH^jJy=i@lmTEF(%YXI=GV<``SaoPxtlg2)YqtCqz42^{u0BE)v8Zkc9TmQ{rSJhT{A-UjoP&T69hv^JV%ec>ztm)4Z*LJVQ7 zlp>WV9c#@D#yRhuXFqlz1m&%U2SH3o6+6dVdFz=8Fj1O;Q_`aYo^kPFlpq;FIHxXu zeIEDo{CS=_v*P-6(k%r&ZkFSdH6PC;^w+&JZtZt}UCZ|QsQgQ?(xQrsQ-5!=qev+; zC)&TTQajN}V(p1=wNthd{X=){wyum+IGW&zBJ^mzf96PUTn-WdPOY@rkKNMr{TBwt z*btu`EC0$`(k^s_hA|`tAYl}an2XZx-o1F|tq)~c?@SdBt1^4zOE4mO<|vA;+`l)= zGExKp?D>_KUmX=+25a4e?|pBYCeE|7c6M%VZFSXDdA(lu-fOK+t#-@GXhHyRo|8CQ z+%{|N7PV!Zb&W>jhd=z`unh5vo zb|rnV&Cm215XeDpVHFvS0jO#ki+Nnk*lzb%J_DZvfV!S#;8rqEgoDaBE}J-8zC>#uWi#2<~Hbsi=43}L9!!a9LJO6KZ+uy)bZoT0pNufUdZ!YDHSKla8LN=x>LrT zIK2k|Bn=1@%A^nhz@Rq(W~CH6uQbI;9J(WIap1@Rh;bSdk?EPLtQero3RDO{z-Wp{ zMPSNK6vfUKro?K1)>Y1cs?jA&_eVhIxvZ*)$Ri+V<9$M^sp6hB6?+0S!}u2Tz=mB! zKk#7R8Cup4m9Z%#OWvjz%@I7-0SJ zC>r)9d+!_~)^r2_2`U_xZy%OwaGsd}5Lx_#f~fO1jJ`;pfZuLLABU|Ytg9PyTACwo zbhmHtE}SJx_3RpbbC~;9f$v)?$v1O_MO~+81}R}?)SRZOWk_oxLSua#p>=+kRJHlo z0iYJEap%;aV1~jS01@l8$i);+5@OA@oC*igM5op8pAZm_V6&`S3Wt?WtE+AVxK4 z&8!}Lk-m}CXHl(aH&U(gK zY=FQGVmry^Kq_f_(kCyCe($T+%p5f$&)9E=-FqVEsCO6tF>CK zEX#r#wr#6CwDBsFdDNEdT(MdJBN_ZW~5n+}%zOtUGmbHPU*1X(5WDs;IVD z{~~3KEwj8Vs$!6%Qm7CSnZSAKog@=`2IpPnOuy_SDL_VC*C7>yTlu0|KoKgXoOOk_ zE@lFD0OT0~G*Lt;&VEwlM%FW3@bNEB`*DFBpJH5TYRYV#Dzl!zu9C(K zN+}{0o+K4M6^0!JZPmkth=@idMAipHg~$g*3PuW#h?EMl29;8i-+B;0-h1yo@UY|k zSQLgIxqu@DvCFTO&=vl5JPL0p&K0Qc> z0XT>L8oi9D8nwA`y{3lS4!|kSDU5-^sh-E8=5{3v06K>bpa)>BQtPaBMXF{Relhc> zj_@}Xs|e;U_*s95aW}>UxA#PoT?BxlD2OOTmXCvH&gO?^)>@DAlA|a}({%Wyk6sWq za>G^w>wlfKRw?zp?|rW<%X+=uY&OFyLnV0YTu~I(xiFS4fqZ9X=jTE>?+yB=Rzbz66(A5r z01U*T=t}<-vhA`aQlnytiZF_X@gj)q9r(~<_QLnE3SNCp$UJ=WkQD<6gu@hR?>UX5 z#d@<_79BSr#EN~_Rthm@0>IGL)P@ebhy>_I(p&>s<{(*DdQj zT*8J+Ap%55le6@Az8hDQBtVY{qSuADi#mVP%YW!M-2`xX7-F*{fzRxHhph@eo?V4+ z_^C4m&%7>yeoi0zB~IW+N`}=a*`rDoArm73GepD;U_FNXP~(7zTJ&Cm>|x)_tugzmWQDZ=sky8c-Ee-y6r~F!#Ssv3Ux@v z;@det;c>TGsW^_Ewch(Aj)O+@%t5#hcLm-9AYI_TkLcMkc*i9HXV8Daz2G6+h6gRV z0W9~Ezn+6VWMc(7fd)eKO6zNY9Q!MoyHG7SECHZ283TB50I(l#KTHpQ-T#MI{kIgt zfb*C5NE_@`6Z9$D1BQ)+~5TOZ(PX$`|=wS-#i6J25N;s`?T#< z&-WvCaR?43iWNn~8o(k_gSQ=UcOU1ca(F^>3Xp(}F2YQR?71o{LW+`gMcKpml4m&p zq_q@AHP<&}5^JrAs4`|$vy!z^wOUPuYTpwc>kvr~qp+OG1DxRI4^5gv47V}Hd*5s} z>-Dax7Jx>2Aw`Aq&OW;o=Qb=>YZ_>Od1VlRvM7Bsj6X>0R~JWwjP)m z0oZ$EocB?j0AKiuDUR8)XP`-c1dc?og`rc#dtZ4+0JAEw$m|RvRNi;XQV}5}3`60s zhs{=pr^9ZP{OXvIkN} z&GEP8nX^G&6%{i(@7eR*+}yr>d*e6>$)>NpcA~O2jx;b>=W4au%*_b!4BuTp6o*VVQ1jI(O?p<&Ig7?1H@8@M1*7p8j;H;~swJ3^!Ino*t^RgHUc|v2& z1yb&eh{jd`P}*LsP64y#Rud0gN$+dp@$dUDIdD0)W2k4a(&C*2k@g4-e&!WM%N?Co z+cS8zGdK6fjXvKD2EL z9``+a_B0xes;Vw{38r(p+qEX<$dnZ`Bx#K&Ix$pd=odyr&e^m+L!|Qlns<&O4Zyu_ z517+hT_YQ7T~Pue#VVY}kdAbc0JHbbGsp8Y^Sc+kTl1_;Rp#wZ*w-Ngd(eu&fhX{J z1q@0N09aG03mQO$h`F_yF=^O}Y|5lU1V2s*4c*7U6X2|hEwJNCE5*#6GFOOc6i>K{ zPM-l1qIGQ8Y1Bwaz>FJmy3hkNvt?g7M~I3@DMg+WRFhR=7{C~VXYfZkW<<8kp${uc z3noDj;ePQ?1-9^%Z%98qi+tb0$97&PhnqX3o-@w%9NU14bwq(`@amtk&483=h>2f5 z*&38JMT~@{!K;?c!9`OU1b}2GymIu^|M~A$(-;7_DE+>j^$)&#*C4ZskO-^FzWDsB zY9!nd;OXOuW1!RP4FM`(AcDN&TMy|y+o&{v2nsPP$}Fo$At17?bkjMHbir-HOhiRl zy|%J4TU8<8p|JeS3on(yiO#H*{=mZzj)W9wP2c|er;4%)1>CK1H{Epez4zYZoOjO6 z%*=f0OJ6eo@U3Q}$;_3hmX>xNI&^hemP(PS%-sCkEw|rVR@KmPXzf!^JU#BZ%ARk& z?M5AG_Uyf{)oP#r!k1Q7PK6-*%GzCv+xPC=Wt{`yh;+N(eQjkqQrdeT#c{je9haPc z;{<_Ms*|NUk=h+;bybDs09F%je8B$?t9az=@b6FZb2GT}Huot9#dqOxK#x8~NX_hX zOQ-HW;UGfSg=5#M_wUAc9OLhv^v_kCF)*_3@yi~44R&d$JIEEsiEnPhgA4A^xA}K8 z)R?_int_X|&*%Yp7IqcfeHQ*Is>Ko1Bd&Qi%#`D+2Ff{@HIN(5y|><5rD9ZMp~~G+TDA0CrJ|g@gYRsT3bs~M6|}7rLyO^jEr-x z+v_#z^|0}qjF}082LSf%+n1(kRaJSOhro$TSq{>Bt~Zi8t4=%9k=oR04mUQaV+MdY zt(QeMY^O?!P(ue)6vo?xVb7`R%WND1Kaf7vfJ<%SZ zTSZEPQZ7xSK~{w)zc^SxC17DmWtb4 zGC&+fg|(Tf5Fs69u@7~&_YooypgV2AaiRXxlHI$?TLdITL`+naMp0FkqeBQ_fY9Ix zwLa4#Sh26zky0p~Qw2eU`=t=}{zueFM!$U8S~v1CR849ws? z=}5=SK{hC*s;Xpm%;20OqN=K#=TJ4-Id844s>+zkIkLtUMbYc^#?i^aNk5ic%p4+Z z!s&Skajw;BA-F<2hlti%=RE)i=b%-Y|g1#6dm>w_mSrdcyzKDgXUNb>;K+i*0yW zkxq59vN} z;3KX9G9Z*??%A_PL}Ktq_#1t{GQ2U_E2s@{r9-bKu&wU)8|)ca>tl~VGx&=n-?z*| zRZ4jmN+O0F2|D)Lv2EM7wOTDCoJ=`oW>P9vN-KqDknGml!C){L44!@V*?aG~XTz@! z(e?)p97vL+s;Z(W!axc>_&m>zF_*ya&dedStd^>!U9)B3`vXt3ZmvX9-k;9fMy0|J zCTTrXP(@VC77^om%aw(x^58cQ2PP^`0Kl3OnGw-D=S|6uRV`{YYBhx?Iwm(1M`|r& z@8MWuM^cA+ePx(O4iITXP!p5*h18>-{kD2zO-GFA83x`0a6(!Qo2U)5T(nZ-N-q_# zhEUpm=u1kqUTn0EcG>`JnNO7k0?Z^S0BA%4u*`}m?26d4V?R8!4D$&)<17LyRKdZY zC|!?Z>l}~j%uERV;a}JLxlTv7DAGyxQB?H$Y}BycZ?N=*I)d>4Fk4%!$8%+s#b66(Uw{{DNOO7#c_?@O z#Q%>7bSM2J*aURh%U8X3t;RfH2GHp6tM{s({ZsQlMNn(%ZC(57QtRNZZC4#xdAdC~ zZn<*aB)D@gKbZKug4{w?avumZC5YhJTkpsx4R^)W>Lvq)f=I(SL79;dJ$mp6>Zt#R z@tv_WH8Wdd08x!JX!uTR^;(u?!K}`?P%DNM6-5zh z@NY_P;n1OXeLWUstbXJs@|->=fT^TkFNaoSHIJ0(B8EB#qTm$xcPl9l&#(H zX@fyj6r;beEp^d)zff;5L#Hgl@6L^}-Z!E+)|vpAfsokyvaDce2)3g+k71bJvu9>D zFN)Qw07S>!D+>mgiIX6#nL!f)536=L0Ax5V*-3>AR82H+mQT+)Jn0XiP{;^QCrJZ< z9aaqhg~R|cP^{t-1}w5&5Jb3NK5V(DGk9PaAEXXzGHlTdXp5SUoAfunE>V6#7g^lk zX`4v;WJKjdmkwfPAOz>xjT)zfj7pji?zri5Dup3Vh6vTTFW+br<-OZdxC4UsjG%~y z?@trjJqUL~BvL?Xhh)AS{y${14KnIVz(^@0Z=?iggoCW)XS%pvmfVE{( zt6j1E#$I{y(b{ECBqi{||fHANKPvBCsMg$a@8UVGi$7 zurG&hgSJ2C|8o26!8D&KT{bA%i>*C#&E3bkPrE_oFhlkhDvi-ee*Ojia;j&YuOdnu zIO9#j9b#|>aso}D_qsb);2DcK`WP6kEl1-BgF7~f8}($isQQ)3ksz`=iQ_DKL2<)E z|Kh23BC4u#b)_9A$^62C_ns!YlGW>V0LZgENs_WGLusHO$MZaIwOXOA(wV&Yq4;CF z-TuyZzEi8!LJ!n&udYxtW_fuThAGZYYpu1es!Az!kl35l#R)*#ku zqtdSAy(#mIh@vPo_kpzD80QIXxEBBbyo=%*j>2Hpd0!YHY;QuX9_NOqb&VJzU6syS zTT_)O>5JcWDng3zOwJKzRyrC<eRh$6MP)T~P1>z^sd5luPp&L{OoQ0?dN z@UQcigkrU}tjd?4e~r>qC+Iw1jCAC4Y~&F7W$C;}A_j244W$9O?>zx1z{LNf8`ms`BF)-E^MHMH}cr&vtu61k~(o7N2csF*BRBS8fLTz}@T#Mp*6o)!9!;}JM z@BM~_&+)p_yT&^k;@trdpc$p1{D2$nd%VTIzP2^k@Wg_?T_1QD}Vp>$K$ll-Un%Z?JhI^yw3ASK5%`SYVVjCB8@K` zD+dKD0`Ixe&|mt-$~T|yHflQLS|LC*I?0bBI<;2)&Y!+IX-lGb@&nxazz=H`4V#}& zE*sv}YBNuK>(9RX#IteI;GuZ|a9p;p1T+I;9e_tnx`5Kgr;{mS3`-SWwkaU476 z>h=1IufBHt)LJbo5OJ;DK5*655C70ZD=Vi$(8B8K>R0~y%e7i9xZ$HDy7TV4-01W` zk^1W2{H?Jz99LNHb}cUL-LWgQ(rnc0-~8s^zVz}7Nm36LXzxh|ER!-^Q&SW09cKSiIyQ`Yfwpy*(t2#AR_uhb4UJXvs-&AFmCN1(>fxer5 z?koP636Q}c5&#;TTZ;%fl>Gz`wP=v11gsz@M9Q`?SO5hWdMWWtZF?HkJp0n-2vn3k>nf$g zo$EZiNX43JMb*1VX?4t_Hj|-I4iN%`LN19)CX-F8s=DUdYm`z)Up;D!IcICt5Prkn zhkmGGPZ)yT0pR6VUI73#a_tA-Ei-@r``@psN^2c5o5xlEFJ)DkNiT+XrW_1Zq(dPo z@BFz63*yG(%k%0j!l-p+@@HTEW_KkA9V2cmCL6;4RPYNh0`O*!$pyObpxui&N%ZL! zNPt959eL)=xo|R9cn1JA(jGX3r&rFo(KHLdMOj+lXrjtlq}1d}hk%7Q%&ZWt_l-EI zO-T0e8XM+)Jlo1>yc@qQAQ#=YT zrPjxLF!t<7y@&+7W@-8XNKggWFYW9b(+c6DjiE^GI2E@LKCo;tc`O zBTU5K`%nX0htGyu>nMt}(ov{6V=7X*EUU^GzrK&=4}52MaO|1cFK0)|<7|DAnF%$s z&x?)&>opiCLORK6^0kHXK3vz}f*lx(rD2oST7!I5(XcwcqNuXPYX7*k1ppEd05SMc zeO1rkiIr#D&sL(SLqHA;0=>jvbx_U04G0l{2SCq$TXRpWQ){hOaamqocdlh&tij&O_^s*?AA3wfh#|{_;qlY}^6U!%hz23gP`;=0f zI>CipL+`BBO5vz;(|7|=tJQ9};f6fVmzS3(lbZoxWo2b~d6|ep?9I4vBmmUw^(0A> zB=Hx&d1?qAt18>=xFn5{kSBg45z+a&ZW$5FB8%em^!&&Gh|XGOucCMyX`>W1no&~I zfEr0{DKZDS@s2eQZ>}WKN?}p5zi0(#5x@d7Ks@@ZXGln;qzi8|cAC+6pW8wq)}t6k znYpnJ8Rx@KW`bltT?q|ZDvF3Yw(>kw{Gr+-0)|7ZR=ZA&&`D}i>)0c3;wb$f_ z*)m0S-NzWkPo0lhaDCwWy7%NWd_n6k`3&FSy3ZEA(aB%*Mh1zZXi&5V<~5a7vyEK} zb!NI*yX&1HorE(e2mX2YyoJFqpdZv)@>O5ikjc&9G^F2lzYFXWYHF@|a-IH(D5@zn z3g%|7RTAl%wMA+Ar)#q!C|9fXmyYun5r{wo@VET$z%YuL7(4-G(f7T}fsq+J*?P9K zX?A6pW!OO`kGA0sc;|CC5M41d1hv&+wE+2Y@axB0swaG*fAMjr<6Lm zaxyRSW~*7R*Uug{6E=R$X7ljj!&Oyd2%fpAB1G&nI`$QN$1WTtc;_nT7>8jp2&fei;zWET7tX4Qv05Vy0J*m# zhY3{H_|v|BK@dc^UwY^IMprs+`I-&StXqeGf^+oD^Ycb6`+C;~<}EZU%FHResdo>e zGQK-wcip0Y%kbzfk8oZ zv;U}r>Ra$TA$tXZ^62G}?~-6$tjmEXtTvHiijG3Gnn0eCg zJ}GvMG1pypU8B(mq41YJPUiJe<^upYlYHOG@qz4|txAeDM0(3F7ZH*M4Ubh3071FY z@Bu)?vM`}K)J0P4)_cdS!x3Egs~@(s2tD0uI%>_Ny{cI5_Y?p#S3C+?;jPyg;~WEn ziU8Li+5!WDbH4J`ndJ7ck({e0yi}0VMlIo)i>gBDv?K^3+%Ji1W1ZSQQ`4o%wey_G z@*94NUVjnjtwcPYU9!Kfe?8C7z@y~dP`wdOmuqAk`}O&B9s2Q~^LX>Kb&WZv;u#fS zIQxMHj!GbNi>WfS|5fpQBCr~TLV42 zSl0o-vqL2BZCUn|iUGlJQOEiF^e;K^uef8Is-ZIgc<=UazrG&NJiq++mB~V+nO2F` zsb|Lw6=eJU9oM+`eqVj%MfGI`F#rq$fqWXqrH1rsdT5+s!b8=LlOv%~n?oc;5r<~9 zU~0UJd6s|}3_*N}_B89th9UqfrJAjoGmVFSzps>97q7yglxnxztE;PX^K;E+^Yl)D zD9`gGO%EPCSWP4?gy^i}$B%CrC2x$mat0DtFqo_e4RgjbEWO|dw}y? z0-4VwWY2faq1%^@OIm6@NN{TILRvqK3+WUA`d{`2qBsJx>whS}F@HRl`xR@NTcw`bq3 zC2PI+Zf@X);iw1cYmm*9U>pT@%6u(lwx1} z;up%Q8hb{^?Z=r}N72e_N4smMw2lD4S=*dnxbnuEf+sc4^M#$eLKDpkvG+v=kC-@T z+r49H-16Y&Vb@Lcu@d?Ysu-FXoO%uaD1uZ&GWue8+w4B2q7$cHKtKk!f8jc4%9 zb!}c;OVEI4(Y0s2^gV9_LD6xkq35~ZU} z@4;GY(%PA7%R*JofhjO);y9!&s?|HEEHGf0kk_A#BB3b?O-RFrtQTe-QiKCjmB6Wv zXsaWU%G%6SsgBY(4%MO(rFXaHN^e&N1C7YQnKb}V*Aa{cv{niLs^Q#?7CZttV0Ye| zc+X5obG5o(->Sl$wS#!!Oa&a$cSL6hf(Z8uEZt4%qnZYQ5YqB`<@>qb{!Fne=kVHW z)xPOG7eWN2sxoiTSr%$L23@8IBCTi?@rVSTt@A^tdw8H|xbaZuYI2Q;TF2I&u_@!( zUFk}x*EN{W<(Pj%vLlJ(v64mrYwcLL+aWH9Z)0sJ#mx+e!v;+2KBs4Et+S4anSDrP z9hct@d5V+Ch2h;#*HoT}t@opP^FexsgjZ4uy(c143NKtB#Au6RqNtVUovi4F0iQst zhbO+F|I>m$c#VB8gJm#znFDYwnF~V|m>1{wD;0U~L)ra>)?Np;EVGUoD2OY_uJ@0A z*ZtAA;dc+<`*6}#WfB^{AplVk> z({5V>QE{z+mnt}w!1fkg>7a^WCWRe+d|oMST@~suUf2O+tzBALdjAJL(Cv1g`R+4u z5>G~%13*<)01zkfS>9x@irI??zrQQojZ31-|9tG?Zz1IJvgt3s-k44YxM-!ZkP!}C!jJPghWgR zAfoL{%~LA_?|ITGV5@R;Cd#nMvA*HAd(Zv8wfX4wL5$PdI0Iz!87CrCrA2l~Xsl)L zFI11Ju&MLRO6$^D_P!Ry;S)IE3Ydn@B`lpSa~mn$2=W~uCcUH4%FCkXYzXS6s`Q9uu9<(E&i=tP8b_lnT?7gwP0r2=@!M zZh$kK&P6};zq{&l|L+%~nBr71f_G>0zK6(!Ed=#hb@--t<8w*&t-bGO{}ECI0A*HH zmHo+o^}*I$YK{AW&veI$TjNlf(-lZOAZ*G;eVw5)6YuwWKNw#=)qy92jK?AywhEP@ zbB4-HG*1n0aGs$u<7dY9nGBVI$>X5)>BbjePSI+0b?uQyA7+L>{oLaUA{|8@$odKt zd$0~n4^RAq`Y+lh48VN4wae)9DFAS)+`e*0Z{|gaR+WnZasVb!3L*fGp?SprZNGlJ zrLM8PnTpmd^_%u81{12mFu<~3_F%0l2|NOv^e0uMPb(6{%47h5OcqMb$qK!=?4M(H z3|>K$<7*7n92~A_6`0E^+p~M`|MCCye^^^vo0PT}z)>)&v^YCEd*|JE4hDnoKK0$; z4xg0orZSV>@$olEMBQ$;+wIQG%nT265Se)#;u1=^_jTPh-a9`(AD+3Wp)y+QcBk{$2jBnWKl1V4{=Gl=`j@|iY14Zc^m1lE z(m_dk=ewOeRAid`Fn&`2MtFvSqc~AIF0-CBRq#A`=X{wDgJ75$fdIYXL1pWW1OP~5 zT&pWhFwEBF_R$jnz#NQPAo{=tpXt0F8Hrvzmwy9dkr#HmP|Sb=w69?trtnu_|XBGZ85wYs^}| z_m4jI@ps*Se|xR{22CUgQ!7(NxNoVvcxFfX=)bzgTmG~E*YgaVG&LdqAl)|k^~Kwq zUA0fjwX=(OL{N&DdC)Bg;V1r^50Fwb^L2kgiIyPuuP9oYKeW8|{on=fW~YFm zRiD=wS$4pF=h~e|-Lp8$h!jEnRr=No_~pa=ff_U!yzvz_I~BiL6vrcI0Vo2pP_*%B z4fO^eVgLedY`(%P4jcf%L|!nyBNB9=KxP zKm8~FqSx)x1vS*>{hrpk)oR^-$L(cVKKY#|C*^w}&vF20wOVf2e+R>eBBEZu*X#9K ztyZm8+u$-kapJ_v%1V&z;b4cE%d(8)`0l&!cFv9KwZo_ZlQHJjTW_t`>v0^10`PCF zneO=-VW!p6QS`|B-}kW(J@UtY{+D0;{GUP6jN;gP%gm@E7^ab|>-$51GmTk7S~)og z6QFhOiv4?z9y?KFd6cAEd}|Z88+ud@Ju)LW2kn!lDjg{zQr`RCz#srw2LPZH0NNr8 z_l+$IVEdCl_33M_zEbNbO>2rOeF?FZTO z{L%~}n##Um%MAVJf#`JCPC#TGX&@L0a!wOnOVN2Sj%&3@tJA@|i0GVk)&%)(s@$5l zZT*2kmW_pbRaH?GrD+=6qNG$5MUmDB=siY}rcDjGLfCOonOZ}d*2eBqW?yeKa8y)M zDa!J^D9Uimvf+Jn6!m(&JkJ60;kVH1#m$yAhKRB}&o5lyatL@xYe}4>_rCL)-nFAO7B7Fe)7*SsLVP44#k}I8w>>xr6QAi8!u>)Zszi?q}_#h220vCMM8p-B(gQ zt0^v^?SP-dKc7*DoBqlYUqsr=R54hBn4#+H7rX@z#&V8GnR?#X92gx~f&>98n`u=W z)u7as@9*I2W?*j@{~pj2#DEo&A_8k{-!ET(_@009lmA^lDC_lF6zK~dDzl}0Pm&~x zqFZmf)f)TclTU`K>M)VU+UxZW9z6KcOE2Yl9>vi(=K&b{{eHLGou8kNqu3ZzRaKDP zb`&<_z0dQ!Ua#MC&pp=KBuU0u)S>vJwf45#ZcCCRO;e>*h`+itw`aJDq9~e;Mv^4& zefRwjzvuohe(mdj{>Og^aXm^?@2qD~TlHd#qj-GS6%3*j?%T5jj3?W@(%3B~`pL5k z1Z0Rjp#$pxiGaB`sE{EX1ZpAxsParHt%yuM0N|hbxu4s&dsm~`s?};`Sr!-cCaTJE zVR2Dw{i&b&$$Gv1AAj}VfA7T?=F)l;MY*#BQ`K}-iz4s6AIkUPR1hW&9F=ZVfB+CF zwODVS9P~oHb^_=*Goxbybn8o{Ro;b_KfG;BDnbf21x98rY!#9*Jg}Ph@yRO>`@<~H zEWl6y)KBd{c(7it*X#ANEPvq6Zh|lky8JwZ`vv?pZ!d{&OAiRMI}$*6@H2;0On>#u z-_N_YF&~k_P)+Lxn2XO;UHgrHpNOEea@M!o-BzRi)BoM0*4Tv|Gr(X<%WtebQ*ha~ zT6I)4VDFi=QqR6x@kFzp7oI(8?a%?tz^}cGq3AC&rxCn*9MVL2W<;v0eCO_~4;;Fs zECwO#RH>+{ip#&=-0Kx#N~xdz`GSbv`!r3rFD~`^JtERN>UKMS`Imp1B#HM-M8;H4eCr>A(L%`bkN@P4*V5XS zo>4@!*7#8XZU}JKI^xg`(Q_O}S)O00uONgA=YuRsl5{qG*In^>RkW(>9Omu#$=G$~@Oo41_sU|t z%${;M<>S(OW1-R*&oy`UvgbWoU|{eH;x0bpY(DHx1LS-OY?*z6%?Kj))_J2bj#N^b z$~kk(Ew_I3V;{Grou8X))a+m|pi6Z$6u!Y)t;Q+ebn{Kj{N#6@1b`@t#<>kz>wWw7 zDWzV1`Q@@KYqeU~2zl?FbNzmQZEejM6OIT*sT7=L+04w$t+(E4txeN3yg0ldG-|!= zw%e3a^?E%>_MovYvCInceJ~g_8uc_y-~G;e-g(C@-+bb`fB5_V6?7Wa8~LDg-XrR9 z@e*c!?f9|XJ9dRnS`~$L&ilDEO%VXGzka1+MT+9cTkEZLo@tx~%N$Z;*N&d_n!NJ> z!(_@Tt3uAM$$J3s3!nMS!tCtg_U&n!7DbWg`2{yrxT%dXi`%!yas2U5{z#l8|Mh?S z?I)gjdN!#=QCxagspNYq7|B6uQX(9>&2vpotb6Ed*T2dX||G2F{euB|0L+ z%Gt0Ec>o4ZPys^8?zBgrp)e&PGBAvK(YlHHF@>`XptMH7em?L3KmW5oySTKpu&_|8 z)h?2i1mW!`&EK$azZf5v-!j%Wr3ZldJvCH#|IZvyvHI(O_fqFX*<6fOM9wBfjyCfmRBX0!Ct13iFaD zggfMUX{{R-svME>WfhMXT4onT>CEg>9(9RSr~=`gdHc2LK*XIpmd4%e(>Uq( z`<-rAYaMF2^aq1RvsqSQ3q%`So1rzm))5g!QGy8V_G+5eLuf=*RVPjypAhb5`?hVZ zR?FHg3a3B_ubFsrX69i_b4Eb#eRZKdVMyq`@Avz06z|`?e=w8(#J;SOY~XEL+ybiLpI@Q%88Ul z%t?l8>e>*&tBtut)o-}k*GJ^a{nGe6^He1K8*FcRqzwYfbaGE62uDH2;+Pn8$LeRb zFeC_zUhBMj z@Gny)hNDqmfQI5pK2;tqBZM7ax|8--<=z+bVCz2gJO z<8NtqdAfUg=Ct0V-M)12O}QR^_^xxd0CQwe&xvxGQ)ZT|465t8^qQQnlr2)yv^=^> z6n(uk?^!s!)Q!?_oLR)TJFm@a!bcyUhHkv_g1TQbn7&f-t}I^pnpu(~NyF#tZ3dRJ zEpxmro2r)%evRW~@F-BBTrHt%4;EROc*eFLv;eAzWO`o>zJvA2Ab{YHGJsb@&Q5d^ctK6|Qt8>e3mBtm;tNG&4z{U8t+Of_OVvzwY>s~w2V zJJ1?1WY4A52jrALR?2*M4%K%T;=g|VIuJ3U{wgs+*onvIzpiCN{;U$^e)UaXZsL8a zoAh}j&ga`17e{I_^|(kSsotKR7&)FHZUm2K?nPWP!j7YZiIKHOOq6H1nKHjROW(6b z9r&XlBcn3h-0E~e*yH&+XEbkUh&Vd*$?C3%I>`oM=Z@(Y?8mgJ#bpN;ih$` zAWdNJ55}bT$`lrv=Nh~BiJ}Kz8+VPPr5Lsaa(DDNwPGt;Bon8<2JJHuwi+V zsrsjhUMeUdhG#-84)wu*RGeyBpANe>B?qO?uX64Bjd5fPx{oW&Dy{9rH0Me+A7>AE zujiVW?Z!7=ztpA%Mb)>Ee-O-E84(BBuiq-S!5cak^XF8Cc4g#rIJkD!H=c5eVvMlW z`Jk9xcAnQ`_+ZT0AUu!MDFu8RZhU1kOZ}m&Xh7csQNglW|B6C zqf(%ZY~7^syLX~m4l!p~X5lCFhj*kSt`|&~G*&FYAAFeWy5PPStv&p$9nK$%_4!jx&0%KhE!=tTgAZEBJ{%&2XX?bd#Jk|- z<+D-apnk^P0e>f_qwE9HEQw=mUQ%b7`=$2}=@#<(`g#|SVyRuubZnfmU-6ik&b||= z8AK1Ms@0DYs2Dh1o8hI?9Gg6etJdkn=_JKOhc{-goEE=?k;+9rzv?-py`GFwOiZZ~ z7B+d8cue5o?G$sxM4t6~RrC~l-({}`nj-%)nX#)q>*8(p<9%mMD$2z??LDM3eK;pJ zD}rckxX5WH`<W`ZmW0>ZMA)d8OO&M@3dI;HuN?c9TF@#p?{NP7BZ z{n3l-9m^_F=QEu40vHr0ThFQAvkD_UOQ+`U{T4qsxhXgPN9uXzqY%sZM_kcNOY=t= zUM-~-dK>H{7Hyaj&%i%)$OJ4oHox5|=@sJ(DN?F_ttq519lN?1ew#`=bV~CA`{Erl zt$_UI!0x<=B$ZG;4m+O-B7a$^`#BB{+e~Rm7tH3Pmth|dJazr0F&6shoN<<>Mg7o{ zf0JUrP6_Zp)Ot#v*Ku_M|Fv4`{Jmtv+1=9>eSXj<)xzYxYbM4G+juWT3ie{faO*tGotoh;AOn8bEdN%D8iK5TDCZ`R0 z(TeR4F-*LxE_8H$!^t37U3lwYKn2GQKC1pSO5NmA~R@3@-cL34&KJO*yx9o8`KIQkCuNAC5L;-K#f$J(R=|W3E$21+*8yj>Ynt{4b7V}siZMh+2kVr{5IC> zD&({@u3R8KZ1_8KEpbQP+`#eZm6!ZxGlRZCn=x&%sw>2>Y3ZijB7`N%BX{xC)SNFr zm-`VdX&7p|9-V?OLR`P0EwNbBj5XLYB4^v`Lg-h2E7_aM*eoAa^L@(iE*A3Z_ZWr1n@jjX!Y#y3(Y9|r25iF;bl z`-hbkWA60L+SrBWUlHu^;fWSyU_N@tvC9G(`$LOhprk+W$Eb!whG%{Ar%&DhU&!O@ zyl}*KOg=VV_AT9jE593pcy{OgP$nJylb3E);?g!E^KdhX~nJQ^nUd$xK z4Iqzl!3XkAEVaMK%ADEMn7)%sKQg^csGo3px!~Vi;FFNvdFR+jTk-w-PEO8b-eHuV zE1o98ir88cf$rg==ouM z4?n-_BDHFLX+I(^EbJO1DadN_{5w@c0u^>*wKHGon zCz+RFJe0C|XJdLNNw6Qx6KLD;)1m_+&X0>Om3$Tk`Ya45cZw=KeP$Bz9>bXBAow3gl*nhsQcpiB+s>~x`y{~O93n1Wl>}Yp8+2O`t4BI|de%)4-zg5> ze~(qa;Lv(f%ruy%-Vy{yi=yv+4&yP_((GvrY(_LR1W&(`x$lRm;xj#+ z$a6h1KbDJ?E^+Dn((;GXj3+h544s{C+;nBRn0&uTGQ_g$eQhpnqj+R}f{Zn}KR7x% z3UR-BD;RxuJpuU4@w!dF)@%#P!||GppWsOH1tZ5xSU+ml3;b>`t-A!H_fERDHb({S z-X`ra80(B%V7AWX5H4T4<&$rLAYj^9?Vw{@wtnVFM?)_eYXz)Pyt)@k4)O_eO66Qy zo5aiOTFUT~2L?i7S*tt*e`lM#kMl>|XRWP?b$S>4M{VM5tR#PboDUD{bERtIA68OT z3Zd?}c-i9#5pcN&F=4n6bDoZG8F6^ziggENQ0R(zg0a_cUN!SrTZXTb#|v7Pnb%_O zACJ;7b?raAg$=vyU9q;TEFV?)ymgf0h0N_VAz_p7?KVFBlwDwz77TiPEDr#IBQl;k zn`hoZF`Z<#?{=J8Mx1%xd1T~-ClDmGg;F(;Q7)u7fi`)57B+j%Flv(6haGo*;+tIwW8dyEKQ ze6Yec6ZYXX^rKM3b9h2wP%WgUT-r6F1h3Qkg!%KNgM5jUrvI0k0Ca9PgL)}&+Pbm2o}ENUs6{FUWe z`o@$=68#IwNlV-Dhfh#DO@A@Snh0xIEVo>`@XDX%)}>@0h}p@-1s-`4LPPUt(i0sK zf2<|xv&vQ;-!)@awY_`E*WAkpDFWuT_tR6J%|pM;|Xw;zIUV*9TAE$udd~#eXA6wqECAf0M`ah{2Q6GeL^z zrPhY7e<3UXT=2IKBgn|OsqUURfAo^5+s@bH<(m}f{GsexnG4#O;a*xFpYW5NxtEZh zmkyJmoR|K{b-DPA;n<9JL^<1MDW%@%4>i~0^EW*4bp+NHQr_)MBt9>;34sM-mu;L^ zk`3633>r`>&w#wO)(msKJ=(cWpt-_bObwSilCyCi?ToB3_G))E3F}x6`Grk1TpABZ z&VF&v^+%{y@1r2Z`tC^k+@Ks`4BDNzr32j=lqtNcy{O;#+3KacMLg!5}t+FLLFn6=IIPp$fXq1$9*vc`coP#Rs$M@-`!q?=t}# zw1QRg%C@!$Ur}NGSW$u4UF^H}X|r63`B}1^s*k$J=6A;=NYpL)c1oKX?egKb<#GpZ z)W@82Q#6iCDg5{>F{mvR^+&BR+V@@>C6Iue#)i!~JM)Fl=LI%LY|Qu7kAJ%j;<1>V z(@2S9+UvVE{$tuf@RroV@(<@5oRQv=kLPO&5wVrUp*AcxQcbR?^UKW7Wr!1zq?j6Z zoIlAd?4(#l&NAXf@&$%tdD1ICZWQw~EP9RVyN1Lkhm(y$(?>8cCmhlx;vJh7qmC>mni@0(Ue+maRAT)@~fR z-JxlqP^Wp~&67sSLwLPoYGF7E7Ii0QOaUvmG=wknucXea!{O`!m&tjUbNzG>%c(+%? z)^*EkXR&L2GKGU#f+KP^24dgj)wkwP-)5f)!FgRB{}RS(mzQukPWrMh$J1)9oB;id zSHit_IzKsl=vrEP;}yJmlp9GkwTc_=sorZRG6i8H$*sl&kuYREslCuS$2Mf@!y&E?Bg@m zp9`h!6r(5=6cUO6vo;VFN6BA!DE*RdA|k@D^2TtlKrsI8hyq2!G+Qj zsA`V-~pOCrxqqzuJ-F_S!rVz&?D%`!0MNL_MiLK?jKjewlp`S%CWP% zUPv?ZnQ31hwGLES(O9?NRbR~(;MPuR+4`+3&T!MU%E^%kv-Zk$?WTP-qy;xmsD^IH zl>5Ka&wblK)z6BS%mK7Id=QkQY9@ar1HPVrhaPpc6 z+~q={Yp9R_tlRNHIHP_hkw4?AAjz}2yNi#~XZUMzQ{G@rl(Hp9M=H%9dU7PVV`|B2 z>_t) zOhYgWfn!qCTV`A0ep9MvnPo-anVCT>F)q?AWbQ6Wm7s2?8|Vy6my+vpNx+(r_BqfRW^k#b_(P&zu_3vLOf^qSew;t z%&C=wpW4Y61g_A6K0&l^XmnT(e&oa4!dvCuwtKfU9rau)8Xahjhk4X|_-hLI^M8tO zCF_eK?BIg&EM!v{1Z0aaU(sZ){&97|e9*NQ0{)z~9wN^nf?xe#tzu&#A4JhBkWC7;G&4V1usi@hB(ChEHSI2fGw?7VqY4j_GK}x%>$4ead~Nj!<3K@Gb$$VFa|qG^QTF zE+_@s1Pq!1`i%i>f57b!{z4l)>qpX49jG2?Y5+dMU&izTT=<{^<&l5epY1w&u?PA0&nxmdp~e;y2km4hxw@7xOW?XVlQtD zTplkg9R;*a%mKgopLuK@r|m$0%Ee@_q>d|Trs8+LL@^3<=hq_FegNItxp#h38*>*( zKtx6~5D`TMh}Y;n!ddIrf`5Fzg@r|fG~ea%%YY+{F|xeEaA#nnw5(#Z(XaT% zH>)wXTF;i%Zwrl4t&v4IQj%A`_f+})rNuHh77-TYT}BwG#>a4bHb}1%AQ0I7G8qLc z%?3mCVJp}yjY?75$YZ^Fm*XAJkX;zza+0!SZPWv085_TD8NS`0u~y}0`0K_9L5L+= z@b9{s^vnl3M7cJ$)jpQu#)B6=%B-^^vssd^(6AtCY1 zn-tjm>bB!349nu=F!yQW@Ki-=rZ8$P_}nPl2f{n1g7z?bGc!)Dp2m_)i#lmtHnBV{ zPm5&DnmF%VMS?0)6z}H`$G~mUc-I*rxSm3qxkgYrRL>`s*$V zDVDPD@9JA0?t5?fz+`tVct&sRp_I@M0kiM#BZ|Bi&h^%FcX99(j;WU>(_*g1tv3EamtAfn?4GI-w~S?~illdm|`yi_3$ zG-jMT<&?zY^kO>rhX9OaU5wq*U8^Sg(v6eKr>1CIxzeD-M8Kk{d|CJU9;hIwoyg+@ zdUcr%*87tx;IE?w{?@=q^ zI|8iVfx6sPtN-DT$aHhxGYNfBZaKoyR{Y{q}0_x?%Y5Q=qI0h*XUz8{ADmZ6}!9g&pDmPRO9m#0iiv?rkbF$Fi>-QVjL;Rvl;j%0_!293jkGQiU~5P5?L9a) znVA1!jG#)}o_2V6ynZ+)^o>as^F-j-V-R--tslFZZ&khpb}`yWE_I|~{wHUqme3#} z5NskrIA63FppxbFk(3Ei|U84a12I(i$j21(TliqZ>hf%5$3i~?Kh~MtMbKt)4a-j z;IPymSmiq7#;ia1GNZ_~Y_N1_NvGsu`!oGk_13OSrcR?>;7BeT1m4114ThOlm)DAa zib^QPon<0}y|arWTz$Y5c5xgy72kUUFLugP^uV0?$&;JUpFd~2EPaN^9n-;?StuC4 zv$GS5;FWDM;1dbybhe z#0ADu|MD$F+10{5iv3&dNdf263~%e&GVWan_kdOA$^IJ5sM`swwv_FyuVQ{-`7NdV z?d&_JDiN4q%UZKH+tKrpdTuEjI`Z+_^EAFA<|>I;`FV`SfIxr;d&U4CZCaI;HN zHUXpfR!ay{p53Fax2wnEf4%C&u;7UpDE8$Jm~n_Oa^&P8zO&3nO$JpO1{U^~duhwa z2ukY3gxRY?im_3f>EVs03* zkrYX8zBK*`N5TaR+16u2mrP%Qp#}cfz4TjfZbx8~t)-*Fmxn7s`Irwt#bV>9hPiFP zAnWTZ8<=*5As1=dh#JND606J}d9{O&kNIex1u#&PRCnir(eR_{U9zqITO;!F$ohgI zG}LA-Uod{Z9@@zy4SI7@_{`-?$0I|p!6H%g#Gsbx0zRsXOJ-CYsMkj=$L*H0ZSt1> z2;V5+yAbr_T{7gugeMBGLJC>=!r1oUo=(Ji7H+SXzM}?w*!Rt5-vJYgZ|A#qX=x@e z>gI=$&Az0_4G{au^5&8eXT(-@Zp7Yd2e3=jtrkI>5nca-DxZTBn zXl}2Y|IlquKFb(vXu)jNo02Qj4>I4JeDn}Dhsk3IX>zkk};rmp`tZO@Jc{dMg>-}}F4vtzZ=`};QO0`fxR9Z-iG zFaK(>@zsjVnU`B@%isL#p{7`b*VZ*3{{4(oVxPI=eV_=+@ztM9e?F_1?TS8?u!*hdZqZU&#l%B?4Z8JpPkxj z`)ALyMKe7;bLy{-Zk=t*c(|}R617~&@$ZhgB1UWa$u%7mH~!C^EH-An4{*4M|9PEF z&*Gl%2fIj17blqH-*@odVfyz70SfG|`~K4x|Fm8F_p|&@+W?mSKfT9)Y4cD|IR1C{ z`!e+I!a>;YU&B++aK4umAX>!n*XN<*b`ACY>67O1^MAixf<6CKq!FA&8~X3xNLfgK zVUc$B@AnC2`cIGSed)F0-;d=T^2C{o2p8$HJMs1JzU96@rN*$EObhL!{@od?x|hbq z`v-MY?)*8dQ@*g}^ryyYGayy%UpN2Hclu9v|EKN0bi}_q_P?|N66~+{`ll2BQ=3hW z%Z;ahO_r7ylOZR9!rM?U+K2yo_IXmm9&OimJ5CT+{pYv5&x!c9Fkca|Rh|25Y8{^P z<@&ovF1~v%q;H_5S;)zLfPuY)eJaECrchz+?XT-M5{%ufWUDl-mbwiMc3ciHIH{&s z9t7u@H|kxnrR=!7mt&Soxm*@RS%S^S1Gdl@(fYM&2XmJ7v=c8*HH%bRPnN9tB03GZ zBP1h>-Wb1yx5+LJL{9Yo$rf-sHcfbD7VTSR;D4GYc$tq5NL{Ph-|sM^odW2p<-q2D zrTB`X!QYDq0GKe5k=dX`kf<{$AvrK1%RH~BC-5SFPQyPM3Zb>nKm^SCu-FWD^a9MWd!t&-S<`Uy{NN7*hl zFVc&`F#^ah0f4=J>{b4ED|DA;OZk<01(BXNI5;SZmL0!8;TaSdSSPBB=V7QB zBXrA+j=dF(H^^2rgU!yQ+^PYO=%{B=GDZl5K?bUsk5r#^1b8UhTL8$K1ro=1J|AN3 z>gfpu&>ZzUULx?80H@>X+jG{(89bX7ikzxJBK3<8 z0}ILk?F|8VX`1moqd@RH_Z9Ds3qk(P6O-D@WD1CvU<_Iwb|}y0O=_-{Jw{%=b1@&B zyw0KkPls;wX#+cIL-5xpTT2_8chVrf8-P-K@6^Lnv|-02{j`qkxcK~G6cifFbG>EI zZT~n2rZUFLov^&TypL^Ff_@H*n%nrazqf?JV7$STPeIa_m3u<|aJ?6fQFZ01>nSlU zBVfEf=XMMAJjG6IJMIsR=^$)~b7m0(kUXnEY>irbAp+bOzgXk}Q8hg9vA^3lAPkEA z_5m`mO9xuuEKUmk^Y<)auWlb=2j$^Gicc6o|M8Z9a!{6&OTY5M*bSg&Bn3p7J11?c z@1u7>-dG0Kk0IzZid=JUvD>?7#`WDi)6}|V}L6gBPikl)6|BBF?r)AFkG$ zavAYPSi&|b5$@&2!qGpL#~TO2)E+ak1A6N$bCoF5RPh(lTB}zX#loM24_A3fpx^(6 zdQYV1+^v@IJJ!X`L@rqq$?X*FY5c@|WQw<=($Dh*fP!UyY+_%|JKW{y<8O@^onki$ z7{pS%SzmIo-q5vBxYk|e@G4`jugYOK&JuhmRLnKV@@T+zU%Fx9l5)2?Ir^$vO^`wEbo|8u&{rm&u*uyMPIm`P-EBGB!6GOcx#p<;OjGa)TmITt5J(<9#(2B^ zhWfYfsRM4ctzRX#8e&sl04wzxpmRX@kx3-uk?NP`_(^k)K8`Mqj#W-KatQ^Kp}=Vx zvRG0KFo9G$Xo}i-J$@%4^`Oa!3^#!L~e~ZYrG5#o-?G)7#6wd9FKB{j8uJ}A2Yb~ z{mo^Eb8A>@cd$5f_d0X+2ETIThor+`b8~YafnYK)p2OEV@i}3hpyn!P^`gJx-DJHy zmyud`0K<>@^kBmryExr~x>x4xB@WqFB~2j=lV=9a?WyB<)%uzm&W9(;Yf)S7DA~u& zgTVTIlNA04xbd`%B-TL?{19@hwm2tLxIr;)9!}qdC*;4Ct)2;?bs9$c0?$UNu#=~3 zRxW)HpTwiU@#7XpUpjK5u%uL9_DqS|mZRDke$kA^`(+Wd_1t}W;jQ$Gc_#bq)(ulK zll;EMZW47k>71kb$qeH~IBgOBqlzaiU5&oeYiLN|Jp23;@YzTFK9vhp@UJYKcSLTt ziyO?9(iU=Q3#I^e$Js>>Ap*#Bw!T~MRx)Mst%<1^`>R8^;d0?Fr$dtHY7imz^ZUG*-S=Q|&wHyn2 zhPH%Kef%%X3z#r~dDoPOtbmVx00gmmx3Q#`GT&IA@68e&ci07fHgJrU-*>mUDaU#H zUFkU~bX9wMdt+l`X=w|&+sO|IVT^dhNIt$*ZQ!7KFK!o9MUnXyOyYiZTS7Op=u$qw z%lZz!%u?c_qR~;uND-Mu<@ZJ2C+e45KD#i7$9E_^-!GMTy2Gcz=WWR7Lv{63+qV|| z(kue3-`;jk-N<$lihl?u6d)dqIN6OjwJuFM7&r8Gck53{*7)Ij0NDMhD|k0K5~bG2 zeChYz-d>H+F93`IZ`;+~{oK1d6anzqn1`%&`F-!~i@$4Lz+Myi=3rn^)rF+g_ijiO z9x(1&zWw=rXW)Q@<9)N!Rhw7S3c1Sv6>rly-%6 zqX8LlHFe-z){)x~j%!-zUzre5tJX&%oh1WIQs!ndk2gm_STx8uimwtykn^76W_$ua z98mzH$#mC<6Ko>)&XsSzH)eHC@G4_8u-@_Wu!>dX%#ova2rDnA>c@yNBfKYJbs1q64*({yL1CB7ZX(R}H zSXJRrZ|WGGPyoa%-|vK?Vn4SyA6#>gSKKCRb2^KotB;RQY3qvmOG<@p^A>S}Tb-`) zDth%R$1p(QHFqn)S+sw8u-&+ue7E<`cY03Uprw#m+2Gdl$iAGOa&JbX9XwY##J5G! zu-N7J4WeToM|{ikw9wHSN(~cvQev#p55Hd-1xy+h_q%tXJNv7zQ@#<%P3(uV?dA{(u|^D>XCyUvKhdH8wc!D5MTm;?efVq ze0Lx>CwvZIb?9uGdB3#QSFrsMgHRN}CZ!ACo%dxYg0d_fk;@&|a`$TODYbU+;@Xo| z|2o)f-G`iTi(_H8_jgwTwVlpR{C)zHg5Kq+u0@ayY=5UZOZa&xSfvsB{DfK~pvt}I z1QZyYOK-E{cq2CYeC!8>EMqVj3|Rho`$o{-jESMj=053{r{(fpa(5+GP1y_!Dc>wH zDORPeCIN>=@ZJnCb-ZYEUY=uhV#}|9SwjQh-oJFtx_lGJg%zt&7r=6QnY&-hmy9GM zd|a=w6HVSvWc%gerc96j%XGz#kK24`I^D54GjkR*h=)<$5>fyw$oZ~AR{&VzjbX)(et{&X<;XWH1FOj z4~etU+Zu(Y!$i-sr>C|ugh8>0!~QK2kK>MaR(Xs@Q{G#uUv$sGr>vKgBS+`P9>DD2 z^$iX36@Mr`URCEG9}<(5ei(Wt+500O?6o(4W8;N6!2Cbb0TdP5S%T?hvgGWw6L$R) zrr%m4(CjiQkDEH$F1~!-N%sUszu>P`iDLy@11? z6%SkK;w%khxcE0P`HzId{}vMBABg+E0)_vLYybcG0{;bu{wHYqpW6NtH2tUTpKkb{ zwgEMt|E;9c|FiM_n>P7>3EltGT`gCC4up(=N;drbPS^Nlp1D{BO6mxe%kbwUS*98f zRK2-68eTco>B=j{@VP6io8#u!5*akC0HRy%(J+L?nk;2>ao9slIP7zkPb{`XJ-J0~_Rf4K3}X@qeZJUI5%} z0PDwcP*_PI_hV@(j^u1P_YPh45#+jtB0TGr%vwRFqlblj+p&^Cw=SM8$;SmpKmZJSCW+~Y9<)uPL+s~T7AZ%hLGPtAF9&BP>$rceQq@N}04X2EZIQQ{qhw4_PQ}wy` z!}}KEY7%tZN=b#xz^{l0u=GLexj^dX5ES@NtSu}a%z4&_0hOzLFR7WCev-Gg=DyG7 z*&BB@1|pD>;LrRMey-EVDEdAcE$x%EgLKHTGbjW2%gD<6anMckmuJ1&>I=htukL^o ztdc8aud9t!ZJb1o;;S55iU8*vw$ab|C*Ob{W4n*CtU>U~_sdByADfvC(sL0(kCU%9 z_w4366S6G_$$oPc8GYUT7MYczoktz5>na~{dtAPGmdE1Tvq=t{XU8T_vE5oZsLF6a zJnl)oCR_BO*oKi~)1SmVPuy%Z|Lh>aQ77u^WROd&DD4^#E2k%JPiE3KYcb(%oRGUG)1Iu zX=`ius?qm+8xMO21Z<9job&ZAso^jKLqiY^6r%PDJfkXoZynfI{-S6A&2oW6`t-D8 zG9x3SSLAxuw9A2&ucr)|+dkP`H_<`Qu8Dbt=!!Bu*@td3Ghfx(RTULO$HuUMBrJkz z6L0DICVU_ZE6qf%Si;h_>Pcy4bKj~O)b)i_fLk&)Hnw!JmD-5U85@)9X%T%qwhOsq zMnR&WbXpd6qbS1e4E^`yA6ThdZ z?ofd3P@^}gsVCyHQ=J#4YYbf_G7i=y7&6r*3AIC%JV_=pqEw_e;pW=6s%^;k=V3zu zVZjd@5u&9{BOXl^WwpC^uW9NY3tArQR5zo7YE!$h4j*cXW3l2`b^PME+B6a&N<_Vc zthz7e@QKl$Tc>!5>Iy;5#;9--*OxZk2`$%}(N(dnDuvRyaymlmS?%fIj}B+I?$tJ5 z`nnmLtHGnD9^)|FhHVb9G?gjzceFGOU^NttjNY!=-oa2!qU_( z7|ID@jWrM%;rU}6$70jN(9_dnU|=vlWvdec+#-Ng!KbZ46jW4H6cl{Qx+ItlRd*io zh%xNl^C{EAk$aVaR1EscYQvL14Ddf=FyPw zbZ#K%mPReJ<_aYhl~?QK_Ss2^iQsN6KO(^tkjEXgxSk&NVS;SX5H|gbz6V@Vr*gNy z0J~HVo+-C_0&r4GW|o>C4V)Kwx7h zg;FQlCecEtlGNnoSAHc#MMdp9f`LJa^7Zy+_yZJv37=vD?=!=TxrbWiM)kFSD7}6r zA$J$Z7_@RBCyfh86tJa?_i&8Fa(tExZ>mTs=V)9tWD=g2wdx%iS&+8}Dzoc!Ynp0Vweq4MhQ1o1#yyxTH}hf$+ctA%-c zPqx}>+J(%fAwn5u?~>@-1n1V*I@M?vfO51%&8l|gx>FYnk?HJ z``tqOHG?MzE7)czRYPq<17NK6US~rC167b<6%L-)R`g7y5A``nfHJ=lHhWRBns4U~ zpKInfcoA5=`wvF{Ap)1ypGL3wsgeDNqNBMGBeSH?4K89w@BXVS?>6q|Y6ZXV$`MVl zx5))0P{opRSswtSop?)UuN& zY79jQqI<%7gd@-Ge)HPyy1%?OYezb5;sf)JaZv8o#^ zxD^)A0Sut8psQTaRW&-EzI;v-TTC`l+cW`FiHnO1MJCg*@SRNf>hJGvwY$53f&snk z3V(Q`viJTu+=q~BADSUsZtIJe2FN6wnu5ZrNXP1GCA_n*?~VN(u$NCb)|;dI117+f z1|w$Ez<1%oPL*i7u9_Bcs%pD=Zh6_`3=wRU^Ybp%BQ=kg(7yiuGv;romS@BnbF#Au zC&&c_1rr&P#80DTkaICp0IY$#xMn(?Aw~vwkyjxjWlM=9=W)rI)`_mYuV6b$Nl8Ii zok20H?98OlTD2ZkS~NtsAej#g*Kc{UE2ye&$-UtwfG5L4GK%@YO7+44GJWQ&mhM=c zP+{F;#2FrHQN@}j~+o@H`?zx~zky`b!g8`N%GVrn{dQX#) zBrhA!pbM8Nm&e`9@pydmY4XH{#3c5K+y&ir?njL_Ik~yOuxk7rIqeAo7T|#>Z1U2; zb?34!KmxrwG`+a!hKr^Q4yLi%2A1g!1pr20uK$v_xcL68FwRVGFPBtBhtqg^wa|bW zYlDPruS@}U#;F?Yg2=n6Y{bi0olT(DLP!I(H9;Hs$aE(*D$d?i$iN z)qe5DOjUO9h5<&A{MZu2HPk~X_09Az>9cq!4G$#@MRrTY#W)1dWjN;m;xJ->=(KeK zw|e0^c&&u?-}dw+D78>LqH~%M>ky2###XtXA4ys3otm23|DoP~8%lKK(7@C+LAX0450vS&r`7b2ay_WT1^%Hq;x-sBoO|cc8m;`aVwK}_)>jj z8OhnV$9}gzJ#w!DI<mBsA7@B1BScY;3{@wcwDDMqqA4x@`lj-=c(BmCD_DnX7`>rTsE7i~!MY zpeDfi$JHXDM^niV1 zVvN3KPeZR?Fx|{TE@ZWId%{m#-4{4RO)DWCk*k2V69+L7%ZxEW!TX)PGC-o)#Ml99 zlSCtuXhmMTMLXXBWxEKc)}rQ$*$tmk5=q;M6NXWATslMnwYP@GQ4lx^7>F)8kRXWK zk)zMqu*<8e(m;S$aR%5RpgFn(YyLzZSsG7qYoAbqzO~hm+vRH^6T$CM)q%= z?=Duj&BzWGom}FHpuXia@1Tw%@q{D?2Fo>1p8YN5nFpMtLS2J^xozx z$ZEh?JtTuQZ74o*BqE*A+2|kHiBw(BtxtzQ6H_Kmq4%~L5eL^@oms^-n!P?~Bhg!> zAM2V$*G~&*qF)UxAZDQO+rPWv zf7?_S+OJ8JPqkIDtWX z$U8EUCWc!R+uxO~b?yogrm9|VKz8c9D0RS=N=Z4D3`Cx{X(T#rpLfQV6+o0NXI2j> zLZ@)+D!6rE5s0OuQ~N&S(R#M{#Kc4hJA`FKtAv*}Qcky&b2zo;6SKf>QRo z0?_*{m?oegw^dh9{hqYTHk1`PA7&h^pSErkO2G_3A76q_^RKz|q1GE3 zNxSYrBf^U~^~ciz!kcomhw$cv1A2qYEaeRecYw=ffWalR=<(P)KOI-N4mxRkaNG`N ziRq|%{jvaJ-XRCvDf$3N!z{;1V(Xd9*O|6*id>zhDoQIw6LT$`rm%IOe0Jvq#5MEU z$u!$Oe*VJL24=*n%OH|F#^GZF3WR+d{Rrc1X)$^YrK&Il(Is6M#?uv5=IGCDX>_c{ z+mcc4-Q(Z)?w$%%CEXvS3W?Fh#n3rS()50!1w{pJ_ zlZR>YN>AA_ozHEUkiWcLm1?|2xEN9n@#$vQ)&&ZhzZcQQI_F{>a24?DKk?N+KI%9F zuBOWCnMaW2-jl{l;}Moygr;et5?L^112S@&*e0=@=bfA0K6J~gGFIS>8gAFF0;34} z5)EagiGNR{C)J&#(f7`m(&&)9IoXo7wzgT{1+FwO7wJ#Jyy}yY?rO@)%E0Q9 zmuL7HFtM=UDvBmny&m_F1u-g>q732sk_ZcL6X7Zsksa;o&^#mlz(8VO{lg)MLra5$zM5m(Y+tW#53OmKn z-C{Zxtb;JK)0cs(3q>76Q2`PqQjnO?@bJBA0E>afnsbZmN6fAltsAvt4PEjgj8$z! z(DPU$=4WQ!ry9GffQ7Ki2ZU<(jk}g^AZDo=5FFg#C1WZ@$sz6VlFG_zucxD-+pP2{ z-~hjxe!dn%U*V&*J**rqgBT6AKU|%_Q77y;{0N(lz%On)6jBTB$48%rj*G-rq`x9f zG?TM&JK5`s%xx=4v_7>_s>^Sa+}&Tu2+4tf31B5s_M=eC26Z%jdw>~4-v-yt`YLeg z1J{=!F>PxN{c|XiFa)GGNT{YJ{zeN?RdQo5H3hn67#J1^{Vfpr5N;sK4~(kKecZXf zFNGpxtHH?H@3q^5sDi!DfdLaPB0!#Mn}E1e8?J{~mfz>w{>%udF1gJJaei7(DA+r| zAZlQy&@xTi6U;|{Z2J`r1kuEmdzF_0=w1u(QcvIJiyZTO8pH0&QYDzOv}+eK2I2hF zH^!qLvip+g2U3mqc6Z%Dyg}r`MDE1m?CdNEopmys@Q^JH4GrCW4j7Wzbo(t-a`?_{ zI<;U7U`f^sO+h3(9Jw^q*LSU5JaQF$!R~r`By6ug9XS)*ZKn{|wcO5Szma&uOhs9F zzjXuTLxz2cn&Coiw_Bo!N#1~v30`gIq6V)_`tv#@+uPi6kN+I|&)l2U07H+{}Ld@j+wqcBfFz?+I0rJ+YJD00w~#n-luun;7wn@ zehn^`Mv@v6*47x_$Nka_YQ9*mt$F`mpiu2uWvT#08xz|Z%ZFx85l|==7rRtDW%$@J zDjV~Hxbcc7Z#=D93Lh%z z00D+EHu4Cq{xQwbkNvo|29Oveh``a|P{kD0HUO3GuMd1H6#N7b$^dn1(F@F7FRBVU z9_eqM8UX!t1Q-7a6BCWCXWCenxY^z^jYm(vw?N@$nuvbj_Wes;WB*8u5a^cCR~%IqN_B+8kS@dRB(9!)sMcH<%_z$b zQh~Oc87F_-%uZp&n*@5@^sFhhH9c{eZ|SO@F~`UtmUgg}-M7*@UcOSegYaEbINf#Y z9c>Z5a{HM5yF2fh$JyTO-rh&<>p+qa6nKfat$+cN2r~J2Twt(Ii)ZbhSMHy$mk`E{ z)NEYJ?1E(Lzw!Fk#RjTcXTg=d<+z7e1eVJH_R5pw8r#?5>!lDKN`AB@{@~)_>9f^y^KPXh1k_Ay^?FNUI(hYm6eh;xh*4L zJNQmRYfkBD{+W$1pa=uW00jC{{O6z8S;rqobJV^^$W(uz{k|O2w6C=x(u=J5W%SL) z!V#$Of&cXr+-wqk^&Jot>XZ7~AwrC?=PiqgwMhZ*1vel;0q$?lmq+cDl?-#QPmb9w^R#}G-*snACOZR!GP}oq7U%LEtNT{E&kz=5Ms&E|L2!B zWQ{f$Z(3$Ow0(9qZLZT?EwJ(J)B2u8&I=uh3+D6I82iGN%q0ri1jJ#z$%gS{p{Z~X zEH*ps%dMVo{(geVM#)~lZlQ5U3$(+;ZDHm(U2O!+NHEHw7%_~M1^t2JrW6jop1WEm ze2!`&cdHVV{)8+2S(1J~*#tR>I{+s75C8o6+}BtEH1d~i0)FtmKinLMe9&wY0?h0y z$jf_=ZQ}a?uh09{S31DN(z&Jyq$kt+t3chi%z1Dt`|fNJP;^PzA7C@ zN8A7OS!vw79sKR~61b)~#rTK>X_d@3bvi_u>l_w=Z}dYhX!Y* zugs6X98_!_Rs^kq&sB%>Y03Em`H+Y6`=Cth0U&WkzWVdxl?)*9v9x(L9G}PXy08aq&Bn^AO*T@@gR@O@c)jHtwrjgCY!{eGX zU=D6G2N0#=7as1f)i-arc6D{tSo(453jtc|xx-}sSAt)kbU^v!W*G3VclYN}B|zr9 z8Y|lJwn@l(g2?F=;Jb4LI3P7TqGlK(4}p{9e1vS$8e@BqYMXJR?TYJ>ZwpcOJ>X5V zJ=?WC14OR@>C z6*q{H8bT#b&Z)G0a1yRB{AA=<+|qIQPt7SSU92xy*e&p9w~5Oo_qIJ!5gm@Q^jHU^xWKm)9~cBXD)vwO%fTC}(2@;)F7*3>}#dTU*q0zG|h zw*vZ0aR2JjoFI_*y?wY8749^TaZK?0%pU09E&wDJKwOaix=EgoD0?L*l|_tia+P8Q zgwC-Si>s?W26c0S-$qWYrE#P z=|24E#tteGtg*cUK-Gs+MA@&kbUY^&Bw<6?Q^UGbVvz}?h{GOFt zUefppOzb|kGd=VgD9^&Z5_fp z43gFmy_asHqhtkNqlQcHc3WUhyO#^MJ(nKdB0P%26p=AqQvUDD`#F|B9T_#1rgsH9 zswdPLCSo#}nJ)P>5md6Tr(XyOU{`ttw zR_)=#ljZ0)Qk+-l)cYis7r%Xx&{lntDX(=;P}Q2o$9jzmqvuk}n)GO}BG7zojX+_t z7E=*zzhO=Ay!cnjvSoO}t%Ekj+ZfL#@^N0LU#c2+b=tYtAB`N-h2H4xxUBwsLKrB1 zGqidd$UaMT>D8D(q3nlnX49G{Eyz@GJIvmJehG27YL6bKFdnn<;U`G*t5aztej@n7 zd0#t@-7H?Z7D!KFr)n{DyM;xy{;%X^D@L3Qp$wd zU_c?LbW4L$D6ke_sX+?qF5{a_JuI|N3_1-K{iTkUWpRk4TCEGh?S|u_+C6SB^B>^D zwQ96t5rM*Rd@BeJLuIzl2-#yN@Zutqv2I$!A8|`ad#2|d8yV$)6*rGd_H-bEFPI>I z!PI4AEavhjDr4hBrk<(t;fR!cUw&3Bm1v~S@Y>~8$woZZI85}eq-&pnD3=R?yaLd zmZn*|;Rv~FEb-hp!WsvVOc5&O)<4hS5oUOo9c|bpurT$?p|`NfRf^{#^B2jPngh9dtieyOXxRJbS5)T$aD%dIpwMoAbJ0eE?Fd))Z zk<`wVj5-1grclmWk|`JeD~_4HaJ%FL`I#9Y+3IC8I^}4=9}6}bLF~qTLXm6Z3sl5@ zyMSUB6_Sb?H#~Sd^4i}BINmB9LP%#t>ou)Q->^x)4Q>$jxz#5fA@8z5rGdOgHOuQQ zp4WXT1xsZHCcMxCyP%f}aYUu-p%RmByu${LKHx>lH(1%5o|`0;NVZ1rs=ETyPaboxhBRufDUigvC=da%VMMTve-yX1wa$|Iro62cZdtgkK=mkwFL!@~4 z_jYW5Vh7{QIoPl>nath6=G5&04Kn(}d%FcGEl!t)FYy_;z zeVEmau71W8Z%B2LI?PB+(z=g*AmC#h)sL5ruT4bq-_U*GRE~n_O|#h;M0~GSfEqtz z7sbm0`b!tZtfFjgg#KbV(A^sjGNKN1XKnaDIIxw$owJvo@w)Rd`UIwLeP+w%!lza}6BbyJZZ2=Rwk++8j4q@jsolHEuBkdaaM4ZO7M>_Wv^Wj&zD4t0> zLzhfa822R28bYdwi3{CFMNS!?7lvkxt%CYAFd2VqB9ehh{-|DGZxZC;ly-FWem9cM zLiXqHQ@UwS&I&*NzAL5NhnlcX{P;lj=A@4qlB?XB4b9dwRYGnllwjm`^tAvJMwCgI*6Jg;xy)f)*a*1@RBdKV;C5=xs>m5&V3ULsx z%v`n#&&cff_OW7e96xQ<`o_uXW6f4Sktz=^#HjgT&ymZK$y9v;9xVg;+aQy_J!_3i zjv3xf)3fnCIcVFSbMPGYyTMn~Sli{`e*yMBSSCf%M_2Lo=QP5u8Sw6NumQ_We3<2i z{YXPqxNwfm!64B&Qf#BetLYWPuU?+$O84838f4ZQdl5Ql-2zO@cw70>JFQXzD|>N7 zDR#8B_c~N^q#|Txwi3kHYYdt_yh)c&KH}?Nx8U~b=&|Ioa~;{ANOUJWF=h{a$2isGi@~%)o{_Bzah{BtUg7wiGhRL=O3+`K*A|xa|ye z{Hx44_Ge)(&WzDnMA@6jrA|hY>5d<_@Z#%7@M(^#bw<#DH5|$fgU+n7al`Bko9Ic_ z_qv@J65Pg@kua6?=_RL;oDpEHZ|RMlNj#^>RsHUZYDKz#Q#5$~JT-#@WGvf~Q-_iw z4=bhfl+)&-yGily6UvCv-MTO!n)fwq9QGpQVP4+U6nv!wTkB?6tFF#_#XD~+36Ha` zkhZzFZ9lap*rub*E`hKR{_A}>^DCs?_MfR7p>9IYutk=vBxgssSphik;L+&2l7{6NMnrP=OLI>z|p z$zc2o6%t-$f&*^j8!gt6DV^VxxK6^Q{$eK=l9Um>zBP9h9K*hm_KY4g?1-C38S5mPUj*Gf&QXp#`*%GbDN%oLSk zIe_uZf0acF1%v@D?_YDugP{A^wkUhD>n6o-woF5XF|T>xI!!Xyx_KH;tH%Yn2`TLh zfejX&VSoMiGvGU}v!o2vm_){nYprAk+D`-u+9*^8sRbKj)fid}6j-9*(`@3B+C^Iv zxMrN>hW!ZJ)e{ucctmJuXeT1Jl+FQ{#|*su3@Z6Pd0b8wu^lVR6r&=jHHf?FRxE8S z4jj7tF7{r%O>ok?`KA7gvvCqkc0NCeVb_pkAF+W>Sl7VpA|H3)M0Lh-myY!2+fJ}} zCjR|oXP?dCWaSVgDC>|Pbsl9)WZr1wLvDLyNrbWtl?!Rq)V=6A69VPk!tvoBkrs~V zs;l~;8ig4~GrqZ)-s^^a^lyku+*^S+{kvF$R2* zvm?;=F!f*X%y;QsRG`foQ-xY`0ogX&=NH&*i+Yncj7>74jVU2@vp#>AoeLJJ6Sy@kP@M8Rx-93aij=6EO zpdi7Id{p3h#d{p#hO<_tDLXmSJ)cz%_S8lt=UZ{19u0T0*bYL6bcy8_Y{P>Rm-M_1 zqa-ubsu7;7hJQyMfDH(W0*hkv9d^%Zt+F#SG>M=RxlyB@H)4i_(c!cyRmkrk{VU-F> zIJ8vYZn}!hrlJ+WJ1+S5CFz<1_P!Zq)kFJf;|w`)X%)`-j;Iu)Hvn8h5nZWF-wcLsoUg&#hkqp&bjCZ^eSCh zmm7)An7P`z*yQXRLG?x(K`5{tHqT>t4JnZc(nwMdJokv_8iTVjbQbY zR=;Ocoq2iw7tt%getz9?vxSA45%galDT}c-i?A*8BuNnJWa(l0z2WUgNGTw}*LB?5 zFf<9r!BEpnpG{7rF~FQSNB?zLR_(j|R9UJrJ-;~`Gq?I1-Jq!9B(za7PpPx;iOgZ@ zj>v<6{;^udXT2TH$(Wy%@99!wJH*sMe!nb7*dz`h%N>T}vZBega2>WA{~r3}&1AWK zC2H*0W0=}wa0Ml4oVR0ckqX?!4YOHWs%jSHCNMYBKHuC=RD+HolwVd3i9gofrnk92 z>)~JXX~V&~h{Oqp3vV;(fj8|Qju{0eT^xK^6%gF_$6-{tu*lc5$W#Y7i!Ov>+N3#A zW9@4$!VR)cj9nnLZ7+Wm!q_d7sg?e}wb+a#Rc$GZDCUT@W~<*AGLMvoYE5$-<-F!@ z4C5~*wdh4Q2Xmz?wsmiR(KUjien|MwbZGLV8cC&qKWKP0$vh6a`WaFl@jano#z;80=Q=mq1sK>P(+_5v<+H?fSq_70WABp&>}Qb%OV`iU~ns(u{Zc^baPifs{42qZ?9g zEP#Xeg6NP~7&GY#b?A7$B_X!Z6KcozD2^PO|Mn8+2m8+2?+^a@{Rh3_xWXR79k`he)O!vNS5l{U^b#K&v1Chr=G0F6>0$lNYqVpql z24yYS2wUq#w+hdO^`u1bZhHXm(Uo@li{ZQ@+l&z3kUMslZwKPXQ%0TbD@_;VM#p0=|3Rlji&{xp`N-aaXSSvo7ZPn+Ib)p*CQzja#PM~ z7dYeQ`reqcWjQCOg;M)a z8}g*(L!pFVi7yi{ZBW^6@mj>?Z+mPBL6r~mWA*&>ubZgYU8F*xqOb}jsslppZ=_Zr z9#iO@WN}6FQj}*Gs9H2wj)>~K!;yZ5+AY}?Ab$0CH)(mUK+W2DB#rX1TJfB#;T`vn#=I_%GbR5|)1Ik0togL2^rSuE@1W^L#CAEyj85XKe^+1*C zX{!>`3#HvDCQ(E4q%@wdht#}kzEovg4brS{<35)vb)mFuR8&U{E(tpM$a+rojR&E<5YEVy0LQ$K&9Mu^mHSzupfmHPwy>Q)%d+ z#hps-@5Rge$4$A4@W&T*{OO@6)JE|X2Yct6asd_iVRZrIb>x_gz`t6iWTsvg0)hJ6 zmJ4pwd-hb7!YI`skF1W%7(aLEuYi`)ut(c> zM>rC$o7-m*)ycm!#GksT(}5DTGkY$;^GAnw-|!G2cXTyWv? z#U|o`jBF1Dlu~*vT7~3#-c zT^AU&F3@$|Rn?YVt{cC(sG$R7fn^L(tsa$9xs>5{xtLZvq(l+Y9+8h&1F4`NTu^;a zqHFZ*k_lad#dv@9w7+Sv!CFPYFF2BMb;p#46REjM{| zU5`Ldpkz_`r>^=@LmQ8*tg41S9M%R*ZBVr891Ax9kBzL?2~aE#bo?4!VH$9uXqR%pVVn^7Z`e zUST457n(dOabTzxRn7c(fty{M(_ktc>clKwwFoujp1l+;Tv5LdiE$cg~N^RIt49fk~RB_k@Vq|eH75JjB9!vneQ49 zInNW3+_==GB#a5@ykPx#PY0=2L+cEpB1|CKzx|ytosoRXMMss`utonno7rqtS;v&baGB(n+;nj=RXy z4SC!k%cX5MYh6b)G7>4)Da|i3l7nk+(+*<&jn;-&555i|SEhHDT0cd2J$}m(r5kEQ z;gYqBcnNZkD9*S?H`?iX5%|RE3D((8cvR1)eVOyubXTE$JRvoAL@=Tz+=rt?+a zw=|Vx$#lDGn5DC>;3MgkGd`#3DV29!^ffW;2R?H!Kr$=?qnm`7v!@9nlg zuW2Jv)f}e%%EA}L_-XEY{X1+fr8bp~b_eyVovq2d?C$h@U5_pMVAtEBjeduCms4MtH*4UiZDNKzrJ+3_l*IOJlBb&cB4hsY{}YVQEtTu?a4$mSA~GM7iIviG$M8p_Ix5;qxYSR?rjry2rFHd&Cp)iwl_#UT{Mo@X+sL%W z*YnBaV;NpE;h}7nzmZf{rp(t+c@lx>e9+M_wK-1rwu_zi!>BIsR*eB=CnsC((oA9m z4Toep3jNf>qs}~cX{q&b`COK`HaSJ4<_AzpOVcy^6;pD-c7%`2ojby+m&SOQAu8Z2 z-_Uz5Sei&QIu$J{qCcKx>I4x}Wv-=9A1iEU$k47BuLS-!#&d2SidnK{YoW%oqSd1p zx7B#=x&WG;J(m$1)4#0mgU*)=OnUZXJ632Nzxud1U%$b}ad3J{oY-sPVL^9na`J5y zGl$}zg6C}P^8*b4=b<>|>*{Li?ildYa%b>}Ue)I}7c%Iid^NnuMr9B`B!@;%9)MaxZ!+%%dMtQLT2zX2bMOqj2ZF!OmdMHaN?uM+@hIu z(=ix#@eEB?u=&ATV}50W^xtCT!R3Xlk)Wr_`%L(JVUaH(7-}89e1~D&zvUkDB+8(L%+o> zTp{FoJx>hwXSC=d-K5O=3JW`v`2i8zzDj2O$Y(@|+Fa{1)I!usxL`-7!4lAm9`)UJuxjoERWRsff@Fe3!p zIJ9TDr2aP#tLL(3b6Kqxbh}MZFc7}bM};(`xu;g&l6|oe5WHNNqw=p;k57qQy>UtN z1Ua@Z_%beg_g%c_Ph*~ACe>~$7n-~^3iEk9pf)Ro=ezY$UXBcdjHw5j><&Jn4pj+D zpOR7QQMRv1w3iGHZk^ocR^|nBNv%cqE@u3cma?|gX|l2^GU*ekRh$&zaM^aauBq|w z!qNlpT_$~eoXMn$?7PoBmRe6j+YPEUZKd*Q05oPazv@H8bYYF$V^CDfS~P>YuQaGI z;0CMT6_K*bv>7(@^}`}pPg<~)Z@OKlIegc2@EUY|gfsLrqIg=16R{z`^j=ALrujT` zsix)>Ueh)hPf5!VnqkWK?DJz9`%AID1Sb*Q+zJ`zZTH`6Wfk9z{=XXN*#lTX;lk6a zNn#*e@+?^s$qO7Qh(gcD-(yN5c5JnJJU2pXwMlSGH`4c`iOZ|)TMQT@8;)$0@Yow`xScoVekIHa{8$aVX zm4Ugw^w}5k+t2iH@guRrU5eg^O+J!J>s`F;?6absiHlE?SV)1#9DO1qe}-5KIH7%0 z73L$*`A`SRKkEU;5a|bj^ty%(LYrESHAx@8AoU>{VK*)`byIXyV3e^lJT96Ls)nBu zssZ<{irbP5;-aHZ$?}jNz5FxiTy7eoQ>4kqyuvT~IT_+Ckg0XHqhf{LCNez!h zylbHDu&y?a5en_qYkbC}VfK_v`C1sN6vD@hHfPsj(boPp9g7fh^gDeJ)5awp9K+>+ zW zwtUIF__l?Ptt;1vdDtZ)Fq_W7VQrmjOu_Vg27Be~8tBbVm0zm41YZGpqEA;HRM9%7 zwcLdFW_e=``_K<}9PQ25)T;g3WtkV$=Er7~%VG@C=@uJh&fou;Jp8EJ`^!hPWmzGs zHa+D@MDJS)@4d^;3%im;>ZsV2KMnpSnpe6@7KmD=P*!rNp6pu55Cgq(AN^RL&Sp05`v);6 zBMN1gjQ2hsPeg6kLT7`bYVg5A3+;SxV;~!+ar15=3Vw!#doMRyI`!ls zwK8ONz?m#bY9szNbpOmNZ}0b=`h2bXq!8kQdzz(Z5AF0fNrt3yf%jE*QM_+)*Hb z?~V@}p+=s^KG6o{p&;L*N|WUHBWujM`mBDFN`ksjJviWOkV^X){sGz~4Zd3PQkYCrd}^YGqI?{`-Y`k9sg) z6ZGq4dx8?eNBxh%WZeEtjfl1FFK_Ljqy2H*zhh*0nfq#MH^Lx(Svr)-r|iioI`WdyxdGboZxCgZ4*t4)V@3p_srDI>CL$bcjW*HC zLV~qnKVA>-n|zW^&n$ZU=FaExO05rxe$rIw45vQNHEZ7(NkITa;-XeIw7MF zhjV?SY^fQp1f7QHbD3CE0SgEc$1%;>>B6d@(~%E`Zi&yv=V)l#P;OR=j6GsL4h zIJ;J8AN8B0WYDM)0fS|57e9(>+nFW=eAHowF-M5+MjRU6=5Iyl;YC@m{fZ_|j2-sd zu4A8aW?2Va5(~Cc{wzgvn>RD8HbqON>@fSbkw(Zn5)PPg{6cIvb1h=T$SD1PK|l7c z7H6tlbf2#CW}VObra?zxRbjG=j-!hrlkhd*wbu^&-H$qwQbuN`7D=r{>E0iX9kP0B z@C(|6VZH*r5o&b96KI{L!sVz?J+jkD#wv#^ zwajJHy3TW~Zx^0sEu32RASJ_ZCi9HFTO0Kvy`_CUBSaGJ%0!<>adTnq*W^9OD3J5s z3X^Dgno@u$_@AP_Ko?w=qYz8hBr5_MrC#JKy@oGJ7wDsaG)we9;P)N$|TszV&5lJ#KdUN&=?uX+7kWn1?_LCNQkZKQy*p}4I zKGDzgVxi!_*<`GTY4`on1^&s`t-C`rEkxVf{8_|Qt<@uHD)dcZlBdSpdYVkH-|fGUw}r!98%1SH zF`z91jl+3L?tV&1qBDN-|2|1KodxgVLkX!=%kCD2pQbvgDJ0i2y0CN|wX2^8P@&Ii zbMETTsi9hBzBq3$ubVr%t?aUUi9Z_$|9%%*AknPmNu9EcX0TwEK*4GieGFpn@@hQo6Z zI`zE!uDiM3={!!Mi8gGnZ@!*B-9itJxuX-Cy%=*R2Kxh zH;|B`%ToJH1)3h<=VI>o?~b8}q(|_|PK<<=YBQ`Zx$D;G>7ljs!OM-Ccf63gg>5`1 zuG@v)QjNOAk`XRGCJ>1r2-@VD>YviiKCP`!KYrIm%w2_7IX{sHB`M5jVHHbi7!Ee# zCIgx1b>{0rY7j}<#si)-um1LFJBt>@CO74XWvRc*KiNJK z21|Ok16lG%Ydv5YnRfp9^V6>zZ89t~UCvsxDw?rrU=!}<} zK1sYJ(Anq$ME@Qcz0_q7!7H*oZql`J{u?zq zL8DdNgD8m8J)@6$;tTDx7mx_Do^|xQoC9Y4LvdL>- zei>Rm#*bnB(Uh62RbU|;zQoty_@0q8ar^D%YuCDUsV|Slt z$+F&i?GKBn6lT!}v$%J-e&jEdfUy+CR1$62>3&@WNb`%GJ?qn#dGv*Y1i4VyfsL(F`6(FEV}*@NIue3LW-e7?|LBW*)m4Cmf|k zHd!5CdG!hhOaMAujLMhN;xT3ffiEwGetI@DNQE}PqLxT!hhe!1W;XfoM}u(&PS`rK zXF}oIAyn;y`#}>v)f=rRA=xQL6F$<4i*Ks{BEb>hkODj_YTx&&`!2o604DxV*@T0u z-+K1G1I&=#fN4r^WajPAgqoV0P%q%QuUI>5c@OYlR{fZw^4A)(y@8!V`T(Y6=83_< zkP%D_`u^qf^?~StVCF_ort}QkDBnu8qn)eUS3DC1>CE zH1Wvyx{7JeQ39GjguyPtMHJ44{QL{eCPV=&xR^ZL)%wRYiLOmteU9b!Z&Uw`p;8WR zu;Ff;?*&sqqXG(|6O((kw`*VLUEJwwT$@$_BRtad`P;N89N!1TTAEnH?iu!?g%IQU z-kHhC3>@H458U5W0OrR5qQ}t=2W`@#m%pkn8yj0+FW{_M2S^8is|+xxD1FHJj;v|; z6#x)k&QXlMzDs7AfcNm}d;()!_;5wDHh_Bod{%x>p9*nvZv*x)rC%Sf1D3Nu-WeJi z8uytezP_d|uG6M!nb>pR`#CYTWosYy_MCYn?e-o(zr+n)WQ+O=toyhlCpX;O4dU|$ zoRQ0k2U@h(l&ao(7=RUaM~#6WcMiP(07_ePD`~mFIQIN-c6Rn1;FsBOeSUSKJ0bzt z-`PcwC$l4zldn+}y;nh$QadB4EE^#?F^0)9CXE5Hr_7@rPFL%z`1*?N8@`vR;r z7|>KY90L4cAi(KyZUH;r6|}kWA6qKM{Qv`?EDH;7{l}IHa1)LJG8SiGoM^9K(1hRl z&VrW8K)`vTv{&`Ac^B_KJFV(?dU*8y?8vMP4meTIu?$K(fE5)$lI(d~EKz&Fe%t~m zFhVm;6-PV32>q6U?3cajw-QQ#>8_3QxDBwsi2y$>kE2>3{zMdNQWCgi4Q)#>UvbQ=gVKFXYtDp_RRU^p5x-%0h%;AqQ*d< zNr3dLCyg#6o@Dd2mzD7Ws!3&;;1fv+{NpvFSVF9{8&nUVUD;HgRAv#8%{FByyw7|E zue9#{7NM5HoV4N;wD+8ZpCh%osPj|F=&R0eUEBIDyt@l43Jl2w*UCWlg)8Hk-^gO!L;A-|L1<2jn z$BNLD7KGVuWm=qbN5T5ysZ{as`7iZtZlY(ouWqxZ882R)i`YSu?yKeV&t`urr93Pnw?>)Da6IHe>+I3 zR(WlY#JVeOI4xRq0O?2Tu%J|HTBvm@ZoB6KU-T2&diNDQ&#+d1w)!%IRV*C0v_y@U zNY4m4ByUx6_uBhueEaN!Qo-0|TZ|D5??ZzZ2h8lJ1kqjq!xbC>xS3Cm)dCgE`ZQy0 zE#PCxVta1@u8i^NWTKpCuUU2Xx?r!tUyP%50kTazCo**}s4q8+BC zWYrXn*Jo{6WH(sQKsPBkq*Vo}oLN2*`OYS3Ty}wt_$x|$6icjp`s?`HQ}t@+%$BXQ z@2`%NLzcIWbBM?V;Ua1N;^XO%Blk1ux$fc=Aow0H0JxO-lctb;AZiK~&$RTdlm5qq z8Z^Hj1ejXOHHC7rvyab#Qo)BUTIKAV01H@N zTORIJ3m}o_6uLo)cw4EfSFfJKS{GQRmUDFF>&P_c^A!iP<%U2$6skGCtS{yLztpCPlh;Wtz#6d0P4?99pDuLP_<0}k4M3C1+d?pb7ukI)n)|} z;C%dtzp`lAvIrPJYx@6Vt*r^!8}L<){*NsVAT`bJxz6wT*zTWWeLx8b3Bl(`r*kS7 zKpR{7-3R;a^-=JPbE~+G-YeTb@sgA6ju}_FU*6GjW7Dq^@??&2 z^yg^;9^>Dvz1yE^dRRR_s>D-z^n`xY5AHKnc$q~p9VHRtA^!3DWr%r(C>>baEq_|F1F|kjd36xZ3H91n8nV_8Jh?NTa2bnluGT>NTc*6>txNI zN3o;it&g+wYDfM^oZepqXIkHTT>ges9HGD7=(beBzbv8mf~U_Uc5#9=L(cMh<$&F@ zW}9a`%TX3zH>g37sNwH@LlhC7{cC2l10%Ebl!o*B=ZsDpcPUnAuk2Qx#=*m;qj4c# z?iK)6xEB;6Z#BvKX%FHy*=Je?pRRMeI;p^G&l3GZFXO0&?=%279q){bEx%jt{dOBu zu?83(Q3SfAlwk;!^yZR#MY9~@4*IDLrp^`k!gvP2qWr7S(T0&zZ_HkT;&1u`=v;Ii92 zhkdTKY_9DA{?Ijm*KsAJE$hi0y6@+f{BsHiV4c3!@b29)kQ)6wj~pR|+AC#gh&8|i z{p!d&#Ek=Wcl(?q2rQ=)VyyV_eyyox4M-_Jb1r_hP6y)NI_J^-kn)souM>a^1)|r{ zL`%j&%hpi~V90MEy8U%4`VRyu0xV9d_CzDPr2lvgt2mFU&S?r8F`v#M2O*U`CE#Mf z(RGapg$Vki9iSSZvof5a?@-cwI1A;22|Jl*JE$rS{ zo$Fb*(!p)uDsLZu7~MZf2Ec>>^1!LGzg~9P3}EG-a|jLr*LzS!PS97tEPuSLw6)MU z2l;Vz)<1SLxb!}pEoHQyuzGKD#11P_oo{-pEv{6P0gLq6S3}VUT%M;=d6`E-&$aDJ zb9Adwy=xW(C*WEzjZ)_N2M1lRDzKF!gQ$l{r0nZ~)6N?e<5_BGUF|;%au)to05Dc) z`5nL#>Ej4o7fbt5Aq#}!r(nq%tTODjzcWp*1HA*CBkitf42^q2}HYqfJVU&TIWPO_|UAic`*-QV-%T|VgdoPW&&{JF`kw5v{Ns|9Bmx^sa}8SAzJ*Wy6@&l z(7ck@4*(C7W)x-r^SV2%Tgm^er*z2C`oO`sS$kRsO;pP8hIB0Fy+Fq+8I}FHg@Es$ zI%J6>1~cpUE4F(1?fWWd`bF9&iAUq7&Fl$sS54V>W}8n^5Y(Yh-ZXunpeRpA>~eD8 z#ke9K$y|l|khm6(Xl$FJm%H;WKm(VmZW{%Bv}nI_Vq-V}-Wa#-^1tUDjnB6k2-QuF zoA*yTjTWBTTj8>{s>n>?kZ<0P)FYGoBDk|OGvKi~Uwt?5RT^2PLN5fv3q;_u;2g2w zz+ktwIhuqZ{En-T4x50d&nZeC!B^Q}pI!DH=r+?z*pw+H8uWsxV(5637rl`m zQ4QW5i>4*%`rYX|Q7RC5l9y{j7K*m-Faf{l&Z878dSHQT*vhZl-55A5*6vie~NJDSgfs;ST z@snVyFhtLbbX^UFQq#lE>L*th`1wS?3vwk&VEfaD=&^Gr0x*PS&&YXSe^=^<9WoV1&l2W&B@OSKDk`vpDF1ANpFQ zj=!WNYoWjo+pJJSXxdaV%FIc!#IG=5RgRx;a@vr(6TA_5(TEh!XQBMv$F&9|!SAZu z!2F83bb)rPN~g|YN_g<%_m#K|KUxit24`PmS--oM7qkZ7^-$tC;?Y0fTlo{tD;qB* z{JgtPHqq1SGR-l<7cbu&HFj$K{<38HK?ddS`^==gIHeU1AML@IzXF$A0HCq>UvT-zo7BT zjnwFO3pon~szC*&Ood2AEeg%%$@Wa_cRzn^+E3Xm7*^gzxWI)*WcGV|JB@)ZtoKWO zL(ms@H0b898pg%5eRkmRJkLJSIE^eXIQt41&cJl!kCbe-WV8SrnPd^nT0sAGETY=+#c!dG20a!(m!Tu0hvkJB18tO{LAA`t z{Vp?ic2!X^j-==*zreqT^Izj#P}20RP5h!7(s)%*OWbd-Fw?T{V*yV zfL=bu1v$OndkmKQD|4m%|I~;k^(~gYt2waj)N_|7*X@+?mOUb?>(?+6P_F(1c+rX# z52lFuDKwp1CaD@U&bQDHU#G0x(;VKiV~W?KGaKx6)f-^+(rs zShdeOI`{~+(<14gN|O9H?D4@=9tpgtIlCLc(KJ@LBlRR6Zi-lE@=V#(lh(xf9GKMg z2N+WN9gsDp3bX)upwMZbRapDyUqLP%3_k_*f0`6O)0?gXo7A-T@+riTUDVqn85ZB= zuM&KMy^a}sy0SQD=*-0v{;$pPVl$;lW!5fGTI}u0n?0YBEd6O7f3K4QBYx@ibUyj} z1%7?@B43p}amqh|Woh;S=W0ONjD*9}>@TPB=Dl+9Zn!<>gWWyrrwd*~W?!101`2M? zEfg@(Uzuww?U6{gq8htgw-w(C1g^Ic0+p($DZNWVzrC6tY!IqT!FFZ8lBAXhN z1ZHp>@NJ7J$aG0$5#?}CZosFlvP*oI4s=gPY^ZL&|7gKK*U0Ru|J3r4<-v%_9~vaR z%+yx`V;XxE8Q0CVIe|NVL-7S+kh%Cf*;);E2m~Z}2h}*xmb)W6#$Mn7Pwa2WEEk7C^R$mR-Y~~={ti5C5e;4xbmQFMOoG&^Je{$IZ7fAAq z^WzM%Y8JC2V?Sg##k#yEcvf-pU9O9i6lrbXGzUs=c4P0iy;$5EH^MRlYvu({>ewc$ zWp*!uI(OEHb1n?k$}n-939W6;GhXyVLqY@p99|I|7znQmzasBZ=@35m0%Q#mb0ulX zW`d@lITem^zL%6rEg0qF?U%aNU^GiSv5XOURGF~oi%m@K&#@^C)r{~Pz?l4`uA=Gf z@A_b4!|+78=DBKo%>OpezK+-z6vSbu0=D|dcFyx$Ne&SvGzENtx%3cDE8)}b8wR=bq-%2A9_ z0QmjzUTq88r*+uB!iQ~{qGKQ=;+u)c)G{!6OWQ@G4BAzmrkfnwH#n5EE_O?gfieyw zymk;wh1SyFbJd@AHJqp37UMjm28$wP^!!XAJ13~s;_tnT**UMDOtnd6sTh<@dG6Sw zuU1^aojWt18#&1vd_$NQ-<@+qqLamdyq$J9K<1FNOkg7x=Y{STStq3?45M02eMppt zlOUx`KDgdkSo|Z2Yjy}kKLNqH8zpNLRO_d90Dr(zRH02UZ)gzGAFqhUp*fNAl^ZL!%xpZ z=H^5|%Ex>x5N4h+leJt=AEAEOthbmk;mKD?4YO&uK`>xZXN1bhgp-e|&hT{Je^PT4 z70n@7y%GQ}JR#RTAFf&>C+0O;+}nHmQGio+LUa3Db22c3!~p>RD~{`yMv*L)bal+?){Y>nMC2|^Z6rJ!0{ay=N!=bLGUda zQVpfcNeNQbB*~Y`@w0&=v~Hf&DE1bXyDw; z2MX-S!wR>}T{4)QhoXq9Gqb#Q(4|?X6U9yXH9pGGWskf=<~<3oK~~3xxPWT!zGpE3 z($93sA?VQk&R1uqXsX%|L;!41c@Th3t(tXL&x?ItSI(e5n|UJR_nz}WRI7|qDi;jx zNOCo)Qyc^kVIC->Yob63LM`t|VZ*k|4E;oWwM0K0=en~leZR33YvT~2KnsOFlATO~ z+S$|uU&L9qH{w1eBMK+6NTS5Z_id1OhE}C)7`AX{nzMXTq|Kv6io53QY|<*jCL_1Q z?D>&h%xrANb-hVfI}3p#bse6l=m)7U#XoA}0zJ9W&NPMej?h634%0GLpPnIW}%^nTqT~HP24XOX) znW>LcwOXW1kCiHzwtAeKTWk%o`wNHOVJtG1Bm)G6t(43Ln6(XkD-UaEZaL!^uYL(9 z5I5zyK~kpCX%&Ggj!kqx)}$4E79a=}T9@2~J!)QU;uJrS!F)$VX$Hi;oU|54mo;MR%R?KkB0{<7FJ<4{ zlf?6}Y|ll&AY(@Ie1@Fg!dSY3TTj7KScmv1|9f#|K15-2D55|M#nJ*sL-KUu`<1Ea{Y`hsIa^>C4pO8()|dNN-vG(; zPo%lAPs2g!!-y>WVWzgUZl*k7pCqu^_PE#L(-6_u`Q2qx#IRhEHX5M5#DCTl=b0-c zZ(A{yp``{BLB)TtsNi7dW>vcp#Q};0+dC1FCAHlS+*HfXK9?wcI6{GO^;Sd67dx@& zm0C-uT$OQsVv-d|V3G+q)BH<$-fLhPSvXY;?CAzF-*s-X)vL8PHXWFHT6U6a(~n7}eb$X8GDrY*Bk+3!({jB3cB6C$)o&P!Y2p^Z+$FfaSem?#GFwrR zrCoOPZUHnsEXuO^>`1BHgw5$EgC~gi&P4yZU8r>0Q=&%doJez+&dHr~N4->aJ-E^B z21CXe3&-0B62HrP`n;@0eflOH6)#E6>s*zRO9>sA2Cn>Yc+T{tqY6`haS7QqYyY$= zSU9mES#V%gM>01#m9a-{Nr|EtGPqUWe`klTJbYo%O$JyRF;iT&qavOOI{E5UEt@z2 znyg5Fu1;!9(_uR3lC#*nN1$0DGF2AGOp_@?vqC`#jHN|r{u99(wdkXD5GF0w^#UQR zz!wsw^c{CSx5~>fa`@T|^=qn)P*l+Z!^-Ocj4=?1+X=r(CXv3H$UL+UkIwG-Fe4 z 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
GtO;QPeTW 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 buhsuQJ%~m5NJ;Q000000NkvXXu0mjfIS2QH From 44d87bc28ceb768f3fbbf70cef13b1e86b67933b Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sun, 16 Aug 2026 00:17:34 -0400 Subject: [PATCH 44/75] feat: tab completion matches anywhere in a name, prefix hits first Closes #36 --- .changeset/gentle-dolphins-wonder.md | 5 ++ .../tabcompleters/BlacklistTabCompleter.java | 6 +- .../tabcompleters/DimensionTabCompleter.java | 4 +- .../tabcompleters/HomesTabCompleter.java | 4 +- .../ImportSourcesTabCompleter.java | 6 +- .../tabcompleters/MaterialsTabCompleter.java | 4 +- .../PlayerHomesTabCompleter.java | 6 +- .../sethomestwo/utils/TabCompletions.java | 46 ++++++++++++++ .../BlacklistTabCompleterTest.java | 60 +++++++++++++++++++ .../sethomestwo/utils/TabCompletionsTest.java | 57 ++++++++++++++++++ 10 files changed, 183 insertions(+), 15 deletions(-) create mode 100644 .changeset/gentle-dolphins-wonder.md create mode 100644 src/main/java/com/samleighton/sethomestwo/utils/TabCompletions.java create mode 100644 src/test/java/com/samleighton/sethomestwo/tabcompleters/BlacklistTabCompleterTest.java create mode 100644 src/test/java/com/samleighton/sethomestwo/utils/TabCompletionsTest.java diff --git a/.changeset/gentle-dolphins-wonder.md b/.changeset/gentle-dolphins-wonder.md new file mode 100644 index 0000000..1c6df07 --- /dev/null +++ b/.changeset/gentle-dolphins-wonder.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +Tab completion now matches anywhere in a name, not only from the start, so typing net completes to world_nether and ase finds a home called base. Names that start with what you typed are still listed first, so nothing you already do changes. diff --git a/src/main/java/com/samleighton/sethomestwo/tabcompleters/BlacklistTabCompleter.java b/src/main/java/com/samleighton/sethomestwo/tabcompleters/BlacklistTabCompleter.java index f01f6a8..d13f394 100644 --- a/src/main/java/com/samleighton/sethomestwo/tabcompleters/BlacklistTabCompleter.java +++ b/src/main/java/com/samleighton/sethomestwo/tabcompleters/BlacklistTabCompleter.java @@ -2,10 +2,10 @@ import com.samleighton.sethomestwo.dao.BlacklistDao; import com.samleighton.sethomestwo.utils.ServerUtil; +import com.samleighton.sethomestwo.utils.TabCompletions; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; -import org.bukkit.util.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -25,7 +25,7 @@ public List onTabComplete(@NotNull CommandSender commandSender, @NotNull boolean canonical = "blacklist".equalsIgnoreCase(label); if (canonical && args.length == 1) { - StringUtil.copyPartialMatches(args[0], Arrays.asList("add", "remove", "list"), completions); + completions.addAll(TabCompletions.matching(args[0], Arrays.asList("add", "remove", "list"))); return completions; } @@ -34,7 +34,7 @@ public List onTabComplete(@NotNull CommandSender commandSender, @NotNull List source = removing ? new BlacklistDao().getAll() : ServerUtil.getValidDimensions(); String lastArg = args.length == 0 ? "" : args[args.length - 1]; - StringUtil.copyPartialMatches(lastArg, source, completions); + completions.addAll(TabCompletions.matching(lastArg, source)); return completions; } } diff --git a/src/main/java/com/samleighton/sethomestwo/tabcompleters/DimensionTabCompleter.java b/src/main/java/com/samleighton/sethomestwo/tabcompleters/DimensionTabCompleter.java index ce9dd79..ff0edfa 100644 --- a/src/main/java/com/samleighton/sethomestwo/tabcompleters/DimensionTabCompleter.java +++ b/src/main/java/com/samleighton/sethomestwo/tabcompleters/DimensionTabCompleter.java @@ -1,10 +1,10 @@ package com.samleighton.sethomestwo.tabcompleters; import com.samleighton.sethomestwo.utils.ServerUtil; +import com.samleighton.sethomestwo.utils.TabCompletions; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; -import org.bukkit.util.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -17,7 +17,7 @@ public class DimensionTabCompleter implements TabCompleter { public List onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s, @NotNull String[] args) { List completions = new ArrayList<>(); for(String arg : args){ - StringUtil.copyPartialMatches(arg, ServerUtil.getValidDimensions(), completions); + completions.addAll(TabCompletions.matching(arg, ServerUtil.getValidDimensions())); } return completions; diff --git a/src/main/java/com/samleighton/sethomestwo/tabcompleters/HomesTabCompleter.java b/src/main/java/com/samleighton/sethomestwo/tabcompleters/HomesTabCompleter.java index 49a7670..d9a7423 100644 --- a/src/main/java/com/samleighton/sethomestwo/tabcompleters/HomesTabCompleter.java +++ b/src/main/java/com/samleighton/sethomestwo/tabcompleters/HomesTabCompleter.java @@ -2,11 +2,11 @@ import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.utils.HomesUtil; +import com.samleighton.sethomestwo.utils.TabCompletions; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; -import org.bukkit.util.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -25,7 +25,7 @@ public List onTabComplete(@NotNull CommandSender commandSender, @NotNull Player player = (Player) commandSender; List homeNames = HomesUtil.getPlayerHomesNameOnly(new HomesDao(), player.getUniqueId()); - StringUtil.copyPartialMatches(args[0], homeNames, completions); + completions.addAll(TabCompletions.matching(args[0], homeNames)); return completions; } } diff --git a/src/main/java/com/samleighton/sethomestwo/tabcompleters/ImportSourcesTabCompleter.java b/src/main/java/com/samleighton/sethomestwo/tabcompleters/ImportSourcesTabCompleter.java index 6951bab..991a6c1 100644 --- a/src/main/java/com/samleighton/sethomestwo/tabcompleters/ImportSourcesTabCompleter.java +++ b/src/main/java/com/samleighton/sethomestwo/tabcompleters/ImportSourcesTabCompleter.java @@ -1,10 +1,10 @@ package com.samleighton.sethomestwo.tabcompleters; import com.samleighton.sethomestwo.commands.ImportHomes; +import com.samleighton.sethomestwo.utils.TabCompletions; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; -import org.bukkit.util.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -24,11 +24,11 @@ public List onTabComplete(@NotNull CommandSender commandSender, @NotNull List completions = new ArrayList<>(); if (args.length == 1) { - StringUtil.copyPartialMatches(args[0], ImportHomes.SOURCES.keySet(), completions); + completions.addAll(TabCompletions.matching(args[0], ImportHomes.SOURCES.keySet())); } if (args.length == 2) { - StringUtil.copyPartialMatches(args[1], List.of("confirm"), completions); + completions.addAll(TabCompletions.matching(args[1], List.of("confirm"))); } return completions; diff --git a/src/main/java/com/samleighton/sethomestwo/tabcompleters/MaterialsTabCompleter.java b/src/main/java/com/samleighton/sethomestwo/tabcompleters/MaterialsTabCompleter.java index c8db245..ec35870 100644 --- a/src/main/java/com/samleighton/sethomestwo/tabcompleters/MaterialsTabCompleter.java +++ b/src/main/java/com/samleighton/sethomestwo/tabcompleters/MaterialsTabCompleter.java @@ -1,10 +1,10 @@ package com.samleighton.sethomestwo.tabcompleters; +import com.samleighton.sethomestwo.utils.TabCompletions; import org.bukkit.Material; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; -import org.bukkit.util.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -32,7 +32,7 @@ public List onTabComplete(@NotNull CommandSender commandSender, @NotNull validMaterials.add(mat.getKey().toString().toLowerCase()); } - StringUtil.copyPartialMatches(args[1], validMaterials, completions); + completions.addAll(TabCompletions.matching(args[1], validMaterials)); return completions; } } diff --git a/src/main/java/com/samleighton/sethomestwo/tabcompleters/PlayerHomesTabCompleter.java b/src/main/java/com/samleighton/sethomestwo/tabcompleters/PlayerHomesTabCompleter.java index 19b9dcc..92c8324 100644 --- a/src/main/java/com/samleighton/sethomestwo/tabcompleters/PlayerHomesTabCompleter.java +++ b/src/main/java/com/samleighton/sethomestwo/tabcompleters/PlayerHomesTabCompleter.java @@ -3,11 +3,11 @@ import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.utils.ServerUtil; +import com.samleighton.sethomestwo.utils.TabCompletions; import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; -import org.bukkit.util.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -27,7 +27,7 @@ public List onTabComplete(@NotNull CommandSender commandSender, @NotNull if (args.length == 1) { List names = new ArrayList<>(); Bukkit.getOnlinePlayers().forEach(player -> names.add(player.getName())); - StringUtil.copyPartialMatches(args[0], names, completions); + completions.addAll(TabCompletions.matching(args[0], names)); return completions; } @@ -40,7 +40,7 @@ public List onTabComplete(@NotNull CommandSender commandSender, @NotNull homeNames.add(home.getName()); } - StringUtil.copyPartialMatches(args[1], homeNames, completions); + completions.addAll(TabCompletions.matching(args[1], homeNames)); } return completions; diff --git a/src/main/java/com/samleighton/sethomestwo/utils/TabCompletions.java b/src/main/java/com/samleighton/sethomestwo/utils/TabCompletions.java new file mode 100644 index 0000000..05f2e05 --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/utils/TabCompletions.java @@ -0,0 +1,46 @@ +package com.samleighton.sethomestwo.utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; + +/** + * Tab completion matching shared by every completer in the plugin. + */ +public final class TabCompletions { + + private TabCompletions() { + } + + /** + * Returns the candidates that contain the fragment, case-insensitively. + * Candidates that start with the fragment come first, then the rest; + * source order is kept within each group and duplicates are dropped. + * + * @param fragment what the player has typed so far; empty matches everything + * @param candidates the names to choose from + * @return the matching candidates, in ranked order, never null + */ + public static List matching(String fragment, Collection candidates) { + String needle = fragment.toLowerCase(Locale.ROOT); + LinkedHashSet prefixHits = new LinkedHashSet<>(); + LinkedHashSet substringHits = new LinkedHashSet<>(); + + for (String candidate : candidates) { + String haystack = candidate.toLowerCase(Locale.ROOT); + if (haystack.startsWith(needle)) { + prefixHits.add(candidate); + } else if (haystack.contains(needle)) { + substringHits.add(candidate); + } + } + + List matches = new ArrayList<>(prefixHits); + for (String hit : substringHits) { + if (!prefixHits.contains(hit)) matches.add(hit); + } + return matches; + } +} diff --git a/src/test/java/com/samleighton/sethomestwo/tabcompleters/BlacklistTabCompleterTest.java b/src/test/java/com/samleighton/sethomestwo/tabcompleters/BlacklistTabCompleterTest.java new file mode 100644 index 0000000..6d357fe --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/tabcompleters/BlacklistTabCompleterTest.java @@ -0,0 +1,60 @@ +package com.samleighton.sethomestwo.tabcompleters; + +import com.samleighton.sethomestwo.support.HomeFixtures; +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.command.PluginCommand; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +import java.util.List; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class BlacklistTabCompleterTest extends ServerTestBase { + + private List complete(String label, String... args) { + PlayerMock player = addPlayer(); + PluginCommand command = Objects.requireNonNull(plugin.getCommand("blacklist")); + + return new BlacklistTabCompleter().onTabComplete(player, command, label, args); + } + + @Test + void aFragmentFromTheMiddleOfAWorldNameOffersThatWorld() { + assertEquals(List.of("world_nether"), complete("blacklist", "add", "net")); + } + + @Test + void aWorldNamePrefixStillOffersThatWorld() { + assertEquals(List.of("world_nether"), complete("blacklist", "add", "world_n")); + } + + @Test + void theSharedPrefixOffersEveryWorldInServerOrder() { + assertEquals(List.of("world", "world_nether", "world_the_end"), complete("blacklist", "add", "world")); + } + + @Test + void aFragmentMatchingNoWorldOffersNothing() { + assertEquals(List.of(), complete("blacklist", "add", "xyz")); + } + + @Test + void subcommandsMatchAnywhereToo() { + assertEquals(List.of("remove"), complete("blacklist", "mov")); + } + + @Test + void removingOffersOnlyBlacklistedWorldsMatchingTheFragment() { + HomeFixtures.blacklist("world_nether"); + HomeFixtures.blacklist("world_the_end"); + + assertEquals(List.of("world_the_end"), complete("blacklist", "remove", "end")); + } + + @Test + void theOldAliasGoesStraightToWorldNames() { + assertEquals(List.of("world_nether"), complete("add-to-blacklist", "net")); + } +} diff --git a/src/test/java/com/samleighton/sethomestwo/utils/TabCompletionsTest.java b/src/test/java/com/samleighton/sethomestwo/utils/TabCompletionsTest.java new file mode 100644 index 0000000..8fb78fc --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/utils/TabCompletionsTest.java @@ -0,0 +1,57 @@ +package com.samleighton.sethomestwo.utils; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TabCompletionsTest { + + private static final List WORLDS = List.of("world", "world_nether", "world_the_end"); + + @Test + void anEmptyFragmentOffersEverythingInSourceOrder() { + assertEquals(WORLDS, TabCompletions.matching("", WORLDS)); + } + + @Test + void aFragmentFromTheMiddleOfANameMatches() { + assertEquals(List.of("world_nether"), TabCompletions.matching("net", WORLDS)); + } + + @Test + void aPrefixStillMatches() { + assertEquals(List.of("world_nether"), TabCompletions.matching("world_n", WORLDS)); + } + + @Test + void matchingIgnoresCase() { + assertEquals(List.of("world_nether"), TabCompletions.matching("NET", WORLDS)); + // The candidate keeps its own casing in the result. + assertEquals(List.of("World_Nether"), TabCompletions.matching("net", List.of("World_Nether"))); + } + + @Test + void prefixMatchesRankAboveSubstringMatches() { + // "minecart" contains "cart" and comes first in source order, but + // "cart" starts with it and must be offered first. + assertEquals(List.of("cart", "minecart"), TabCompletions.matching("cart", List.of("minecart", "cart"))); + } + + @Test + void sourceOrderIsKeptWithinEachTier() { + List source = List.of("stone_axe", "axe", "sandstone", "stone"); + assertEquals(List.of("stone_axe", "stone", "sandstone"), TabCompletions.matching("stone", source)); + } + + @Test + void aNameIsOfferedOnce() { + assertEquals(List.of("stone"), TabCompletions.matching("stone", List.of("stone", "stone"))); + } + + @Test + void aFragmentThatMatchesNothingOffersNothing() { + assertEquals(List.of(), TabCompletions.matching("xyz", WORLDS)); + } +} From e08453387d6c07ffcf6db6acbd5647065d070fde Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sun, 16 Aug 2026 14:24:22 -0400 Subject: [PATCH 45/75] feat: report anonymous usage metrics to bStats Adds bStats (org.bstats:bstats-bukkit 3.2.1, shaded and relocated to com.samleighton.sethomestwo.lib.bstats) with a metrics package that counts every command by canonical name and typed alias, every homes menu and management menu action, teleport source and outcome, and database or item data failures by kind. Config adoption and homes per server and per player are reported as buckets. Nothing personal is sent: no names, UUIDs, coordinates, home names or addresses, only counts and ranges. Metrics are always on. The only opt-out is bStats' own server wide switch in plugins/bStats/config.yml, which the reporter reads before constructing bStats. Under MockBukkit the test base writes that file disabled, so no test constructs bStats or makes a network request. The reporter starts from a delayed task like the update check and swallows any failure. Charts: Advanced Bar rankings for the last window plus Single Line history per command and totals, since bStats keeps history only for line charts; Simple Pies for config adoption. Every custom chart is registered on the bStats plugin page with a matching id. Also moves the test suite off deprecated MockBukkit and Bukkit APIs and removes the last deprecated calls from main code, so the build compiles with zero deprecation warnings. Closes #29 --- .changeset/kind-ravens-gather.md | 5 + .github/workflows/tests.yml | 14 ++ README.md | 17 ++ pom.xml | 10 + .../samleighton/sethomestwo/SetHomesTwo.java | 19 ++ .../sethomestwo/commands/GoHome.java | 3 + .../sethomestwo/commands/GoPlayerHome.java | 3 + .../sethomestwo/commands/ListHomes.java | 2 + .../connections/ConnectionManager.java | 2 + .../samleighton/sethomestwo/dao/HomesDao.java | 25 +++ .../sethomestwo/datatypes/PersistentHome.java | 2 + .../datatypes/PersistentString.java | 2 + .../sethomestwo/gui/HomeActionsGui.java | 6 + .../samleighton/sethomestwo/gui/HomesGui.java | 17 +- .../sethomestwo/metrics/Buckets.java | 37 ++++ .../metrics/CommandUsageListener.java | 69 +++++++ .../sethomestwo/metrics/Errors.java | 26 +++ .../sethomestwo/metrics/MetricsReporter.java | 176 ++++++++++++++++++ .../sethomestwo/metrics/UsageCounters.java | 70 +++++++ .../sethomestwo/metrics/WindowShare.java | 53 ++++++ .../samleighton/sethomestwo/models/Home.java | 11 ++ .../tabcompleters/MaterialsTabCompleter.java | 5 +- .../sethomestwo/utils/DatabaseUtil.java | 4 + .../sethomestwo/commands/BlacklistTest.java | 22 +-- .../sethomestwo/commands/CreateHomeTest.java | 47 +++-- .../sethomestwo/commands/DeleteHomeTest.java | 10 +- .../commands/GetPlayerHomesTest.java | 8 +- .../sethomestwo/commands/GoHomeTest.java | 81 ++++++-- .../sethomestwo/commands/ImportHomesTest.java | 8 +- .../sethomestwo/commands/MoveHomeTest.java | 14 +- .../commands/OpenHomesGuiTest.java | 10 +- .../commands/PlayerHomeAdminCommandsTest.java | 49 +++-- .../sethomestwo/dao/HomesDaoTest.java | 17 ++ .../sethomestwo/events/PlayerJoinTest.java | 5 +- .../events/RightClickHomeItemTest.java | 14 +- .../sethomestwo/gui/GuiSessionTest.java | 22 +-- .../sethomestwo/gui/HomeActionsGuiTest.java | 69 +++++++ .../sethomestwo/gui/HomesGuiClickTest.java | 28 +++ .../gui/HomesGuiPaginationTest.java | 30 ++- .../sethomestwo/metrics/BucketsTest.java | 43 +++++ .../metrics/CommandUsageListenerTest.java | 96 ++++++++++ .../sethomestwo/metrics/ErrorsTest.java | 52 ++++++ .../metrics/MetricsReporterTest.java | 147 +++++++++++++++ .../metrics/UsageCountersTest.java | 55 ++++++ .../sethomestwo/metrics/WindowShareTest.java | 74 ++++++++ .../sethomestwo/support/ServerTestBase.java | 18 ++ .../utils/BlacklistEnforcementTest.java | 2 +- .../sethomestwo/utils/BypassNodesTest.java | 20 +- 48 files changed, 1390 insertions(+), 129 deletions(-) create mode 100644 .changeset/kind-ravens-gather.md create mode 100644 src/main/java/com/samleighton/sethomestwo/metrics/Buckets.java create mode 100644 src/main/java/com/samleighton/sethomestwo/metrics/CommandUsageListener.java create mode 100644 src/main/java/com/samleighton/sethomestwo/metrics/Errors.java create mode 100644 src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java create mode 100644 src/main/java/com/samleighton/sethomestwo/metrics/UsageCounters.java create mode 100644 src/main/java/com/samleighton/sethomestwo/metrics/WindowShare.java create mode 100644 src/test/java/com/samleighton/sethomestwo/metrics/BucketsTest.java create mode 100644 src/test/java/com/samleighton/sethomestwo/metrics/CommandUsageListenerTest.java create mode 100644 src/test/java/com/samleighton/sethomestwo/metrics/ErrorsTest.java create mode 100644 src/test/java/com/samleighton/sethomestwo/metrics/MetricsReporterTest.java create mode 100644 src/test/java/com/samleighton/sethomestwo/metrics/UsageCountersTest.java create mode 100644 src/test/java/com/samleighton/sethomestwo/metrics/WindowShareTest.java diff --git a/.changeset/kind-ravens-gather.md b/.changeset/kind-ravens-gather.md new file mode 100644 index 0000000..5fd5032 --- /dev/null +++ b/.changeset/kind-ravens-gather.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +The plugin now sends anonymous usage counts to bStats, so the maintainers can see which commands, menu buttons and settings are used, and whether database errors are happening in the wild. Nothing personal is sent, and the bStats switch in plugins/bStats/config.yml turns it off. The README lists exactly what is collected. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c56afdc..52a4d79 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,6 +28,20 @@ jobs: - name: Build and test run: mvn -B verify + - name: Prove the shaded libraries were relocated + run: | + jar="$(ls target/SetHomesTwo-*.jar | head -n 1)" + listing="$(unzip -l "$jar")" + if echo "$listing" | grep -q ' org/bstats/'; then + echo "org.bstats was not relocated"; exit 1 + fi + if ! echo "$listing" | grep -q ' com/samleighton/sethomestwo/lib/bstats/'; then + echo "relocated bstats classes are missing"; exit 1 + fi + if echo "$listing" | grep -q ' net/wesjd/'; then + echo "anvilgui was not relocated"; exit 1 + fi + - name: Run the release script tests run: bash scripts/test-release.sh diff --git a/README.md b/README.md index b2cf4d6..9251c05 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,23 @@ To pick one up, copy the key you want out of [`default-config.yml`](https://gith

+### Anonymous usage statistics + +Set Homes Two reports anonymous usage counts to [bStats](https://bstats.org/plugin/bukkit/SetHomesTwo/33420), the same service most Bukkit plugins use. It tells the maintainers which commands and menu buttons are actually used, so the plugin can be trimmed and improved based on real use rather than guesswork. + +Every 30 minutes the plugin sends totals for that window, and nothing else: + +- how many times each command was run, and which spelling was typed (for example `/home` versus `/go-home`) +- how many times each button in the homes menu and the management menu was clicked +- how many teleports started from the menu versus a command, and how each ended (completed, cancelled by moving, refused because a teleport was already counting down, blocked by the blacklist, moved to a safe spot, cancelled as unsafe) +- which settings are on: home limits and their type, cancel on move, teleport safety, the delay as a range, whether the compass and default icon items are still the defaults, and whether LuckPerms is installed +- the number of homes on the server and the average per player, both as ranges (for example 51 to 500) +- how many times the plugin hit a database or item-data error, by kind only (a count of failed writes, never the message or the data) + +bStats itself adds the things it collects for every plugin: server software and Minecraft version, Java version, player count range, online mode, and country. No player names, UUIDs, coordinates, home names or server address are ever sent, and the aggregated charts are public. + +There is no per-plugin switch. To turn bStats off, set `enabled: false` in `plugins/bStats/config.yml` and restart; that file is shared by every plugin on the server that uses bStats, and Set Homes Two honours it before sending anything. + ## Coming from EssentialsX or Set Homes v1 Your players keep their homes. The old plugin does not even need to be running, because the importer reads its data files directly. diff --git a/pom.xml b/pom.xml index 6f76d60..5046ffa 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,10 @@ net.wesjd.anvilgui com.samleighton.sethomestwo.lib.anvilgui + + org.bstats + com.samleighton.sethomestwo.lib.bstats + @@ -140,5 +144,11 @@ 1.10.13-20260714.043149-1 compile + + org.bstats + bstats-bukkit + 3.2.1 + compile + diff --git a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java index acca8fc..9346997 100644 --- a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java +++ b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java @@ -4,6 +4,9 @@ import com.samleighton.sethomestwo.connections.ConnectionManager; import com.samleighton.sethomestwo.dao.TeleportAttemptsDao; import com.samleighton.sethomestwo.enums.DebugLevel; +import com.samleighton.sethomestwo.metrics.CommandUsageListener; +import com.samleighton.sethomestwo.metrics.MetricsReporter; +import com.samleighton.sethomestwo.metrics.UsageCounters; import com.samleighton.sethomestwo.events.PlayerJoin; import com.samleighton.sethomestwo.events.PlayerLeave; import com.samleighton.sethomestwo.events.PlayerMoveWhileTeleporting; @@ -39,7 +42,9 @@ public class SetHomesTwo extends JavaPlugin { private final ConnectionManager connectionManager = new ConnectionManager(); private final Map guiSessionMap = new HashMap<>(); + private final UsageCounters usageCounters = new UsageCounters(); private UpdateChecker updateChecker; + private MetricsReporter metricsReporter; /** * Looked up by name because JavaPlugin.getPlugin(SetHomesTwo.class) needs the @@ -68,6 +73,9 @@ public void onEnable() { ); updateChecker.checkLater(); + metricsReporter = new MetricsReporter(this); + metricsReporter.startLater(); + // Plugin startup logic registerCommands(); registerEventListeners(); @@ -118,6 +126,8 @@ public void onDisable() { player.removePotionEffect(PotionEffectType.NAUSEA); } + if (metricsReporter != null) metricsReporter.shutdown(); + // Close database connections connectionManager.closeConnections(); @@ -217,6 +227,7 @@ public void registerEventListeners() { getServer().getPluginManager().registerEvents(new PlayerLeave(this), this); getServer().getPluginManager().registerEvents(new RightClickHomeItem(this), this); getServer().getPluginManager().registerEvents(new PlayerMoveWhileTeleporting(), this); + getServer().getPluginManager().registerEvents(new CommandUsageListener(this), this); } /** @@ -251,7 +262,15 @@ public UpdateChecker getUpdateChecker() { return this.updateChecker; } + public MetricsReporter getMetricsReporter() { + return this.metricsReporter; + } + public Map getGuiSessionMap() { return this.guiSessionMap; } + + public UsageCounters getUsageCounters() { + return this.usageCounters; + } } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java b/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java index fd0aca0..6ab6a0a 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java @@ -1,8 +1,10 @@ package com.samleighton.sethomestwo.commands; +import com.samleighton.sethomestwo.SetHomesTwo; import com.samleighton.sethomestwo.dao.Dao; import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.enums.UserError; +import com.samleighton.sethomestwo.metrics.UsageCounters; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.utils.ChatUtils; import com.samleighton.sethomestwo.utils.ConfigUtil; @@ -60,6 +62,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command } // Teleport player to home + SetHomesTwo.instance().getUsageCounters().increment(UsageCounters.Family.TELEPORT_SOURCE, UsageCounters.SOURCE_COMMAND); homeToTeleportTo.teleport(player); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java index 7af2ede..af77098 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java @@ -1,8 +1,10 @@ package com.samleighton.sethomestwo.commands; +import com.samleighton.sethomestwo.SetHomesTwo; import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.enums.UserError; import com.samleighton.sethomestwo.enums.UserInfo; +import com.samleighton.sethomestwo.metrics.UsageCounters; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.utils.ChatUtils; import com.samleighton.sethomestwo.utils.ConfigUtil; @@ -62,6 +64,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command home.setCanTeleport(false); } + SetHomesTwo.instance().getUsageCounters().increment(UsageCounters.Family.TELEPORT_SOURCE, UsageCounters.SOURCE_COMMAND); home.teleport(admin); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/ListHomes.java b/src/main/java/com/samleighton/sethomestwo/commands/ListHomes.java index eab1f49..39479ec 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/ListHomes.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/ListHomes.java @@ -23,6 +23,8 @@ import java.util.UUID; public class ListHomes implements CommandExecutor { + // Bungee chat is deprecated upstream in favour of Adventure, which Spigot servers do not have. + @SuppressWarnings("deprecation") @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { // Players only guard diff --git a/src/main/java/com/samleighton/sethomestwo/connections/ConnectionManager.java b/src/main/java/com/samleighton/sethomestwo/connections/ConnectionManager.java index 14a6e93..98a47c5 100644 --- a/src/main/java/com/samleighton/sethomestwo/connections/ConnectionManager.java +++ b/src/main/java/com/samleighton/sethomestwo/connections/ConnectionManager.java @@ -1,6 +1,7 @@ package com.samleighton.sethomestwo.connections; import com.samleighton.sethomestwo.SetHomesTwo; +import com.samleighton.sethomestwo.metrics.Errors; import org.bukkit.Bukkit; import java.sql.Connection; @@ -34,6 +35,7 @@ public boolean createConnection(String key, String dbName) { return true; } catch (SQLException e) { Bukkit.getLogger().severe(String.format("There was an issue creating the database %s", dbName)); + Errors.count(Errors.DB_CONNECT); } return false; diff --git a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java index a8154af..20fcf09 100644 --- a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java +++ b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java @@ -307,6 +307,31 @@ public boolean nameExists(UUID playerUUID, String name, Integer excludeId) { return false; } + /** + * @return total homes on this server, 0 if the read fails + */ + public int countAll() { + return countQuery(String.format("select count(*) as total from %s", TABLE_NAME)); + } + + /** + * @return number of distinct players holding at least one home, 0 if the read fails + */ + public int countPlayersWithHomes() { + return countQuery(String.format("select count(distinct player_uuid) as total from %s", TABLE_NAME)); + } + + private int countQuery(String sql) { + ResultSet rs = DatabaseUtil.fetch(this.conn, sql); + if (rs == null) return 0; + try { + return rs.next() ? rs.getInt("total") : 0; + } catch (SQLException e) { + Bukkit.getLogger().severe("There was an issue counting homes."); + return 0; + } + } + /** * The UUID of the player who owns homes stored under this name, or null. * A stale name can collide across two accounts (an old owner who renamed diff --git a/src/main/java/com/samleighton/sethomestwo/datatypes/PersistentHome.java b/src/main/java/com/samleighton/sethomestwo/datatypes/PersistentHome.java index 545f9d3..fe18d2a 100644 --- a/src/main/java/com/samleighton/sethomestwo/datatypes/PersistentHome.java +++ b/src/main/java/com/samleighton/sethomestwo/datatypes/PersistentHome.java @@ -2,6 +2,7 @@ import com.samleighton.sethomestwo.models.Home; import org.apache.commons.lang3.SerializationUtils; +import com.samleighton.sethomestwo.metrics.Errors; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; @@ -39,6 +40,7 @@ public class PersistentHome implements PersistentDataType { return (Home) ois.readObject(); } catch (IOException | ClassNotFoundException e) { Bukkit.getLogger().severe("There was a problem deserializing a home."); + Errors.count(Errors.ITEM_DATA); e.printStackTrace(); } diff --git a/src/main/java/com/samleighton/sethomestwo/datatypes/PersistentString.java b/src/main/java/com/samleighton/sethomestwo/datatypes/PersistentString.java index 7ba29b9..08ad6fb 100644 --- a/src/main/java/com/samleighton/sethomestwo/datatypes/PersistentString.java +++ b/src/main/java/com/samleighton/sethomestwo/datatypes/PersistentString.java @@ -1,6 +1,7 @@ package com.samleighton.sethomestwo.datatypes; import org.apache.commons.lang3.SerializationUtils; +import com.samleighton.sethomestwo.metrics.Errors; import org.bukkit.Bukkit; import org.bukkit.persistence.PersistentDataAdapterContext; import org.bukkit.persistence.PersistentDataType; @@ -37,6 +38,7 @@ public class PersistentString implements PersistentDataType { return (String) ois.readObject(); } catch (IOException | ClassNotFoundException e) { Bukkit.getLogger().severe("There was an issue deserializing a string."); + Errors.count(Errors.ITEM_DATA); e.printStackTrace(); } diff --git a/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java b/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java index bbc9b63..31bcdee 100644 --- a/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java +++ b/src/main/java/com/samleighton/sethomestwo/gui/HomeActionsGui.java @@ -5,6 +5,7 @@ import com.samleighton.sethomestwo.datatypes.PersistentString; import com.samleighton.sethomestwo.enums.UserError; import com.samleighton.sethomestwo.enums.UserSuccess; +import com.samleighton.sethomestwo.metrics.UsageCounters; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.utils.ChatUtils; import com.samleighton.sethomestwo.utils.ConfigUtil; @@ -25,6 +26,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Objects; +import java.util.Set; /** * The management submenu for a single home. Addressed by home id so a rename @@ -42,6 +44,8 @@ public class HomeActionsGui implements GuiScreen { public static final String ACTION_CANCEL_DELETE = "cancel-delete"; public static final String ACTION_BACK = "back"; + private static final Set KNOWN_ACTIONS = Set.of(ACTION_RENAME, ACTION_MOVE, ACTION_ICON, ACTION_DELETE, ACTION_CONFIRM_DELETE, ACTION_CANCEL_DELETE, ACTION_BACK); + private static final int SLOT_RENAME = 0; private static final int SLOT_MOVE = 1; private static final int SLOT_ICON = 2; @@ -164,6 +168,8 @@ public void onClick(InventoryClickEvent event, GuiSession session) { if (action == null) return; + if (KNOWN_ACTIONS.contains(action)) SetHomesTwo.instance().getUsageCounters().increment(UsageCounters.Family.GUI_ACTION, action); + if (ACTION_BACK.equals(action)) { session.openHomeList(player); return; diff --git a/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java b/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java index 920c8ee..3039c61 100644 --- a/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java +++ b/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java @@ -4,6 +4,7 @@ import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.datatypes.PersistentHome; import com.samleighton.sethomestwo.enums.UserError; +import com.samleighton.sethomestwo.metrics.UsageCounters; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.utils.ChatUtils; import com.samleighton.sethomestwo.utils.ConfigUtil; @@ -201,11 +202,19 @@ public void onClick(InventoryClickEvent event, GuiSession session) { if (!(clickedItem.getType().equals(backPageMaterial) || clickedItem.getType().equals(nextPageMaterial))) return; + UsageCounters counters = SetHomesTwo.instance().getUsageCounters(); + // Move to prev page - if (clickedItem.getType().equals(backPageMaterial)) currentPage--; + if (clickedItem.getType().equals(backPageMaterial)) { + currentPage--; + counters.increment(UsageCounters.Family.GUI_ACTION, UsageCounters.GUI_PAGE_PREVIOUS); + } // Move to next page - if (clickedItem.getType().equals(nextPageMaterial)) currentPage++; + if (clickedItem.getType().equals(nextPageMaterial)) { + currentPage++; + counters.increment(UsageCounters.Family.GUI_ACTION, UsageCounters.GUI_PAGE_NEXT); + } // Display new inv state to player this.displayInventory(player); @@ -241,6 +250,10 @@ public void onClick(InventoryClickEvent event, GuiSession session) { player.closeInventory(); + UsageCounters counters = SetHomesTwo.instance().getUsageCounters(); + counters.increment(UsageCounters.Family.GUI_ACTION, UsageCounters.GUI_TELEPORT); + counters.increment(UsageCounters.Family.TELEPORT_SOURCE, UsageCounters.SOURCE_GUI); + // Teleport player to home home.teleport(player); } diff --git a/src/main/java/com/samleighton/sethomestwo/metrics/Buckets.java b/src/main/java/com/samleighton/sethomestwo/metrics/Buckets.java new file mode 100644 index 0000000..fdfbaab --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/metrics/Buckets.java @@ -0,0 +1,37 @@ +package com.samleighton.sethomestwo.metrics; + +/** + * Bucket labels for the bStats scale and config pies. Ranges are what the + * dashboard shows, so change them only with a matching change to the README. + */ +public final class Buckets { + + private Buckets() {} + + public static String delay(int seconds) { + if (seconds <= 0) return "0"; + if (seconds <= 3) return "1-3"; + if (seconds <= 10) return "4-10"; + return "10+"; + } + + public static String homesPerServer(int total) { + if (total <= 0) return "0"; + if (total <= 50) return "1-50"; + if (total <= 500) return "51-500"; + if (total <= 5000) return "501-5000"; + return "5000+"; + } + + /** + * Average homes per player with at least one home, rounded down. + */ + public static String homesPerPlayer(int total, int players) { + if (total <= 0 || players <= 0) return "0"; + int average = total / players; + if (average <= 1) return "1"; + if (average <= 3) return "2-3"; + if (average <= 10) return "4-10"; + return "10+"; + } +} diff --git a/src/main/java/com/samleighton/sethomestwo/metrics/CommandUsageListener.java b/src/main/java/com/samleighton/sethomestwo/metrics/CommandUsageListener.java new file mode 100644 index 0000000..f2dafd7 --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/metrics/CommandUsageListener.java @@ -0,0 +1,69 @@ +package com.samleighton.sethomestwo.metrics; + +import com.samleighton.sethomestwo.SetHomesTwo; +import org.bukkit.Bukkit; +import org.bukkit.command.PluginCommand; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.server.ServerCommandEvent; + +import java.util.Locale; + +/** + * Counts every SetHomesTwo command typed by a player or the console, by + * canonical name and by the label actually typed. Commands owned by other + * plugins are ignored. Never cancels or alters the event. + */ +public class CommandUsageListener implements Listener { + + private final SetHomesTwo plugin; + + public CommandUsageListener(SetHomesTwo plugin) { + this.plugin = plugin; + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerCommand(PlayerCommandPreprocessEvent event) { + count(event.getMessage()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onConsoleCommand(ServerCommandEvent event) { + count(event.getCommand()); + } + + /** + * Counts the command on the given line if it belongs to this plugin. + * Swallows runtime failures: a metrics failure must never abort a command. + */ + void count(String commandLine) { + try { + String label = firstToken(commandLine); + if (label == null) return; + + PluginCommand command = Bukkit.getPluginCommand(label); + if (command == null || command.getPlugin() != plugin) return; + + String namespace = plugin.getName().toLowerCase(Locale.ROOT) + ":"; + String typed = label.startsWith(namespace) ? label.substring(namespace.length()) : label; + + UsageCounters counters = plugin.getUsageCounters(); + counters.increment(UsageCounters.Family.COMMAND, command.getName()); + counters.increment(UsageCounters.Family.ALIAS, typed); + } catch (RuntimeException ignored) { + // Counting is best effort. + } + } + + private static String firstToken(String commandLine) { + if (commandLine == null) return null; + String trimmed = commandLine.trim(); + if (trimmed.startsWith("/")) trimmed = trimmed.substring(1); + if (trimmed.isEmpty()) return null; + int space = trimmed.indexOf(' '); + String label = space < 0 ? trimmed : trimmed.substring(0, space); + return label.toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/com/samleighton/sethomestwo/metrics/Errors.java b/src/main/java/com/samleighton/sethomestwo/metrics/Errors.java new file mode 100644 index 0000000..3729e5f --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/metrics/Errors.java @@ -0,0 +1,26 @@ +package com.samleighton.sethomestwo.metrics; + +import com.samleighton.sethomestwo.SetHomesTwo; + +/** + * Counts plugin failures by category for the bStats errors chart. Categories + * only, never messages, so nothing server-specific leaves the server. + */ +public final class Errors { + + public static final String SQL_WRITE = "sql-write"; + public static final String SQL_READ = "sql-read"; + public static final String DB_CONNECT = "db-connect"; + public static final String ITEM_DATA = "item-data"; + + private Errors() {} + + /** + * Safe to call from anywhere the plugin logs a failure; a null plugin + * (before it is registered) is ignored. + */ + public static void count(String category) { + SetHomesTwo plugin = SetHomesTwo.instance(); + if (plugin != null) plugin.getUsageCounters().increment(UsageCounters.Family.ERROR, category); + } +} diff --git a/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java b/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java new file mode 100644 index 0000000..02db037 --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java @@ -0,0 +1,176 @@ +package com.samleighton.sethomestwo.metrics; + +import com.samleighton.sethomestwo.SetHomesTwo; +import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.enums.DebugLevel; +import com.samleighton.sethomestwo.utils.ConfigUtil; +import org.bstats.bukkit.Metrics; +import org.bstats.charts.AdvancedBarChart; +import org.bstats.charts.AdvancedPie; +import org.bstats.charts.SingleLineChart; +import org.bstats.charts.SimplePie; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.util.Locale; +import java.util.function.BooleanSupplier; +import java.util.function.Function; + +/** + * Sends anonymous usage counts to bStats. The only class that touches + * org.bstats; everything else increments {@link UsageCounters}. + */ +public class MetricsReporter { + + /** bStats service id for SetHomesTwo, from bstats.org. The reporter stays off while this is 0. */ + public static final int PLUGIN_ID = 33420; + + /** Keeps bStats off the boot path, same as the update check. */ + public static final long STARTUP_DELAY_TICKS = 100L; + + private final SetHomesTwo plugin; + private final int pluginId; + private final BooleanSupplier enabled; + private final Function factory; + + private AutoCloseable running; + + public MetricsReporter(SetHomesTwo plugin) { + this(plugin, + PLUGIN_ID, + () -> bStatsEnabledGlobally(plugin.getDataFolder().getParentFile()), + counters -> new BStatsHandle(plugin, counters)); + } + + /** + * bStats' server-wide switch, plugins/bStats/config.yml. Metrics has no + * per-plugin toggle, so this is the one opt-out and it is honoured before + * bStats is built at all. A missing file means enabled, as bStats itself treats it. + */ + static boolean bStatsEnabledGlobally(File pluginsDir) { + File config = new File(new File(pluginsDir, "bStats"), "config.yml"); + if (!config.isFile()) return true; + return YamlConfiguration.loadConfiguration(config).getBoolean("enabled", true); + } + + MetricsReporter(SetHomesTwo plugin, int pluginId, BooleanSupplier enabled, Function factory) { + this.plugin = plugin; + this.pluginId = pluginId; + this.enabled = enabled; + this.factory = factory; + } + + /** + * Schedules the start. The bStats switch is read when the task fires, not + * when scheduled. + */ + public void startLater() { + plugin.getServer().getScheduler().runTaskLater(plugin, this::startNow, STARTUP_DELAY_TICKS); + } + + private void startNow() { + if (running != null || pluginId <= 0 || !enabled.getAsBoolean()) return; + try { + running = factory.apply(plugin.getUsageCounters()); + } catch (Throwable t) { + if (ConfigUtil.getDebugLevel().equals(DebugLevel.INFO)) + Bukkit.getLogger().info("Could not start bStats metrics: " + t.getMessage()); + } + } + + public void shutdown() { + if (running == null) return; + try { + running.close(); + } catch (Exception ignored) { + // Nothing sensible to do at shutdown. + } + running = null; + } + + public boolean isRunning() { + return running != null; + } + + /** + * bStats chart id for one command's line chart: `command_` plus the name + * with hyphens as underscores, so `go-home` reports under `command_go_home`. + * Each id has to be registered on the plugin's bStats page. + */ + static String commandChartId(String commandName) { + return "command_" + commandName.toLowerCase(Locale.ROOT).replace('-', '_'); + } + + /** + * The real bStats wiring. Constructed only on a live server; the relocation + * check inside Metrics throws under an unshaded classpath. + */ + private static final class BStatsHandle implements AutoCloseable { + private final Metrics metrics; + + BStatsHandle(SetHomesTwo plugin, UsageCounters counters) { + metrics = new Metrics(plugin, PLUGIN_ID); + + // bStats keeps history (and time filters) only for line charts, and a + // line chart carries one number, so each usage family gets a bar chart + // for the ranking of the last window plus line charts for trends. The + // bar chart drains the family and WindowShare hands the same window to + // the line charts registered after it, so nothing is read twice. + WindowShare share = new WindowShare(counters); + + metrics.addCustomChart(new AdvancedBarChart("command_usage", () -> share.bars(UsageCounters.Family.COMMAND))); + for (String command : plugin.getDescription().getCommands().keySet()) { + metrics.addCustomChart(new SingleLineChart(commandChartId(command), + () -> share.count(UsageCounters.Family.COMMAND, command))); + } + metrics.addCustomChart(new SingleLineChart("commands_total", () -> share.total(UsageCounters.Family.COMMAND))); + + metrics.addCustomChart(new AdvancedBarChart("command_alias_usage", () -> share.bars(UsageCounters.Family.ALIAS))); + + metrics.addCustomChart(new AdvancedBarChart("gui_action_usage", () -> share.bars(UsageCounters.Family.GUI_ACTION))); + metrics.addCustomChart(new SingleLineChart("gui_actions_total", () -> share.total(UsageCounters.Family.GUI_ACTION))); + + metrics.addCustomChart(new AdvancedBarChart("errors", () -> share.bars(UsageCounters.Family.ERROR))); + metrics.addCustomChart(new SingleLineChart("errors_total", () -> share.total(UsageCounters.Family.ERROR))); + + metrics.addCustomChart(new AdvancedBarChart("teleport_source", () -> share.bars(UsageCounters.Family.TELEPORT_SOURCE))); + metrics.addCustomChart(new SingleLineChart("teleports_total", () -> share.total(UsageCounters.Family.TELEPORT_SOURCE))); + + metrics.addCustomChart(new AdvancedPie("teleport_outcome", () -> counters.snapshotAndReset(UsageCounters.Family.TELEPORT_OUTCOME))); + + metrics.addCustomChart(new SimplePie("max_homes_enabled", () -> String.valueOf(config().getBoolean("maxHomeEnabled", false)))); + metrics.addCustomChart(new SimplePie("max_homes_type", () -> { + if (!config().getBoolean("maxHomeEnabled", false)) return "off"; + String type = config().getString("maxHomesType"); + return type == null ? "groups" : type.toLowerCase(Locale.ROOT); + })); + metrics.addCustomChart(new SimplePie("cancel_on_move", () -> String.valueOf(config().getBoolean("cancelOnMove", true)))); + metrics.addCustomChart(new SimplePie("teleport_safety", () -> String.valueOf(config().getBoolean("teleportSafety", true)))); + metrics.addCustomChart(new SimplePie("teleport_delay", () -> Buckets.delay(config().getInt("delay", 3)))); + metrics.addCustomChart(new SimplePie("open_home_item", () -> defaultOrCustom("openHomeItem", "compass"))); + metrics.addCustomChart(new SimplePie("default_home_item", () -> defaultOrCustom("defaultHomeItem", "white_wool"))); + metrics.addCustomChart(new SimplePie("luckperms_installed", () -> String.valueOf(Bukkit.getPluginManager().getPlugin("LuckPerms") != null))); + metrics.addCustomChart(new SimplePie("homes_per_server", () -> Buckets.homesPerServer(new HomesDao().countAll()))); + metrics.addCustomChart(new SimplePie("homes_per_player", () -> { + HomesDao dao = new HomesDao(); + return Buckets.homesPerPlayer(dao.countAll(), dao.countPlayersWithHomes()); + })); + } + + private static FileConfiguration config() { + return ConfigUtil.getConfig(); + } + + private static String defaultOrCustom(String key, String defaultValue) { + String value = config().getString(key, defaultValue); + return value != null && value.equalsIgnoreCase(defaultValue) ? "default" : "custom"; + } + + @Override + public void close() { + metrics.shutdown(); + } + } +} diff --git a/src/main/java/com/samleighton/sethomestwo/metrics/UsageCounters.java b/src/main/java/com/samleighton/sethomestwo/metrics/UsageCounters.java new file mode 100644 index 0000000..7c74905 --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/metrics/UsageCounters.java @@ -0,0 +1,70 @@ +package com.samleighton.sethomestwo.metrics; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.LongAdder; + +/** + * In-memory usage counters, one map per family. Increments are lock-free and + * safe from any thread; a snapshot is what bStats reads once per submission. + */ +public class UsageCounters { + + public enum Family { COMMAND, ALIAS, GUI_ACTION, TELEPORT_SOURCE, TELEPORT_OUTCOME, ERROR } + + public static final String SOURCE_GUI = "gui"; + public static final String SOURCE_COMMAND = "command"; + + public static final String OUTCOME_BLACKLISTED = "blacklisted"; + public static final String OUTCOME_ALREADY_TELEPORTING = "already-teleporting"; + public static final String OUTCOME_CANCELLED_MOVED = "cancelled-moved"; + public static final String OUTCOME_UNSAFE = "unsafe"; + public static final String OUTCOME_RELOCATED = "relocated"; + public static final String OUTCOME_COMPLETED = "completed"; + + public static final String GUI_TELEPORT = "teleport"; + public static final String GUI_PAGE_NEXT = "page-next"; + public static final String GUI_PAGE_PREVIOUS = "page-previous"; + + private final Map> families = new EnumMap<>(Family.class); + + public UsageCounters() { + for (Family family : Family.values()) { + families.put(family, new ConcurrentHashMap<>()); + } + } + + public void increment(Family family, String key) { + families.get(family).computeIfAbsent(key, k -> new LongAdder()).increment(); + } + + /** + * Current counts for the family, without clearing them. + */ + public Map snapshot(Family family) { + Map out = new HashMap<>(); + families.get(family).forEach((key, adder) -> { + int value = (int) Math.min(Integer.MAX_VALUE, adder.sum()); + if (value > 0) out.put(key, value); + }); + return out; + } + + /** + * Current counts for the family, then reset so the next window starts at zero. + * This is the shape a bStats AdvancedPie callback returns. + */ + public Map snapshotAndReset(Family family) { + Map out = new HashMap<>(); + ConcurrentHashMap map = families.get(family); + for (String key : map.keySet()) { + LongAdder adder = map.remove(key); + if (adder == null) continue; + int value = (int) Math.min(Integer.MAX_VALUE, adder.sum()); + if (value > 0) out.put(key, value); + } + return out; + } +} diff --git a/src/main/java/com/samleighton/sethomestwo/metrics/WindowShare.java b/src/main/java/com/samleighton/sethomestwo/metrics/WindowShare.java new file mode 100644 index 0000000..94d81ac --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/metrics/WindowShare.java @@ -0,0 +1,53 @@ +package com.samleighton.sethomestwo.metrics; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +/** + * Lets several charts report the same submission window of one counter family + * without draining it more than once. bStats runs every chart callback in + * registration order within one submission, so the bar chart drains and parks + * the window, and the line charts registered after it read the parked copy. + */ +final class WindowShare { + + private final UsageCounters counters; + private final Map> parked = new EnumMap<>(UsageCounters.Family.class); + + WindowShare(UsageCounters counters) { + this.counters = counters; + } + + /** + * Drains the family into the one-value-per-bar shape AdvancedBarChart wants + * and parks the window for {@link #count} and {@link #total}. + */ + Map bars(UsageCounters.Family family) { + Map window = counters.snapshotAndReset(family); + parked.put(family, window); + Map out = new HashMap<>(); + window.forEach((key, count) -> out.put(key, new int[]{count})); + return out; + } + + /** One key's count in the parked window, 0 when absent. */ + int count(UsageCounters.Family family, String key) { + return window(family).getOrDefault(key, 0); + } + + /** Sum of every key in the parked window. */ + int total(UsageCounters.Family family) { + int sum = 0; + for (int value : window(family).values()) sum += value; + return sum; + } + + /** + * The parked window, draining the live counters if no bar chart parked one + * first so a line chart registered on its own still reports. + */ + private Map window(UsageCounters.Family family) { + return parked.computeIfAbsent(family, counters::snapshotAndReset); + } +} diff --git a/src/main/java/com/samleighton/sethomestwo/models/Home.java b/src/main/java/com/samleighton/sethomestwo/models/Home.java index ff98035..71f583d 100644 --- a/src/main/java/com/samleighton/sethomestwo/models/Home.java +++ b/src/main/java/com/samleighton/sethomestwo/models/Home.java @@ -6,6 +6,7 @@ import com.samleighton.sethomestwo.enums.UserError; import com.samleighton.sethomestwo.enums.UserInfo; import com.samleighton.sethomestwo.enums.UserSuccess; +import com.samleighton.sethomestwo.metrics.UsageCounters; import com.samleighton.sethomestwo.utils.ChatUtils; import com.samleighton.sethomestwo.utils.ConfigUtil; import com.samleighton.sethomestwo.utils.TeleportSafetyUtil; @@ -203,6 +204,7 @@ public void teleport(Player player) { // Home is blacklisted guard if(!this.getCanTeleport()) { + countOutcome(UsageCounters.OUTCOME_BLACKLISTED); ChatUtils.sendError(player, ConfigUtil.getConfig().getString("teleportToBlacklistedDimension", UserError.TELEPORT_IS_BLACKLISTED.getValue())); return; } @@ -212,6 +214,7 @@ public void teleport(Player player) { // Guard to check if player is currently teleporting if (isAlreadyTeleporting) { + countOutcome(UsageCounters.OUTCOME_ALREADY_TELEPORTING); ChatUtils.sendError(player, ConfigUtil.getConfig().getString("teleportedWhileTeleporting", UserError.ALREADY_TELEPORTING.getValue())); return; } @@ -251,6 +254,7 @@ public void teleport(Player player) { teleportAttemptsDao.delete(player.getUniqueId()); player.resetTitle(); player.removePotionEffect(PotionEffectType.NAUSEA); + countOutcome(UsageCounters.OUTCOME_CANCELLED_MOVED); bukkitTask.cancel(); return; } @@ -277,18 +281,21 @@ public void teleport(Player player) { Location safeDestination = TeleportSafetyUtil.findSafeLocation(destination); TeleportSafetyUtil.releaseChunkTickets(prefetchDestination, plugin); if (safeDestination == null) { + countOutcome(UsageCounters.OUTCOME_UNSAFE); ChatUtils.sendError(player, ConfigUtil.getConfig().getString("unsafeHome", UserError.UNSAFE_HOME.getValue())); player.resetTitle(); player.removePotionEffect(PotionEffectType.NAUSEA); return; } if (safeDestination != destination) { + countOutcome(UsageCounters.OUTCOME_RELOCATED); ChatUtils.sendInfo(player, ConfigUtil.getConfig().getString("movedToSafeSpot", UserInfo.MOVED_TO_SAFE_SPOT.getValue())); } destination = safeDestination; } player.teleport(destination); + countOutcome(UsageCounters.OUTCOME_COMPLETED); player.removePotionEffect(PotionEffectType.NAUSEA); player.resetTitle(); player.playNote(player.getLocation(), Instrument.BELL, Note.sharp(2, Note.Tone.F)); @@ -299,4 +306,8 @@ public void teleport(Player player) { }, 0, 20L); } + + private static void countOutcome(String outcome) { + SetHomesTwo.instance().getUsageCounters().increment(UsageCounters.Family.TELEPORT_OUTCOME, outcome); + } } diff --git a/src/main/java/com/samleighton/sethomestwo/tabcompleters/MaterialsTabCompleter.java b/src/main/java/com/samleighton/sethomestwo/tabcompleters/MaterialsTabCompleter.java index ec35870..b4ad05b 100644 --- a/src/main/java/com/samleighton/sethomestwo/tabcompleters/MaterialsTabCompleter.java +++ b/src/main/java/com/samleighton/sethomestwo/tabcompleters/MaterialsTabCompleter.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Locale; public class MaterialsTabCompleter implements TabCompleter { @Nullable @@ -29,7 +30,9 @@ public List onTabComplete(@NotNull CommandSender commandSender, @NotNull Material[] allMaterials = Material.values(); for(Material mat : allMaterials){ if(!mat.isItem()) continue; - validMaterials.add(mat.getKey().toString().toLowerCase()); + // Every non-legacy material is keyed minecraft:, so this equals + // getKey() without touching the deprecated accessor. + validMaterials.add("minecraft:" + mat.name().toLowerCase(Locale.ROOT)); } completions.addAll(TabCompletions.matching(args[1], validMaterials)); diff --git a/src/main/java/com/samleighton/sethomestwo/utils/DatabaseUtil.java b/src/main/java/com/samleighton/sethomestwo/utils/DatabaseUtil.java index 2648e81..06d8552 100644 --- a/src/main/java/com/samleighton/sethomestwo/utils/DatabaseUtil.java +++ b/src/main/java/com/samleighton/sethomestwo/utils/DatabaseUtil.java @@ -1,6 +1,7 @@ package com.samleighton.sethomestwo.utils; import com.samleighton.sethomestwo.enums.DebugLevel; +import com.samleighton.sethomestwo.metrics.Errors; import org.bukkit.Bukkit; import org.jetbrains.annotations.Nullable; @@ -86,6 +87,7 @@ public static boolean execute(Connection connection, String sql, Object... param return true; } catch (SQLException e) { Bukkit.getLogger().severe("Could not execute sql statement."); + Errors.count(Errors.SQL_WRITE); } return false; @@ -106,6 +108,7 @@ public static int executeUpdate(Connection connection, String sql, Object... par return statement.executeUpdate(); } catch (SQLException e) { Bukkit.getLogger().severe("Could not execute sql update statement."); + Errors.count(Errors.SQL_WRITE); } return -1; @@ -128,6 +131,7 @@ public static ResultSet fetch(Connection connection, String sql, Object... param return statement.executeQuery(); } catch (SQLException e) { Bukkit.getLogger().severe("Could not execute sql fetch statement."); + Errors.count(Errors.SQL_READ); } return null; diff --git a/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java b/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java index ed7e84f..844ab83 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/BlacklistTest.java @@ -17,7 +17,7 @@ void addStoresTheWorld() { PlayerMock player = addPlayer(); player.addAttachment(plugin, "sh2.add-to-blacklist", true); - server.execute("blacklist", player, "add", "world_nether").assertSucceeded(); + assertTrue(server.execute("blacklist", player, "add", "world_nether").hasSucceeded()); assertTrue(new BlacklistDao().getAll().contains("world_nether")); } @@ -28,7 +28,7 @@ void removeDropsTheWorld() { PlayerMock player = addPlayer(); player.addAttachment(plugin, "sh2.remove-from-blacklist", true); - server.execute("blacklist", player, "remove", "world_nether").assertSucceeded(); + assertTrue(server.execute("blacklist", player, "remove", "world_nether").hasSucceeded()); assertFalse(new BlacklistDao().getAll().contains("world_nether")); } @@ -39,7 +39,7 @@ void listPrintsTheEntries() { PlayerMock player = addPlayer(); player.addAttachment(plugin, "sh2.get-blacklisted-dimensions", true); - server.execute("blacklist", player, "list").assertSucceeded(); + assertTrue(server.execute("blacklist", player, "list").hasSucceeded()); assertTrue(player.nextMessage().contains("world_nether")); } @@ -50,7 +50,7 @@ void addIsRefusedWithoutItsOwnNode() { player.addAttachment(plugin, "sh2.get-blacklisted-dimensions", true); player.addAttachment(plugin, "sh2.add-to-blacklist", false); - server.execute("blacklist", player, "add", "world_nether").assertSucceeded(); + assertTrue(server.execute("blacklist", player, "add", "world_nether").hasSucceeded()); assertTrue(player.nextMessage().contains("permission")); assertFalse(new BlacklistDao().getAll().contains("world_nether")); @@ -63,7 +63,7 @@ void removeIsRefusedWithoutItsOwnNode() { player.addAttachment(plugin, "sh2.get-blacklisted-dimensions", true); player.addAttachment(plugin, "sh2.remove-from-blacklist", false); - server.execute("blacklist", player, "remove", "world_nether").assertSucceeded(); + assertTrue(server.execute("blacklist", player, "remove", "world_nether").hasSucceeded()); assertTrue(player.nextMessage().contains("permission")); assertTrue(new BlacklistDao().getAll().contains("world_nether")); @@ -77,7 +77,7 @@ void listIsRefusedWithoutItsOwnNode() { player.addAttachment(plugin, "sh2.remove-from-blacklist", true); player.addAttachment(plugin, "sh2.get-blacklisted-dimensions", false); - server.execute("blacklist", player, "list").assertSucceeded(); + assertTrue(server.execute("blacklist", player, "list").hasSucceeded()); assertTrue(player.nextMessage().contains("permission")); } @@ -87,7 +87,7 @@ void theOldCommandNameStillWorks() { PlayerMock player = addPlayer(); player.addAttachment(plugin, "sh2.add-to-blacklist", true); - server.execute("add-to-blacklist", player, "add", "world_nether").assertSucceeded(); + assertTrue(server.execute("add-to-blacklist", player, "add", "world_nether").hasSucceeded()); assertTrue(new BlacklistDao().getAll().contains("world_nether")); } @@ -97,7 +97,7 @@ void anUnknownWorldIsRejected() { PlayerMock player = addPlayer(); player.addAttachment(plugin, "sh2.add-to-blacklist", true); - server.execute("blacklist", player, "add", "not_a_world").assertSucceeded(); + assertTrue(server.execute("blacklist", player, "add", "not_a_world").hasSucceeded()); String message = player.nextMessage(); assertTrue(message.contains("not_a_world"), message); @@ -113,7 +113,7 @@ void theAddSuccessMessageIsOverridableInConfig() { player.addAttachment(plugin, "sh2.add-to-blacklist", true); plugin.getConfig().set("dimensionAddedToBlacklist", "Blocked %s."); - server.execute("blacklist", player, "add", "world_nether").assertSucceeded(); + assertTrue(server.execute("blacklist", player, "add", "world_nether").hasSucceeded()); assertTrue(player.nextMessage().contains("Blocked world_nether.")); } @@ -124,7 +124,7 @@ void aFailedAddIsReportedInsteadOfClaimingSuccess() { player.addAttachment(plugin, "sh2.add-to-blacklist", true); HomeFixtures.breakBlacklistWrites(); - server.execute("blacklist", player, "add", "world_nether").assertSucceeded(); + assertTrue(server.execute("blacklist", player, "add", "world_nether").hasSucceeded()); String message = player.nextMessage(); assertTrue(message.contains("issue adding dimension"), message); @@ -139,7 +139,7 @@ void aFailedRemoveIsReportedInsteadOfClaimingSuccess() { player.addAttachment(plugin, "sh2.remove-from-blacklist", true); HomeFixtures.breakBlacklistWrites(); - server.execute("blacklist", player, "remove", "world_nether").assertSucceeded(); + assertTrue(server.execute("blacklist", player, "remove", "world_nether").hasSucceeded()); String message = player.nextMessage(); assertTrue(message.contains("issue removing dimension"), message); diff --git a/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java index 6711f75..3f1a491 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/CreateHomeTest.java @@ -17,7 +17,6 @@ import java.util.logging.LogRecord; import java.util.logging.Logger; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -29,7 +28,7 @@ void aHomeIsCreatedAtThePlayersLocation() { PlayerMock player = addPlayer(); player.teleport(new Location(overworld, 12, 65, -8)); - server.execute("create-home", player, "base").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base").hasSucceeded()); var homes = new HomesDao().getAll(player.getUniqueId()); assertEquals(1, homes.size()); @@ -42,7 +41,7 @@ void aBareCommandCreatesTheDefaultHome() { PlayerMock player = addPlayer(); player.teleport(new Location(overworld, 7, 65, 7)); - server.execute("create-home", player).assertSucceeded(); + assertTrue(server.execute("create-home", player).hasSucceeded()); List homes = new HomesDao().getAll(player.getUniqueId()); assertEquals(1, homes.size()); @@ -56,9 +55,9 @@ void aBareCommandCreatesTheDefaultHome() { void theNameDefaultBehavesLikeTheBareCommand() { PlayerMock player = addPlayer(); - server.execute("create-home", player, "default").assertSucceeded(); + assertTrue(server.execute("create-home", player, "default").hasSucceeded()); player.nextMessage(); - server.execute("create-home", player).assertSucceeded(); + assertTrue(server.execute("create-home", player).hasSucceeded()); assertTrue(player.nextMessage().contains("You already have a home called")); List homes = new HomesDao().getAll(player.getUniqueId()); @@ -74,7 +73,7 @@ void theNameDefaultBehavesLikeTheBareCommand() { void theIconSentinelNeverBecomesTheHomeName() { PlayerMock player = addPlayer(); - server.execute("create-home", player, "base", "default").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base", "default").hasSucceeded()); List homes = new HomesDao().getAll(player.getUniqueId()); assertEquals(1, homes.size()); @@ -88,7 +87,7 @@ void aDuplicateNameIsRejected() { PlayerMock player = addPlayer(); HomeFixtures.persist(player, "base"); - server.execute("create-home", player, "BASE").assertSucceeded(); + assertTrue(server.execute("create-home", player, "BASE").hasSucceeded()); assertTrue(player.nextMessage().contains("You already have a home called")); assertEquals(1, new HomesDao().getAll(player.getUniqueId()).size()); @@ -98,7 +97,7 @@ void aDuplicateNameIsRejected() { void aNonMaterialSecondArgumentBecomesTheDescription() { PlayerMock player = addPlayer(); - server.execute("create-home", player, "base", "my", "main", "base").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base", "my", "main", "base").hasSucceeded()); var homes = new HomesDao().getAll(player.getUniqueId()); assertEquals(1, homes.size()); @@ -110,7 +109,7 @@ void aNonMaterialSecondArgumentBecomesTheDescription() { void aMaterialSecondArgumentStillSetsTheIcon() { PlayerMock player = addPlayer(); - server.execute("create-home", player, "base", "diamond_block", "my", "base").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base", "diamond_block", "my", "base").hasSucceeded()); var homes = new HomesDao().getAll(player.getUniqueId()); assertEquals(Material.DIAMOND_BLOCK.name(), homes.get(0).getMaterial()); @@ -121,7 +120,7 @@ void aMaterialSecondArgumentStillSetsTheIcon() { void theChosenIconIsNamedInTheSuccessMessage() { PlayerMock player = addPlayer(); - server.execute("create-home", player, "base", "diamond_block").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base", "diamond_block").hasSucceeded()); assertTrue(player.nextMessage().contains("DIAMOND_BLOCK")); } @@ -135,7 +134,7 @@ void theChosenIconIsNamedInTheSuccessMessage() { void aDescriptionBeginningWithAMaterialWordLosesThatWordToTheIcon() { PlayerMock player = addPlayer(); - server.execute("create-home", player, "base", "stone", "house").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base", "stone", "house").hasSucceeded()); Home home = new HomesDao().getAll(player.getUniqueId()).get(0); assertEquals(Material.STONE.name(), home.getMaterial()); @@ -146,7 +145,7 @@ void aDescriptionBeginningWithAMaterialWordLosesThatWordToTheIcon() { void aSuppliedMaterialIsStored() { PlayerMock player = addPlayer(); - server.execute("create-home", player, "base", "diamond").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base", "diamond").hasSucceeded()); assertEquals(Material.DIAMOND.name(), new HomesDao().getAll(player.getUniqueId()).get(0).getMaterial()); } @@ -157,7 +156,7 @@ void aBlacklistedDimensionIsRejected() { player.teleport(new Location(overworld, 0, 64, 0)); HomeFixtures.blacklist(overworld.getName()); - server.execute("create-home", player, "base").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base").hasSucceeded()); assertTrue(player.nextMessage().contains("You cannot set a home in this dimension")); assertTrue(new HomesDao().getAll(player.getUniqueId()).isEmpty()); @@ -172,7 +171,7 @@ void theSingularMaxHomesLimitIsEnforced() { HomeFixtures.persist(player, "base"); - server.execute("create-home", player, "camp").assertSucceeded(); + assertTrue(server.execute("create-home", player, "camp").hasSucceeded()); assertTrue(player.nextMessage().contains("maximum number of homes")); assertEquals(1, new HomesDao().getAll(player.getUniqueId()).size()); @@ -188,7 +187,7 @@ void groupLimitsAreSkippedWhenLuckPermsIsAbsent() { // The count alone would pass even if the groups branch were never reached. List logged = captureLog(() -> - server.execute("create-home", player, "camp").assertSucceeded()); + assertTrue(server.execute("create-home", player, "camp").hasSucceeded())); assertEquals(2, new HomesDao().getAll(player.getUniqueId()).size()); assertTrue(loggedWarning(logged, @@ -239,7 +238,7 @@ void aValidButNonItemMaterialWordIsDescriptionText() { // water names a real Material but not an item. Storing it as the icon // would make HomesGui throw on new ItemStack and the menu stop opening. - server.execute("create-home", player, "base", "water", "front").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base", "water", "front").hasSucceeded()); Home home = new HomesDao().getAll(player.getUniqueId()).get(0); assertEquals(Material.WHITE_WOOL.name(), home.getMaterial()); @@ -249,9 +248,9 @@ void aValidButNonItemMaterialWordIsDescriptionText() { @Test void theHomesMenuStillOpensAfterAHomeNamedAfterANonItem() { PlayerMock player = addPlayer(); - server.execute("create-home", player, "base", "water", "front").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base", "water", "front").hasSucceeded()); - assertDoesNotThrow(() -> server.execute("homes", player).assertSucceeded()); + assertTrue(server.execute("homes", player).hasSucceeded()); } /** @@ -273,8 +272,8 @@ void anEmptySecondArgumentDoesNotLeakIntoTheDescription() { void theDefaultIconSentinelIsNotDescriptionText() { PlayerMock player = addPlayer(); - server.execute("create-home", player, "base", "d").assertSucceeded(); - server.execute("create-home", player, "camp", "default", "my", "spot").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base", "d").hasSucceeded()); + assertTrue(server.execute("create-home", player, "camp", "default", "my", "spot").hasSucceeded()); List homes = new HomesDao().getAll(player.getUniqueId()); Home base = homes.stream().filter(h -> h.getName().equals("base")).findFirst().orElseThrow(); @@ -291,7 +290,7 @@ void aDefaultIconThatNamesNoItemFallsBackToWhiteWool() { PlayerMock player = addPlayer(); plugin.getConfig().set("defaultHomeItem", "water"); - server.execute("create-home", player, "base").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base").hasSucceeded()); assertEquals(Material.WHITE_WOOL.name(), new HomesDao().getAll(player.getUniqueId()).get(0).getMaterial()); } @@ -301,7 +300,7 @@ void theConfiguredDefaultIconIsUsedWhenNoneIsGiven() { PlayerMock player = addPlayer(); plugin.getConfig().set("defaultHomeItem", "chest"); - server.execute("create-home", player, "base").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base").hasSucceeded()); assertEquals(Material.CHEST.name(), new HomesDao().getAll(player.getUniqueId()).get(0).getMaterial()); } @@ -324,7 +323,7 @@ void aHomeNameOverTheConfiguredLimitIsRejected() { PlayerMock player = addPlayer(); plugin.getConfig().set("maxHomeNameLength", 8); - server.execute("create-home", player, "waaaaaaaaaaaytoolong").assertSucceeded(); + assertTrue(server.execute("create-home", player, "waaaaaaaaaaaytoolong").hasSucceeded()); assertTrue(player.nextMessage().contains("too long")); assertTrue(new HomesDao().getAll(player.getUniqueId()).isEmpty()); @@ -335,7 +334,7 @@ void aNameWithinTheConfiguredLimitIsAccepted() { PlayerMock player = addPlayer(); plugin.getConfig().set("maxHomeNameLength", 8); - server.execute("create-home", player, "base").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base").hasSucceeded()); assertEquals(1, new HomesDao().getAll(player.getUniqueId()).size()); } diff --git a/src/test/java/com/samleighton/sethomestwo/commands/DeleteHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/DeleteHomeTest.java index 171c560..1674add 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/DeleteHomeTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/DeleteHomeTest.java @@ -16,7 +16,7 @@ class DeleteHomeTest extends ServerTestBase { @Test void consoleIsTurnedAway() { - server.executeConsole("delete-home", "base").assertSucceeded(); + assertTrue(server.executeConsole("delete-home", "base").hasSucceeded()); assertTrue(server.getConsoleSender().nextMessage().contains("Only players")); } @@ -25,7 +25,7 @@ void wrongArgumentCountReportsUsage() { PlayerMock player = addPlayer(); HomeFixtures.persist(player, "base"); - server.execute("delete-home", player).assertSucceeded(); + assertTrue(server.execute("delete-home", player).hasSucceeded()); assertTrue(player.nextMessage().contains("Incorrect number of arguments")); assertEquals(1, new HomesDao().getAll(player.getUniqueId()).size()); @@ -36,7 +36,7 @@ void anUnknownHomeIsReportedAndNothingIsDeleted() { PlayerMock player = addPlayer(); HomeFixtures.persist(player, "base"); - server.execute("delete-home", player, "nowhere").assertSucceeded(); + assertTrue(server.execute("delete-home", player, "nowhere").hasSucceeded()); assertTrue(player.nextMessage().contains("You do not have a home by the name")); assertEquals(1, new HomesDao().getAll(player.getUniqueId()).size()); @@ -48,7 +48,7 @@ void aHomeIsDeleted() { HomeFixtures.persist(player, "base"); HomeFixtures.persist(player, "camp"); - server.execute("delete-home", player, "base").assertSucceeded(); + assertTrue(server.execute("delete-home", player, "base").hasSucceeded()); List remaining = new HomesDao().getAll(player.getUniqueId()); assertEquals(1, remaining.size()); @@ -61,7 +61,7 @@ void withTwoHomesSharingANameOnlyOneIsDeleted() { HomeFixtures.persist(player, "base"); HomeFixtures.persist(player, "base"); - server.execute("delete-home", player, "base").assertSucceeded(); + assertTrue(server.execute("delete-home", player, "base").hasSucceeded()); // The 1.2.0 behaviour change: previously this removed every matching row. assertEquals(1, new HomesDao().getAll(player.getUniqueId()).size()); diff --git a/src/test/java/com/samleighton/sethomestwo/commands/GetPlayerHomesTest.java b/src/test/java/com/samleighton/sethomestwo/commands/GetPlayerHomesTest.java index efde2d8..2eef45a 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/GetPlayerHomesTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/GetPlayerHomesTest.java @@ -17,7 +17,7 @@ class GetPlayerHomesTest extends ServerTestBase { void aNonOpIsRefused() { PlayerMock admin = addPlayer(); - server.execute("get-player-homes", admin, "someone").assertSucceeded(); + assertTrue(server.execute("get-player-homes", admin, "someone").hasSucceeded()); // Bukkit's dispatcher rejects non-op senders before onCommand runs, so this // is its denial message, not the plugin's. The substring matches both. @@ -29,7 +29,7 @@ void anOfflineOrUnknownPlayerIsReported() { PlayerMock admin = addPlayer(); admin.setOp(true); - server.execute("get-player-homes", admin, "nobody").assertSucceeded(); + assertTrue(server.execute("get-player-homes", admin, "nobody").hasSucceeded()); assertTrue(admin.nextMessage().contains("No player by that name")); } @@ -39,7 +39,7 @@ void wrongArgumentCountIsReported() { PlayerMock admin = addPlayer(); admin.setOp(true); - server.execute("get-player-homes", admin).assertSucceeded(); + assertTrue(server.execute("get-player-homes", admin).hasSucceeded()); assertTrue(admin.nextMessage().contains("Incorrect number of arguments")); } @@ -51,7 +51,7 @@ void anAdminSeesAnotherPlayersHomes() { admin.setOp(true); HomeFixtures.persist(target, "base"); - server.execute("get-player-homes", admin, "target").assertSucceeded(); + assertTrue(server.execute("get-player-homes", admin, "target").hasSucceeded()); GuiSession session = plugin.getGuiSessionMap().get(admin.getUniqueId()); assertNotNull(session); diff --git a/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java index 2d0da03..ed5d1b7 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java @@ -1,6 +1,7 @@ package com.samleighton.sethomestwo.commands; import com.samleighton.sethomestwo.dao.TeleportAttemptsDao; +import com.samleighton.sethomestwo.metrics.UsageCounters; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; @@ -9,6 +10,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.Map; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -25,9 +28,13 @@ void disableTeleportSafety() { plugin.getConfig().set("teleportSafety", false); } + private Map outcomes() { + return plugin.getUsageCounters().snapshot(UsageCounters.Family.TELEPORT_OUTCOME); + } + @Test void consoleIsTurnedAway() { - server.executeConsole("go-home", "base").assertSucceeded(); + assertTrue(server.executeConsole("go-home", "base").hasSucceeded()); assertTrue(server.getConsoleSender().nextMessage().contains("Only players")); } @@ -35,7 +42,7 @@ void consoleIsTurnedAway() { void tooManyArgumentsAreRejected() { TestPlayer player = addTestPlayer("traveller"); - server.execute("go-home", player, "base", "camp").assertSucceeded(); + assertTrue(server.execute("go-home", player, "base", "camp").hasSucceeded()); assertTrue(player.nextMessage().contains("Incorrect number of arguments")); } @@ -47,7 +54,7 @@ void aBareCommandTeleportsToTheDefaultHome() { HomeFixtures.persist(HomeFixtures.home(player, "default", new Location(overworld, 44, 70, 44))); plugin.getConfig().set("delay", 0); - server.execute("go-home", player).assertSucceeded(); + assertTrue(server.execute("go-home", player).hasSucceeded()); server.getScheduler().performTicks(100L); assertEquals(44, player.getLocation().getBlockX()); @@ -59,7 +66,7 @@ void aBareCommandWithNoDefaultHomeIsReported() { TestPlayer player = addTestPlayer("traveller"); HomeFixtures.persist(player, "base"); - server.execute("go-home", player).assertSucceeded(); + assertTrue(server.execute("go-home", player).hasSucceeded()); String message = player.nextMessage(); assertTrue(message.contains("default"), message); @@ -70,7 +77,7 @@ void aBareCommandWithNoDefaultHomeIsReported() { void anUnknownHomeIsNamedInTheError() { TestPlayer player = addTestPlayer("traveller"); - server.execute("go-home", player, "nope").assertSucceeded(); + assertTrue(server.execute("go-home", player, "nope").hasSucceeded()); String message = player.nextMessage(); assertTrue(message.contains("nope"), message); @@ -82,7 +89,7 @@ void theUnknownHomeMessageIsOverridableInConfig() { TestPlayer player = addTestPlayer("traveller"); plugin.getConfig().set("homeDoesNotExist", "No home called %s here."); - server.execute("go-home", player, "nope").assertSucceeded(); + assertTrue(server.execute("go-home", player, "nope").hasSucceeded()); String message = player.nextMessage(); assertTrue(message.contains("No home called nope here."), message); @@ -93,7 +100,7 @@ void withoutPermissionTheCommandIsRefused() { TestPlayer player = addTestPlayer("traveller"); player.addAttachment(plugin, "sh2.go-home", false); - server.execute("go-home", player, "base").assertSucceeded(); + assertTrue(server.execute("go-home", player, "base").hasSucceeded()); assertTrue(player.nextMessage().contains("do not have permission")); } @@ -103,7 +110,7 @@ void anUnknownHomeIsReported() { TestPlayer player = addTestPlayer("traveller"); HomeFixtures.persist(player, "base"); - server.execute("go-home", player, "nowhere").assertSucceeded(); + assertTrue(server.execute("go-home", player, "nowhere").hasSucceeded()); assertTrue(player.nextMessage().contains("does not exist")); } @@ -116,7 +123,7 @@ void aBlacklistedHomeRefusesToTeleport() { HomeFixtures.blacklist(overworld.getName()); Location before = player.getLocation(); - server.execute("go-home", player, "base").assertSucceeded(); + assertTrue(server.execute("go-home", player, "base").hasSucceeded()); server.getScheduler().performTicks(100L); assertTrue(player.nextMessage().contains("cannot teleport to this home")); @@ -131,11 +138,11 @@ void aSecondTeleportWhileOneIsRunningIsRefused() { HomeFixtures.persist(HomeFixtures.home(player, "base", new Location(overworld, 100, 70, 100))); plugin.getConfig().set("delay", 3); - server.execute("go-home", player, "base").assertSucceeded(); + assertTrue(server.execute("go-home", player, "base").hasSucceeded()); server.getScheduler().performOneTick(); player.nextMessage(); - server.execute("go-home", player, "base").assertSucceeded(); + assertTrue(server.execute("go-home", player, "base").hasSucceeded()); assertTrue(player.nextMessage().contains("cannot teleport while already teleporting")); } @@ -147,7 +154,7 @@ void theCountdownRecordsAnAttemptAndTitlesThePlayer() { HomeFixtures.persist(HomeFixtures.home(player, "base", new Location(overworld, 100, 70, 100))); plugin.getConfig().set("delay", 3); - server.execute("go-home", player, "base").assertSucceeded(); + assertTrue(server.execute("go-home", player, "base").hasSucceeded()); server.getScheduler().performOneTick(); assertNotNull(new TeleportAttemptsDao().get(player)); @@ -161,7 +168,7 @@ void thePlayerArrivesOnceTheCountdownCompletes() { HomeFixtures.persist(HomeFixtures.home(player, "base", new Location(overworld, 100, 70, 100))); plugin.getConfig().set("delay", 0); - server.execute("go-home", player, "base").assertSucceeded(); + assertTrue(server.execute("go-home", player, "base").hasSucceeded()); server.getScheduler().performTicks(100L); assertEquals(100, player.getLocation().getBlockX()); @@ -176,10 +183,56 @@ void withoutTheTeleportNodeNoRouteToAHomeWorks() { player.addAttachment(plugin, "sh2.teleport", false); player.teleport(new Location(overworld, 0, 70, 0)); - server.execute("go-home", player, "base").assertSucceeded(); + assertTrue(server.execute("go-home", player, "base").hasSucceeded()); server.getScheduler().performTicks(100L); assertTrue(player.nextMessage().contains("permission")); assertEquals(0.0, player.getLocation().getX()); } + + @Test + void aCompletedCommandTeleportCountsSourceAndOutcome() { + TestPlayer player = addTestPlayer("traveller"); + player.teleport(new Location(overworld, 0, 64, 0)); + HomeFixtures.persist(HomeFixtures.home(player, "base", new Location(overworld, 44, 70, 44))); + plugin.getConfig().set("delay", 0); + + assertTrue(server.execute("go-home", player, "base").hasSucceeded()); + server.getScheduler().performTicks(100L); + + assertEquals(1, plugin.getUsageCounters().snapshot(UsageCounters.Family.TELEPORT_SOURCE).get(UsageCounters.SOURCE_COMMAND)); + assertEquals(1, outcomes().get(UsageCounters.OUTCOME_COMPLETED)); + assertEquals(1, outcomes().size()); + } + + @Test + void movingDuringTheCountdownCountsACancelledTeleport() { + TestPlayer player = addTestPlayer("traveller"); + player.teleport(new Location(overworld, 0, 64, 0)); + HomeFixtures.persist(HomeFixtures.home(player, "base", new Location(overworld, 100, 70, 100))); + plugin.getConfig().set("delay", 3); + + assertTrue(server.execute("go-home", player, "base").hasSucceeded()); + server.getScheduler().performOneTick(); + player.teleport(new Location(overworld, 5, 64, 5)); + server.getScheduler().performTicks(100L); + + assertEquals(1, outcomes().get(UsageCounters.OUTCOME_CANCELLED_MOVED)); + assertFalse(outcomes().containsKey(UsageCounters.OUTCOME_COMPLETED)); + } + + @Test + void aSecondTeleportWhileOneIsRunningCountsAlreadyTeleporting() { + TestPlayer player = addTestPlayer("traveller"); + player.teleport(new Location(overworld, 0, 64, 0)); + HomeFixtures.persist(HomeFixtures.home(player, "base", new Location(overworld, 100, 70, 100))); + plugin.getConfig().set("delay", 3); + + assertTrue(server.execute("go-home", player, "base").hasSucceeded()); + server.getScheduler().performOneTick(); + assertTrue(server.execute("go-home", player, "base").hasSucceeded()); + + assertEquals(1, outcomes().get(UsageCounters.OUTCOME_ALREADY_TELEPORTING)); + assertEquals(2, plugin.getUsageCounters().snapshot(UsageCounters.Family.TELEPORT_SOURCE).get(UsageCounters.SOURCE_COMMAND)); + } } diff --git a/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java b/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java index 8059df1..741df9c 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/ImportHomesTest.java @@ -45,7 +45,7 @@ void blacklistActivityAddsASecondReplyLine() throws IOException { writeBlacklist("world_nether"); PlayerMock player = authorizedPlayer(); - server.execute("import-homes", player, "sethomes").assertSucceeded(); + assertTrue(server.execute("import-homes", player, "sethomes").hasSucceeded()); player.nextMessage(); // homes summary line assertTrue(player.nextMessage().contains("blacklist")); @@ -56,7 +56,7 @@ void noBlacklistActivityMeansNoSecondLine() throws IOException { writeEmptyHomesFile(); PlayerMock player = authorizedPlayer(); - server.execute("import-homes", player, "sethomes").assertSucceeded(); + assertTrue(server.execute("import-homes", player, "sethomes").hasSucceeded()); // 0 homes and 0 blacklist activity: only the summary line is sent. The // dry-run hint is gated on (imported > 0 || blacklistImported > 0), so @@ -73,7 +73,7 @@ void dryRunHintIsNotShownWhenTheOnlyBlacklistActivityIsAlreadyPresent() throws I writeBlacklist("world_nether"); PlayerMock player = authorizedPlayer(); - server.execute("import-homes", player, "sethomes").assertSucceeded(); + assertTrue(server.execute("import-homes", player, "sethomes").hasSucceeded()); boolean sawHint = false; String message; @@ -91,7 +91,7 @@ void configNotesArePrintedAsConfigLines() throws IOException { v1Config.save(new File(setHomesDir(), "config.yml")); PlayerMock player = authorizedPlayer(); - server.execute("import-homes", player, "sethomes").assertSucceeded(); + assertTrue(server.execute("import-homes", player, "sethomes").hasSucceeded()); boolean sawConfigLine = false; String message; diff --git a/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java index 7d5d4fa..6be327d 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/MoveHomeTest.java @@ -20,7 +20,7 @@ void theHomeMovesToThePlayersLocation() { HomeFixtures.persist(player, "base"); player.teleport(new Location(overworld, 100, 70, -40)); - server.execute("move-home", player, "base").assertSucceeded(); + assertTrue(server.execute("move-home", player, "base").hasSucceeded()); Home moved = new HomesDao().getAll(player.getUniqueId()).get(0); assertEquals(100.0, moved.getX()); @@ -31,7 +31,7 @@ void theHomeMovesToThePlayersLocation() { void anUnknownHomeIsReported() { PlayerMock player = addPlayer(); - server.execute("move-home", player, "nope").assertSucceeded(); + assertTrue(server.execute("move-home", player, "nope").hasSucceeded()); String message = player.nextMessage(); assertTrue(message.contains("nope"), message); @@ -46,7 +46,7 @@ void withoutPermissionTheCommandIsRefused() { Location before = player.getLocation(); player.teleport(new Location(overworld, 100, 70, -40)); - server.execute("move-home", player, "base").assertSucceeded(); + assertTrue(server.execute("move-home", player, "base").hasSucceeded()); assertTrue(player.nextMessage().contains("permission")); assertEquals(before.getX(), new HomesDao().getAll(player.getUniqueId()).get(0).getX()); @@ -59,7 +59,7 @@ void movingIntoABlacklistedWorldIsRefused() { HomeFixtures.blacklist("world_nether"); player.teleport(new Location(nether, 10, 70, 10)); - server.execute("move-home", player, "base").assertSucceeded(); + assertTrue(server.execute("move-home", player, "base").hasSucceeded()); assertTrue(player.nextMessage().contains("blacklisted")); } @@ -70,7 +70,7 @@ void theAliasWorks() { HomeFixtures.persist(player, "base"); player.teleport(new Location(overworld, 5, 70, 5)); - server.execute("uhome", player, "base").assertSucceeded(); + assertTrue(server.execute("uhome", player, "base").hasSucceeded()); assertEquals(5.0, new HomesDao().getAll(player.getUniqueId()).get(0).getX()); } @@ -81,7 +81,7 @@ void movingAcrossWorldsRewritesTheWorldAndDimension() { HomeFixtures.persist(player, "base"); player.teleport(new Location(nether, 8, 70, 8)); - server.execute("move-home", player, "base").assertSucceeded(); + assertTrue(server.execute("move-home", player, "base").hasSucceeded()); Home moved = new HomesDao().getAll(player.getUniqueId()).get(0); assertEquals(nether.getUID().toString(), moved.getWorld()); @@ -92,7 +92,7 @@ void movingAcrossWorldsRewritesTheWorldAndDimension() { void theWrongNumberOfArgumentsShowsTheUsage() { PlayerMock player = addPlayer(); - server.execute("move-home", player).assertSucceeded(); + assertTrue(server.execute("move-home", player).hasSucceeded()); assertTrue(player.nextMessage().contains("Incorrect number of arguments")); assertTrue(player.nextMessage().contains("Usage: /move-home ")); diff --git a/src/test/java/com/samleighton/sethomestwo/commands/OpenHomesGuiTest.java b/src/test/java/com/samleighton/sethomestwo/commands/OpenHomesGuiTest.java index 0e1564d..e4ab0f7 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/OpenHomesGuiTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/OpenHomesGuiTest.java @@ -14,7 +14,7 @@ class OpenHomesGuiTest extends ServerTestBase { @Test void consoleIsTurnedAway() { - server.executeConsole("homes").assertSucceeded(); + assertTrue(server.executeConsole("homes").hasSucceeded()); // The command reports back rather than doing anything. assertTrue(server.getConsoleSender().nextMessage().contains("Only players")); } @@ -23,7 +23,7 @@ void consoleIsTurnedAway() { void aPlayerWithNoHomesIsTold() { PlayerMock player = addPlayer(); - server.execute("homes", player).assertSucceeded(); + assertTrue(server.execute("homes", player).hasSucceeded()); assertTrue(player.nextMessage().contains("You have not created any homes yet.")); } @@ -33,7 +33,7 @@ void aPlayerWithHomesGetsASession() { PlayerMock player = addPlayer(); HomeFixtures.persist(player, "base"); - server.execute("homes", player).assertSucceeded(); + assertTrue(server.execute("homes", player).hasSucceeded()); GuiSession session = plugin.getGuiSessionMap().get(player.getUniqueId()); assertNotNull(session); @@ -49,7 +49,7 @@ void theSessionIsReusedAcrossInvocations() { // first call to populate it, so assertSame actually proves reuse. plugin.getGuiSessionMap().clear(); - server.execute("homes", player).assertSucceeded(); + assertTrue(server.execute("homes", player).hasSucceeded()); GuiSession first = plugin.getGuiSessionMap().get(player.getUniqueId()); assertNotNull(first); // activeScreen starts null and is only set by openHomeList, so this @@ -57,7 +57,7 @@ void theSessionIsReusedAcrossInvocations() { // some session object happens to exist. assertNotNull(first.getActiveScreen()); - server.execute("homes", player).assertSucceeded(); + assertTrue(server.execute("homes", player).hasSucceeded()); GuiSession second = plugin.getGuiSessionMap().get(player.getUniqueId()); assertSame(first, second); diff --git a/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java b/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java index aa00c43..683fb95 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java @@ -1,6 +1,7 @@ package com.samleighton.sethomestwo.commands; import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.metrics.UsageCounters; import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; import com.samleighton.sethomestwo.support.TestPlayer; @@ -35,7 +36,7 @@ void deletingAnOfflinePlayersHomeWorks() { PlayerMock admin = addPlayer("Admin"); admin.addAttachment(plugin, "sh2.delete-player-home", true); - server.execute("delete-player-home", admin, "Steve", "base").assertSucceeded(); + assertTrue(server.execute("delete-player-home", admin, "Steve", "base").hasSucceeded()); assertTrue(new HomesDao(true).getAll(targetId).isEmpty()); } @@ -51,7 +52,7 @@ void movingAnOfflinePlayersHomeUsesTheAdminLocation() { admin.addAttachment(plugin, "sh2.move-player-home", true); admin.teleport(new Location(overworld, 250, 70, 250)); - server.execute("move-player-home", admin, "Steve", "base").assertSucceeded(); + assertTrue(server.execute("move-player-home", admin, "Steve", "base").hasSucceeded()); assertEquals(250.0, new HomesDao(true).getAll(targetId).get(0).getX()); } @@ -70,7 +71,7 @@ void teleportingToAnOfflinePlayersHomeIsAccepted() { // TeleportSafetyUtil.prefetchChunks reaches an unimplemented MockBukkit call. plugin.getConfig().set("teleportSafety", false); - server.execute("go-player-home", admin, "Steve", "base").assertSucceeded(); + assertTrue(server.execute("go-player-home", admin, "Steve", "base").hasSucceeded()); server.getScheduler().performTicks(100L); assertEquals(30.0, admin.getLocation().getX()); @@ -81,7 +82,7 @@ void anUnknownPlayerIsReported() { PlayerMock admin = addPlayer("Admin"); admin.addAttachment(plugin, "sh2.delete-player-home", true); - server.execute("delete-player-home", admin, "Nobody", "base").assertSucceeded(); + assertTrue(server.execute("delete-player-home", admin, "Nobody", "base").hasSucceeded()); assertTrue(admin.nextMessage().contains("No player by that name")); } @@ -94,7 +95,7 @@ void eachCommandIsRefusedWithoutItsNodeAtTheCommandGate() { PlayerMock admin = addPlayer("Admin"); admin.addAttachment(plugin, "sh2.delete-player-home", false); - server.execute("delete-player-home", admin, "Steve", "base").assertSucceeded(); + assertTrue(server.execute("delete-player-home", admin, "Steve", "base").hasSucceeded()); assertTrue(admin.nextMessage().contains("permission")); assertEquals(1, new HomesDao(true).getAll(target.getUniqueId()).size()); @@ -128,7 +129,7 @@ void theV1AliasesWork() { PlayerMock admin = addPlayer("Admin"); admin.addAttachment(plugin, "sh2.delete-player-home", true); - server.execute("delhome-of", admin, "Steve", "base").assertSucceeded(); + assertTrue(server.execute("delhome-of", admin, "Steve", "base").hasSucceeded()); assertTrue(new HomesDao(true).getAll(target.getUniqueId()).isEmpty()); } @@ -138,7 +139,7 @@ void theWrongNumberOfArgumentsShowsTheUsage() { PlayerMock admin = addPlayer("Admin"); admin.addAttachment(plugin, "sh2.move-player-home", true); - server.execute("move-player-home", admin, "Steve").assertSucceeded(); + assertTrue(server.execute("move-player-home", admin, "Steve").hasSucceeded()); assertTrue(admin.nextMessage().contains("Incorrect number of arguments")); assertTrue(admin.nextMessage().contains("Usage: /move-player-home ")); @@ -214,13 +215,13 @@ void anUnknownHomeIsReportedByEachCommand() { admin.addAttachment(plugin, "sh2.move-player-home", true); admin.addAttachment(plugin, "sh2.go-player-home", true); - server.execute("delete-player-home", admin, "Steve", "nope").assertSucceeded(); + assertTrue(server.execute("delete-player-home", admin, "Steve", "nope").hasSucceeded()); assertUnknownHomeNamed(admin, "nope"); - server.execute("move-player-home", admin, "Steve", "nope").assertSucceeded(); + assertTrue(server.execute("move-player-home", admin, "Steve", "nope").hasSucceeded()); assertUnknownHomeNamed(admin, "nope"); - server.execute("go-player-home", admin, "Steve", "nope").assertSucceeded(); + assertTrue(server.execute("go-player-home", admin, "Steve", "nope").hasSucceeded()); assertUnknownHomeNamed(admin, "nope"); assertEquals(1, new HomesDao(true).getAll(target.getUniqueId()).size()); @@ -236,7 +237,7 @@ void theSuccessMessageNamesTheOwnerAndHomeCanonically() { // Both names typed in the wrong case. The reply must echo the stored // spelling, not what was typed. - server.execute("delete-player-home", admin, "sTeVe", "BaSe").assertSucceeded(); + assertTrue(server.execute("delete-player-home", admin, "sTeVe", "BaSe").hasSucceeded()); String reply = admin.nextMessage(); assertTrue(reply.contains("Steve"), reply); @@ -263,13 +264,31 @@ void withoutTheBypassNodeAnAdminCannotReachAnotherPlayersBlacklistedHome() { TestPlayer admin = adminAt(new Location(overworld, 0, 70, 0)); admin.addAttachment(plugin, "sh2.bypass-blacklist", false); - server.execute("go-player-home", admin, "Steve", "hideout").assertSucceeded(); + assertTrue(server.execute("go-player-home", admin, "Steve", "hideout").hasSucceeded()); server.getScheduler().performTicks(100L); assertEquals("world", admin.getLocation().getWorld().getName()); assertTrue(admin.nextMessage().contains("blacklisted")); } + @Test + void aBlacklistedTeleportCountsTheBlacklistedOutcome() { + TestPlayer owner = addPlayer("Steve"); + HomeFixtures.persist(HomeFixtures.home(owner, "hideout", new Location(nether, 33, 70, 33))); + HomeFixtures.blacklist("world_nether"); + owner.disconnect(); + + TestPlayer admin = adminAt(new Location(overworld, 0, 70, 0)); + admin.addAttachment(plugin, "sh2.bypass-blacklist", false); + + assertTrue(server.execute("go-player-home", admin, "Steve", "hideout").hasSucceeded()); + server.getScheduler().performTicks(100L); + + UsageCounters counters = plugin.getUsageCounters(); + assertEquals(1, counters.snapshot(UsageCounters.Family.TELEPORT_OUTCOME).get(UsageCounters.OUTCOME_BLACKLISTED)); + assertEquals(1, counters.snapshot(UsageCounters.Family.TELEPORT_SOURCE).get(UsageCounters.SOURCE_COMMAND)); + } + @Test void withTheBypassNodeAnAdminReachesAnotherPlayersBlacklistedHome() { TestPlayer owner = addPlayer("Steve"); @@ -280,7 +299,7 @@ void withTheBypassNodeAnAdminReachesAnotherPlayersBlacklistedHome() { TestPlayer admin = adminAt(new Location(overworld, 0, 70, 0)); admin.addAttachment(plugin, "sh2.bypass-blacklist", true); - server.execute("go-player-home", admin, "Steve", "hideout").assertSucceeded(); + assertTrue(server.execute("go-player-home", admin, "Steve", "hideout").hasSucceeded()); server.getScheduler().performTicks(100L); assertEquals("world_nether", admin.getLocation().getWorld().getName()); @@ -297,7 +316,7 @@ void withoutTheBypassNodeClickingABlacklistedHomeInTheAdminViewDoesNotTeleport() admin.addAttachment(plugin, "sh2.get-player-homes", true); admin.addAttachment(plugin, "sh2.bypass-blacklist", false); - server.execute("get-player-homes", admin, "Steve").assertSucceeded(); + assertTrue(server.execute("get-player-homes", admin, "Steve").hasSucceeded()); clickFirstHome(admin); server.getScheduler().performTicks(100L); @@ -314,7 +333,7 @@ void withTheBypassNodeClickingABlacklistedHomeInTheAdminViewTeleports() { admin.addAttachment(plugin, "sh2.get-player-homes", true); admin.addAttachment(plugin, "sh2.bypass-blacklist", true); - server.execute("get-player-homes", admin, "Steve").assertSucceeded(); + assertTrue(server.execute("get-player-homes", admin, "Steve").hasSucceeded()); clickFirstHome(admin); server.getScheduler().performTicks(100L); diff --git a/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoTest.java b/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoTest.java index e29f205..7592aec 100644 --- a/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoTest.java +++ b/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoTest.java @@ -199,6 +199,23 @@ void blacklistedDimensionStillAllowsTheAdminView() { assertTrue(home.getCanTeleport()); } + @Test + void countsCoverEveryPlayer() { + PlayerMock steve = addPlayer("Steve"); + PlayerMock alex = addPlayer("Alex"); + HomesDao dao = new HomesDao(); + + assertEquals(0, dao.countAll()); + assertEquals(0, dao.countPlayersWithHomes()); + + HomeFixtures.persist(steve, "a"); + HomeFixtures.persist(steve, "b"); + HomeFixtures.persist(alex, "c"); + + assertEquals(3, dao.countAll()); + assertEquals(2, dao.countPlayersWithHomes()); + } + /** * Captures what gets logged during {@code action}. The handler is always * removed afterward so it cannot leak into other tests. diff --git a/src/test/java/com/samleighton/sethomestwo/events/PlayerJoinTest.java b/src/test/java/com/samleighton/sethomestwo/events/PlayerJoinTest.java index 3d83d8e..64f703f 100644 --- a/src/test/java/com/samleighton/sethomestwo/events/PlayerJoinTest.java +++ b/src/test/java/com/samleighton/sethomestwo/events/PlayerJoinTest.java @@ -5,6 +5,7 @@ import com.samleighton.sethomestwo.support.ServerTestBase; import com.samleighton.sethomestwo.support.TestPlayer; import com.samleighton.sethomestwo.updates.UpdateChecker; +import net.kyori.adventure.text.Component; import org.bukkit.event.player.PlayerJoinEvent; import org.junit.jupiter.api.Test; @@ -22,7 +23,7 @@ void joiningPlayerWhoMaySeeNoticesIsToldAboutAnAvailableUpdate() { TestPlayer player = addPlayer(); player.addAttachment(plugin, UpdateChecker.NOTIFY_PERMISSION, true); - server.getPluginManager().callEvent(new PlayerJoinEvent(player, "")); + server.getPluginManager().callEvent(new PlayerJoinEvent(player, Component.empty())); String message = player.nextMessage(); assertNotNull(message, "expected the join listener to deliver the update notice"); @@ -40,7 +41,7 @@ void joiningRefreshesTheStoredNameOnExistingHomes() { // account had been seen under a different name previously. new HomesDao().refreshPlayerName(player.getUniqueId(), "OldSteve"); - server.getPluginManager().callEvent(new PlayerJoinEvent(player, "")); + server.getPluginManager().callEvent(new PlayerJoinEvent(player, Component.empty())); assertEquals(player.getUniqueId().toString(), new HomesDao().uuidForName("Steve")); } diff --git a/src/test/java/com/samleighton/sethomestwo/events/RightClickHomeItemTest.java b/src/test/java/com/samleighton/sethomestwo/events/RightClickHomeItemTest.java index 8b40357..a6ce3e7 100644 --- a/src/test/java/com/samleighton/sethomestwo/events/RightClickHomeItemTest.java +++ b/src/test/java/com/samleighton/sethomestwo/events/RightClickHomeItemTest.java @@ -8,6 +8,9 @@ import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; import com.samleighton.sethomestwo.support.TestPlayer; +import java.util.HashMap; +import java.util.Map; +import net.kyori.adventure.text.Component; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.event.block.Action; @@ -21,9 +24,6 @@ import org.bukkit.inventory.meta.ItemMeta; import org.junit.jupiter.api.Test; -import java.util.HashMap; -import java.util.Map; - import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -99,7 +99,7 @@ void anUntaggedCompassIsIgnored() { // Proves the click was dropped silently by the tag-presence guard, // not merely stopped by a different, noisier guard further down // (e.g. the ownership check, which also leaves activeScreen null). - player.assertNoMoreSaid(); + assertNull(player.nextMessage()); } @Test @@ -113,7 +113,7 @@ void aDifferentMaterialIsIgnored() { interact(player, Action.RIGHT_CLICK_AIR, taggedItem(Material.DIAMOND, player)); assertNull(sessionFor(player).getActiveScreen()); - player.assertNoMoreSaid(); + assertNull(player.nextMessage()); } @Test @@ -171,7 +171,7 @@ void anInventoryClickFromAPlayerWithNoSessionIsIgnored() { TestPlayer player = addTestPlayer("owner"); plugin.getGuiSessionMap().clear(); - player.openInventory(org.bukkit.Bukkit.createInventory(player, 9, "unrelated")); + player.openInventory(org.bukkit.Bukkit.createInventory(player, 9, Component.text("unrelated"))); InventoryClickEvent event = new InventoryClickEvent( player.getOpenInventory(), InventoryType.SlotType.CONTAINER, @@ -204,7 +204,7 @@ void anInventoryDragFromAPlayerWithNoSessionIsIgnored() { TestPlayer player = addTestPlayer("owner"); plugin.getGuiSessionMap().clear(); - player.openInventory(org.bukkit.Bukkit.createInventory(player, 9, "unrelated")); + player.openInventory(org.bukkit.Bukkit.createInventory(player, 9, Component.text("unrelated"))); InventoryDragEvent event = dragOn(player); server.getPluginManager().callEvent(event); diff --git a/src/test/java/com/samleighton/sethomestwo/gui/GuiSessionTest.java b/src/test/java/com/samleighton/sethomestwo/gui/GuiSessionTest.java index f2fc7a2..9fa84aa 100644 --- a/src/test/java/com/samleighton/sethomestwo/gui/GuiSessionTest.java +++ b/src/test/java/com/samleighton/sethomestwo/gui/GuiSessionTest.java @@ -3,6 +3,9 @@ import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; +import java.util.HashMap; +import java.util.Map; +import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.event.inventory.ClickType; @@ -16,9 +19,6 @@ import org.junit.jupiter.api.Test; import org.mockbukkit.mockbukkit.entity.PlayerMock; -import java.util.HashMap; -import java.util.Map; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -68,7 +68,7 @@ void clickIsIgnoredWhenNoScreenIsActive() { PlayerMock player = addPlayer(); GuiSession session = new GuiSession(new HomesGui(player)); - InventoryClickEvent event = clickOn(player, Bukkit.createInventory(player, 9, "other"), 0); + InventoryClickEvent event = clickOn(player, Bukkit.createInventory(player, 9, Component.text("other")), 0); session.handleClick(event); assertFalse(event.isCancelled()); @@ -79,10 +79,10 @@ void clickInAForeignInventoryIsNeitherCancelledNorRouted() { PlayerMock player = addPlayer(); GuiSession session = new GuiSession(new HomesGui(player)); - RecordingScreen screen = new RecordingScreen(Bukkit.createInventory(player, 9, "active")); + RecordingScreen screen = new RecordingScreen(Bukkit.createInventory(player, 9, Component.text("active"))); session.setActiveScreen(screen); - InventoryClickEvent event = clickOn(player, Bukkit.createInventory(player, 9, "foreign"), 0); + InventoryClickEvent event = clickOn(player, Bukkit.createInventory(player, 9, Component.text("foreign")), 0); session.handleClick(event); assertFalse(event.isCancelled()); @@ -94,7 +94,7 @@ void clickInTheActiveScreenIsCancelledAndRouted() { PlayerMock player = addPlayer(); GuiSession session = new GuiSession(new HomesGui(player)); - Inventory inventory = Bukkit.createInventory(player, 9, "active"); + Inventory inventory = Bukkit.createInventory(player, 9, Component.text("active")); RecordingScreen screen = new RecordingScreen(inventory); session.setActiveScreen(screen); @@ -110,7 +110,7 @@ void dragIsIgnoredWhenNoScreenIsActive() { PlayerMock player = addPlayer(); GuiSession session = new GuiSession(new HomesGui(player)); - InventoryDragEvent event = dragOn(player, Bukkit.createInventory(player, 9, "other")); + InventoryDragEvent event = dragOn(player, Bukkit.createInventory(player, 9, Component.text("other"))); session.handleDrag(event); assertFalse(event.isCancelled()); @@ -120,9 +120,9 @@ void dragIsIgnoredWhenNoScreenIsActive() { void dragInAForeignInventoryIsNotCancelled() { PlayerMock player = addPlayer(); GuiSession session = new GuiSession(new HomesGui(player)); - session.setActiveScreen(new RecordingScreen(Bukkit.createInventory(player, 9, "active"))); + session.setActiveScreen(new RecordingScreen(Bukkit.createInventory(player, 9, Component.text("active")))); - InventoryDragEvent event = dragOn(player, Bukkit.createInventory(player, 9, "foreign")); + InventoryDragEvent event = dragOn(player, Bukkit.createInventory(player, 9, Component.text("foreign"))); session.handleDrag(event); assertFalse(event.isCancelled()); @@ -131,7 +131,7 @@ void dragInAForeignInventoryIsNotCancelled() { @Test void dragInTheActiveScreenIsCancelled() { PlayerMock player = addPlayer(); - Inventory inventory = Bukkit.createInventory(player, 9, "active"); + Inventory inventory = Bukkit.createInventory(player, 9, Component.text("active")); GuiSession session = new GuiSession(new HomesGui(player)); session.setActiveScreen(new RecordingScreen(inventory)); diff --git a/src/test/java/com/samleighton/sethomestwo/gui/HomeActionsGuiTest.java b/src/test/java/com/samleighton/sethomestwo/gui/HomeActionsGuiTest.java index 11b5a8b..d67fdab 100644 --- a/src/test/java/com/samleighton/sethomestwo/gui/HomeActionsGuiTest.java +++ b/src/test/java/com/samleighton/sethomestwo/gui/HomeActionsGuiTest.java @@ -3,6 +3,8 @@ import com.samleighton.sethomestwo.SetHomesTwo; import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.datatypes.PersistentHome; +import com.samleighton.sethomestwo.datatypes.PersistentString; +import com.samleighton.sethomestwo.metrics.UsageCounters; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; @@ -14,6 +16,7 @@ import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import org.junit.jupiter.api.Test; import org.mockbukkit.mockbukkit.entity.PlayerMock; @@ -247,6 +250,72 @@ void setIconTakesTheHeldItem() { assertEquals(Material.DIAMOND.name(), new HomesDao().getById(player.getUniqueId(), home.getId()).getMaterial()); } + @Test + void everyManagementActionIsCounted() { + PlayerMock player = addPlayer(); + Home home = HomeFixtures.persist(player, "base"); + GuiSession session = new GuiSession(new HomesGui(player)); + + HomeActionsGui gui = openSubmenu(player, home, session); + click(gui, session, player, SLOT_DELETE); + click(gui, session, player, SLOT_CANCEL); + click(gui, session, player, SLOT_DELETE); + click(gui, session, player, SLOT_CONFIRM); + + var actions = plugin.getUsageCounters().snapshot(UsageCounters.Family.GUI_ACTION); + assertEquals(2, actions.get(HomeActionsGui.ACTION_DELETE)); + assertEquals(1, actions.get(HomeActionsGui.ACTION_CANCEL_DELETE)); + assertEquals(1, actions.get(HomeActionsGui.ACTION_CONFIRM_DELETE)); + } + + @Test + void backAndMoveAreCounted() { + PlayerMock player = addPlayer(); + Home home = HomeFixtures.persist(player, "base"); + GuiSession session = new GuiSession(new HomesGui(player)); + + HomeActionsGui gui = openSubmenu(player, home, session); + click(gui, session, player, SLOT_MOVE); + gui = openSubmenu(player, home, session); + click(gui, session, player, SLOT_BACK); + + var actions = plugin.getUsageCounters().snapshot(UsageCounters.Family.GUI_ACTION); + assertEquals(1, actions.get(HomeActionsGui.ACTION_MOVE)); + assertEquals(1, actions.get(HomeActionsGui.ACTION_BACK)); + } + + @Test + void anEmptySlotClickCountsNothing() { + PlayerMock player = addPlayer(); + Home home = HomeFixtures.persist(player, "base"); + GuiSession session = new GuiSession(new HomesGui(player)); + + HomeActionsGui gui = openSubmenu(player, home, session); + click(gui, session, player, 3); + + assertTrue(plugin.getUsageCounters().snapshot(UsageCounters.Family.GUI_ACTION).isEmpty()); + } + + @Test + void anUnknownActionTagCountsNothing() { + PlayerMock player = addPlayer(); + Home home = HomeFixtures.persist(player, "base"); + GuiSession session = new GuiSession(new HomesGui(player)); + + HomeActionsGui gui = openSubmenu(player, home, session); + + ItemStack item = new ItemStack(Material.PAPER); + ItemMeta meta = item.getItemMeta(); + assertNotNull(meta); + meta.getPersistentDataContainer().set(new NamespacedKey(SetHomesTwo.instance(), HomeActionsGui.ACTION_KEY_NAME), new PersistentString(), "bogus"); + item.setItemMeta(meta); + gui.getInventory().setItem(3, item); + + click(gui, session, player, 3); + + assertTrue(plugin.getUsageCounters().snapshot(UsageCounters.Family.GUI_ACTION).isEmpty()); + } + @Test void applyRenameRejectsABlankName() { PlayerMock player = addPlayer(); diff --git a/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiClickTest.java b/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiClickTest.java index bc5c0f3..d78fbe8 100644 --- a/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiClickTest.java +++ b/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiClickTest.java @@ -1,9 +1,11 @@ package com.samleighton.sethomestwo.gui; import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.metrics.UsageCounters; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; +import com.samleighton.sethomestwo.support.TestPlayer; import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryAction; import org.bukkit.event.inventory.InventoryClickEvent; @@ -11,6 +13,7 @@ import org.junit.jupiter.api.Test; import org.mockbukkit.mockbukkit.entity.PlayerMock; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; @@ -110,6 +113,31 @@ void rightClickOnTheAdminListDoesNotOpenTheSubmenu() { assertFalse(session.getActiveScreen() instanceof HomeActionsGui); } + @Test + void leftClickCountsAGuiTeleport() { + TestPlayer player = addTestPlayer("traveller"); + HomeFixtures.persist(player, "base"); + plugin.getConfig().set("teleportSafety", false); + + HomesGui gui = openOwnList(player); + click(gui, new GuiSession(gui), player, 0, ClickType.LEFT); + + UsageCounters counters = plugin.getUsageCounters(); + assertEquals(1, counters.snapshot(UsageCounters.Family.GUI_ACTION).get(UsageCounters.GUI_TELEPORT)); + assertEquals(1, counters.snapshot(UsageCounters.Family.TELEPORT_SOURCE).get(UsageCounters.SOURCE_GUI)); + } + + @Test + void rightClickIntoManagementCountsNoTeleport() { + PlayerMock player = addPlayer(); + HomeFixtures.persist(player, "base"); + + HomesGui gui = openOwnList(player); + click(gui, new GuiSession(gui), player, 0, ClickType.RIGHT); + + assertTrue(plugin.getUsageCounters().snapshot(UsageCounters.Family.GUI_ACTION).isEmpty()); + } + @Test void anEmptyHomeListClosesTheMenuAndExplainsWhy() { PlayerMock player = addPlayer(); diff --git a/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiPaginationTest.java b/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiPaginationTest.java index ea636a7..97605a6 100644 --- a/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiPaginationTest.java +++ b/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiPaginationTest.java @@ -2,9 +2,16 @@ import com.samleighton.sethomestwo.SetHomesTwo; import com.samleighton.sethomestwo.datatypes.PersistentHome; +import com.samleighton.sethomestwo.metrics.UsageCounters; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.event.inventory.ClickType; @@ -16,11 +23,6 @@ import org.junit.jupiter.api.Test; import org.mockbukkit.mockbukkit.entity.PlayerMock; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -64,7 +66,7 @@ private Set homeNames(Inventory inventory) { for (ItemStack item : inventory.getContents()) { if (item == null || item.getItemMeta() == null) continue; if (!item.getItemMeta().getPersistentDataContainer().has(key, new PersistentHome())) continue; - names.add(item.getItemMeta().getDisplayName()); + names.add(PlainTextComponentSerializer.plainText().serialize(Objects.requireNonNull(item.getItemMeta().displayName()))); } return names; @@ -92,6 +94,22 @@ private void clickSlot(HomesGui gui, GuiSession session, PlayerMock player, int gui.onClick(event, session); } + @Test + void pageTurnsAreCounted() { + PlayerMock player = addPlayer(); + HomesGui gui = new HomesGui(player); + GuiSession session = new GuiSession(gui); + gui.setHomes(homes(player, 46)); + gui.displayInventory(player); + + clickSlot(gui, session, player, NEXT_PAGE_SLOT); + clickSlot(gui, session, player, PREV_PAGE_SLOT); + + var actions = plugin.getUsageCounters().snapshot(UsageCounters.Family.GUI_ACTION); + assertEquals(1, actions.get(UsageCounters.GUI_PAGE_NEXT)); + assertEquals(1, actions.get(UsageCounters.GUI_PAGE_PREVIOUS)); + } + @Test void fortyFiveHomesFitOnASinglePage() { PlayerMock player = addPlayer(); diff --git a/src/test/java/com/samleighton/sethomestwo/metrics/BucketsTest.java b/src/test/java/com/samleighton/sethomestwo/metrics/BucketsTest.java new file mode 100644 index 0000000..e48fdc9 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/metrics/BucketsTest.java @@ -0,0 +1,43 @@ +package com.samleighton.sethomestwo.metrics; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class BucketsTest { + + @Test + void delayBuckets() { + assertEquals("0", Buckets.delay(0)); + assertEquals("0", Buckets.delay(-1)); + assertEquals("1-3", Buckets.delay(1)); + assertEquals("1-3", Buckets.delay(3)); + assertEquals("4-10", Buckets.delay(4)); + assertEquals("4-10", Buckets.delay(10)); + assertEquals("10+", Buckets.delay(11)); + } + + @Test + void homesPerServerBuckets() { + assertEquals("0", Buckets.homesPerServer(0)); + assertEquals("1-50", Buckets.homesPerServer(1)); + assertEquals("1-50", Buckets.homesPerServer(50)); + assertEquals("51-500", Buckets.homesPerServer(51)); + assertEquals("51-500", Buckets.homesPerServer(500)); + assertEquals("501-5000", Buckets.homesPerServer(501)); + assertEquals("501-5000", Buckets.homesPerServer(5000)); + assertEquals("5000+", Buckets.homesPerServer(5001)); + } + + @Test + void homesPerPlayerBuckets() { + assertEquals("0", Buckets.homesPerPlayer(0, 0)); + assertEquals("1", Buckets.homesPerPlayer(3, 3)); + assertEquals("1", Buckets.homesPerPlayer(5, 3)); + assertEquals("2-3", Buckets.homesPerPlayer(6, 3)); + assertEquals("2-3", Buckets.homesPerPlayer(11, 3)); + assertEquals("4-10", Buckets.homesPerPlayer(12, 3)); + assertEquals("4-10", Buckets.homesPerPlayer(30, 3)); + assertEquals("10+", Buckets.homesPerPlayer(33, 3)); + } +} diff --git a/src/test/java/com/samleighton/sethomestwo/metrics/CommandUsageListenerTest.java b/src/test/java/com/samleighton/sethomestwo/metrics/CommandUsageListenerTest.java new file mode 100644 index 0000000..4ffbec2 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/metrics/CommandUsageListenerTest.java @@ -0,0 +1,96 @@ +package com.samleighton.sethomestwo.metrics; + +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.server.ServerCommandEvent; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +import java.util.Map; + +import static com.samleighton.sethomestwo.metrics.UsageCounters.Family.ALIAS; +import static com.samleighton.sethomestwo.metrics.UsageCounters.Family.COMMAND; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CommandUsageListenerTest extends ServerTestBase { + + private Map commands() { + return plugin.getUsageCounters().snapshot(COMMAND); + } + + private Map aliases() { + return plugin.getUsageCounters().snapshot(ALIAS); + } + + private void playerTypes(String line) { + PlayerMock player = addPlayer(); + server.getPluginManager().callEvent(new PlayerCommandPreprocessEvent(player, line)); + } + + @Test + void aCanonicalCommandCountsUnderItsOwnName() { + playerTypes("/go-home base"); + + assertEquals(1, commands().get("go-home")); + assertEquals(1, aliases().get("go-home")); + } + + @Test + void aDispatchedCommandIsCounted() { + PlayerMock player = addPlayer(); + player.performCommand("go-home base"); + + assertEquals(1, commands().get("go-home")); + } + + @Test + void anAliasCountsUnderTheCanonicalNameAndTheTypedAlias() { + playerTypes("/sethome base"); + + assertEquals(1, commands().get("create-home")); + assertEquals(1, aliases().get("sethome")); + assertTrue(!aliases().containsKey("create-home")); + } + + @Test + void theNamespacedFormCountsOnceWithoutTheNamespace() { + playerTypes("/sethomestwo:home base"); + + assertEquals(1, commands().get("go-home")); + assertEquals(1, aliases().get("home")); + assertEquals(1, commands().size()); + } + + @Test + void caseAndSurroundingSpacesDoNotMatter() { + playerTypes(" /Go-Home base "); + + assertEquals(1, commands().get("go-home")); + } + + @Test + void aForeignCommandCountsNothing() { + playerTypes("/say hello"); + playerTypes("/notacommandatall"); + playerTypes("/"); + + assertTrue(commands().isEmpty()); + assertTrue(aliases().isEmpty()); + } + + @Test + void consoleCommandsCountToo() { + server.getPluginManager().callEvent(new ServerCommandEvent(server.getConsoleSender(), "get-player-homes Steve")); + + assertEquals(1, commands().get("get-player-homes")); + } + + @Test + void theListenerNeverThrowsOnAnOddLine() { + server.getPluginManager().callEvent(new ServerCommandEvent(server.getConsoleSender(), "")); + server.getPluginManager().callEvent(new ServerCommandEvent(server.getConsoleSender(), " ")); + + assertTrue(commands().isEmpty()); + } +} diff --git a/src/test/java/com/samleighton/sethomestwo/metrics/ErrorsTest.java b/src/test/java/com/samleighton/sethomestwo/metrics/ErrorsTest.java new file mode 100644 index 0000000..688cbf4 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/metrics/ErrorsTest.java @@ -0,0 +1,52 @@ +package com.samleighton.sethomestwo.metrics; + +import com.samleighton.sethomestwo.support.ServerTestBase; +import com.samleighton.sethomestwo.utils.DatabaseUtil; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ErrorsTest extends ServerTestBase { + + private Map errors() { + return plugin.getUsageCounters().snapshot(UsageCounters.Family.ERROR); + } + + private Connection homes() { + return plugin.getConnectionManager().getConnection("homes"); + } + + @Test + void aFailedWriteCountsAsSqlWrite() { + assertFalse(DatabaseUtil.execute(homes(), "insert into no_such_table values (1)")); + assertEquals(-1, DatabaseUtil.executeUpdate(homes(), "update no_such_table set x = 1")); + + assertEquals(2, errors().get(Errors.SQL_WRITE)); + assertFalse(errors().containsKey(Errors.SQL_READ)); + } + + @Test + void aFailedReadCountsAsSqlRead() { + assertNull(DatabaseUtil.fetch(homes(), "select * from no_such_table")); + + assertEquals(1, errors().get(Errors.SQL_READ)); + } + + @Test + void successfulStatementsCountNothing() { + assertTrue(DatabaseUtil.execute(homes(), "select 1")); + assertTrue(errors().isEmpty()); + } + + @Test + void theStaticHookLandsInTheErrorFamily() { + Errors.count(Errors.DB_CONNECT); + assertEquals(1, errors().get(Errors.DB_CONNECT)); + } +} diff --git a/src/test/java/com/samleighton/sethomestwo/metrics/MetricsReporterTest.java b/src/test/java/com/samleighton/sethomestwo/metrics/MetricsReporterTest.java new file mode 100644 index 0000000..9c48095 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/metrics/MetricsReporterTest.java @@ -0,0 +1,147 @@ +package com.samleighton.sethomestwo.metrics; + +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MetricsReporterTest extends ServerTestBase { + + @Test + void aZeroPluginIdNeverStartsTheReporter() { + AtomicInteger built = new AtomicInteger(); + MetricsReporter reporter = new MetricsReporter(plugin, 0, () -> true, counters -> { + built.incrementAndGet(); + return () -> {}; + }); + + reporter.startLater(); + server.getScheduler().performTicks(MetricsReporter.STARTUP_DELAY_TICKS + 5); + + assertEquals(0, built.get()); + assertFalse(reporter.isRunning()); + } + + @Test + void disabledInConfigNeverBuildsTheReporter() { + AtomicInteger built = new AtomicInteger(); + MetricsReporter reporter = new MetricsReporter(plugin, 1, () -> false, counters -> { + built.incrementAndGet(); + return () -> {}; + }); + + reporter.startLater(); + server.getScheduler().performTicks(MetricsReporter.STARTUP_DELAY_TICKS + 5); + + assertEquals(0, built.get()); + assertFalse(reporter.isRunning()); + } + + @Test + void enabledInConfigBuildsTheReporterOnceAfterTheDelay() { + AtomicInteger built = new AtomicInteger(); + MetricsReporter reporter = new MetricsReporter(plugin, 1, () -> true, counters -> { + built.incrementAndGet(); + return () -> {}; + }); + + reporter.startLater(); + server.getScheduler().performTicks(MetricsReporter.STARTUP_DELAY_TICKS - 1); + assertFalse(reporter.isRunning()); + + server.getScheduler().performTicks(5); + assertEquals(1, built.get()); + assertTrue(reporter.isRunning()); + } + + @Test + void theFlagIsReadWhenTheTaskFiresNotWhenScheduled() { + AtomicBoolean enabled = new AtomicBoolean(true); + MetricsReporter reporter = new MetricsReporter(plugin, 1, enabled::get, counters -> () -> {}); + + reporter.startLater(); + enabled.set(false); + server.getScheduler().performTicks(MetricsReporter.STARTUP_DELAY_TICKS + 5); + + assertFalse(reporter.isRunning()); + } + + @Test + void shutdownClosesWhatWasBuilt() { + AtomicBoolean closed = new AtomicBoolean(); + MetricsReporter reporter = new MetricsReporter(plugin, 1, () -> true, counters -> () -> closed.set(true)); + + reporter.startLater(); + server.getScheduler().performTicks(MetricsReporter.STARTUP_DELAY_TICKS + 5); + reporter.shutdown(); + + assertTrue(closed.get()); + assertFalse(reporter.isRunning()); + } + + @Test + void aFactoryThatThrowsLeavesTheServerAliveAndTheReporterOff() { + MetricsReporter reporter = new MetricsReporter(plugin, 1, () -> true, counters -> { + throw new IllegalStateException("relocation check"); + }); + + reporter.startLater(); + server.getScheduler().performTicks(MetricsReporter.STARTUP_DELAY_TICKS + 5); + + assertFalse(reporter.isRunning()); + assertTrue(plugin.isEnabled()); + } + + @Test + void thePluginsOwnReporterIsOffUnderTest() { + server.getScheduler().performTicks(MetricsReporter.STARTUP_DELAY_TICKS + 5); + assertFalse(plugin.getMetricsReporter().isRunning()); + } + + @Test + void theGlobalBStatsSwitchIsReadFromItsOwnConfig(@TempDir File pluginsDir) throws IOException { + assertTrue(MetricsReporter.bStatsEnabledGlobally(pluginsDir), "no bStats config yet means enabled"); + + File bStatsDir = new File(pluginsDir, "bStats"); + assertTrue(bStatsDir.mkdirs()); + File config = new File(bStatsDir, "config.yml"); + + Files.writeString(config.toPath(), "enabled: false\n"); + assertFalse(MetricsReporter.bStatsEnabledGlobally(pluginsDir)); + + Files.writeString(config.toPath(), "enabled: true\nserverUuid: abc\n"); + assertTrue(MetricsReporter.bStatsEnabledGlobally(pluginsDir)); + } + + @Test + void theTestHarnessDisablesBStatsThroughItsGlobalSwitch() { + assertFalse(MetricsReporter.bStatsEnabledGlobally(plugin.getDataFolder().getParentFile())); + } + + @Test + void commandChartIdsAreStableAndUnderscored() { + assertEquals("command_go_home", MetricsReporter.commandChartId("go-home")); + assertEquals("command_homes", MetricsReporter.commandChartId("homes")); + assertEquals("command_get_player_homes", MetricsReporter.commandChartId("Get-Player-Homes")); + } + + @Test + @SuppressWarnings("deprecation") // Paper deprecates getDescription, but its replacement carries no command list and the reporter itself must stay on the Spigot API. + void everyDeclaredCommandGetsAChartId() { + for (String command : plugin.getDescription().getCommands().keySet()) { + String id = MetricsReporter.commandChartId(command); + assertTrue(id.matches("command_[a-z_]+"), id); + } + assertEquals(14, plugin.getDescription().getCommands().size()); + } +} diff --git a/src/test/java/com/samleighton/sethomestwo/metrics/UsageCountersTest.java b/src/test/java/com/samleighton/sethomestwo/metrics/UsageCountersTest.java new file mode 100644 index 0000000..aa98a4b --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/metrics/UsageCountersTest.java @@ -0,0 +1,55 @@ +package com.samleighton.sethomestwo.metrics; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static com.samleighton.sethomestwo.metrics.UsageCounters.Family.ALIAS; +import static com.samleighton.sethomestwo.metrics.UsageCounters.Family.COMMAND; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class UsageCountersTest { + + @Test + void incrementsAccumulatePerKey() { + UsageCounters counters = new UsageCounters(); + + counters.increment(COMMAND, "go-home"); + counters.increment(COMMAND, "go-home"); + counters.increment(COMMAND, "create-home"); + + Map snapshot = counters.snapshot(COMMAND); + assertEquals(2, snapshot.get("go-home")); + assertEquals(1, snapshot.get("create-home")); + assertEquals(2, snapshot.size()); + } + + @Test + void snapshotAndResetReturnsTheCountsAndClearsThem() { + UsageCounters counters = new UsageCounters(); + counters.increment(COMMAND, "go-home"); + + Map first = counters.snapshotAndReset(COMMAND); + assertEquals(1, first.get("go-home")); + + assertTrue(counters.snapshotAndReset(COMMAND).isEmpty()); + } + + @Test + void familiesAreIndependent() { + UsageCounters counters = new UsageCounters(); + counters.increment(COMMAND, "go-home"); + counters.increment(ALIAS, "home"); + + counters.snapshotAndReset(COMMAND); + + assertEquals(1, counters.snapshot(ALIAS).get("home")); + assertTrue(counters.snapshot(COMMAND).isEmpty()); + } + + @Test + void anEmptyFamilySnapshotsToAnEmptyMap() { + assertTrue(new UsageCounters().snapshot(COMMAND).isEmpty()); + } +} diff --git a/src/test/java/com/samleighton/sethomestwo/metrics/WindowShareTest.java b/src/test/java/com/samleighton/sethomestwo/metrics/WindowShareTest.java new file mode 100644 index 0000000..9150363 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/metrics/WindowShareTest.java @@ -0,0 +1,74 @@ +package com.samleighton.sethomestwo.metrics; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static com.samleighton.sethomestwo.metrics.UsageCounters.Family.COMMAND; +import static com.samleighton.sethomestwo.metrics.UsageCounters.Family.GUI_ACTION; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class WindowShareTest { + + @Test + void barsDrainTheFamilyAndParkTheWindowForTheLineReaders() { + UsageCounters counters = new UsageCounters(); + counters.increment(COMMAND, "go-home"); + counters.increment(COMMAND, "go-home"); + counters.increment(COMMAND, "create-home"); + WindowShare share = new WindowShare(counters); + + Map bars = share.bars(COMMAND); + assertArrayEquals(new int[]{2}, bars.get("go-home")); + assertArrayEquals(new int[]{1}, bars.get("create-home")); + assertTrue(counters.snapshot(COMMAND).isEmpty(), "bars must reset the window"); + + assertEquals(2, share.count(COMMAND, "go-home")); + assertEquals(1, share.count(COMMAND, "create-home")); + assertEquals(0, share.count(COMMAND, "delete-home")); + assertEquals(3, share.total(COMMAND)); + + // Any number of readers may look at the same parked window. + assertEquals(2, share.count(COMMAND, "go-home")); + assertEquals(3, share.total(COMMAND)); + } + + @Test + void theNextBarsCallReplacesTheParkedWindow() { + UsageCounters counters = new UsageCounters(); + counters.increment(COMMAND, "go-home"); + WindowShare share = new WindowShare(counters); + share.bars(COMMAND); + + counters.increment(COMMAND, "create-home"); + share.bars(COMMAND); + + assertEquals(0, share.count(COMMAND, "go-home")); + assertEquals(1, share.count(COMMAND, "create-home")); + assertEquals(1, share.total(COMMAND)); + } + + @Test + void aReaderWithoutAPrecedingDrainDrainsTheLiveWindowItself() { + UsageCounters counters = new UsageCounters(); + counters.increment(GUI_ACTION, "back"); + WindowShare share = new WindowShare(counters); + + assertEquals(1, share.total(GUI_ACTION)); + assertTrue(counters.snapshot(GUI_ACTION).isEmpty()); + assertEquals(1, share.count(GUI_ACTION, "back"), "the drained window stays parked"); + } + + @Test + void familiesDoNotLeakIntoEachOther() { + UsageCounters counters = new UsageCounters(); + counters.increment(COMMAND, "go-home"); + WindowShare share = new WindowShare(counters); + + share.bars(COMMAND); + assertEquals(0, share.total(GUI_ACTION)); + assertEquals(1, share.total(COMMAND)); + } +} diff --git a/src/test/java/com/samleighton/sethomestwo/support/ServerTestBase.java b/src/test/java/com/samleighton/sethomestwo/support/ServerTestBase.java index c448d25..6c5e558 100644 --- a/src/test/java/com/samleighton/sethomestwo/support/ServerTestBase.java +++ b/src/test/java/com/samleighton/sethomestwo/support/ServerTestBase.java @@ -9,6 +9,9 @@ import org.mockbukkit.mockbukkit.ServerMock; import org.mockbukkit.mockbukkit.world.WorldMock; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.util.UUID; /** @@ -46,6 +49,21 @@ protected void startServer() { // Turned off before anything drains the scheduler, so the check // scheduled during onEnable can never reach the GitHub API. plugin.getConfig().set("checkForUpdates", false); + + // Same reason: the reporter reads bStats' global switch when its delayed + // task fires, so no test can ever construct bStats or make a network request. + disableBStatsGlobally(); + } + + private void disableBStatsGlobally() { + File bStatsDir = new File(plugin.getDataFolder().getParentFile(), "bStats"); + if (!bStatsDir.isDirectory() && !bStatsDir.mkdirs()) + throw new IllegalStateException("could not create " + bStatsDir); + try { + Files.writeString(new File(bStatsDir, "config.yml").toPath(), "enabled: false\n"); + } catch (IOException e) { + throw new IllegalStateException("could not write the bStats opt-out", e); + } } @AfterEach diff --git a/src/test/java/com/samleighton/sethomestwo/utils/BlacklistEnforcementTest.java b/src/test/java/com/samleighton/sethomestwo/utils/BlacklistEnforcementTest.java index d903fec..db65e45 100644 --- a/src/test/java/com/samleighton/sethomestwo/utils/BlacklistEnforcementTest.java +++ b/src/test/java/com/samleighton/sethomestwo/utils/BlacklistEnforcementTest.java @@ -52,7 +52,7 @@ void creatingAHomeInABlacklistedFourthWorldIsRefused() { PlayerMock player = addPlayer(); player.teleport(new Location(creative, 0, 64, 0)); - server.execute("create-home", player, "base").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base").hasSucceeded()); assertTrue(player.nextMessage().contains("blacklisted")); assertTrue(new com.samleighton.sethomestwo.dao.HomesDao().getAll(player.getUniqueId()).isEmpty()); diff --git a/src/test/java/com/samleighton/sethomestwo/utils/BypassNodesTest.java b/src/test/java/com/samleighton/sethomestwo/utils/BypassNodesTest.java index 71a5d62..a30fbc0 100644 --- a/src/test/java/com/samleighton/sethomestwo/utils/BypassNodesTest.java +++ b/src/test/java/com/samleighton/sethomestwo/utils/BypassNodesTest.java @@ -63,7 +63,7 @@ void aHolderOfTheMaxHomesNodeExceedsTheLimit() { player.addAttachment(plugin, "sh2.bypass-max-homes", true); HomeFixtures.persist(player, "base"); - server.execute("create-home", player, "second").assertSucceeded(); + assertTrue(server.execute("create-home", player, "second").hasSucceeded()); assertEquals(2, new HomesDao().getAll(player.getUniqueId()).size()); } @@ -74,7 +74,7 @@ void withoutTheMaxHomesNodeTheLimitApplies() { TestPlayer player = addPlayer(); HomeFixtures.persist(player, "base"); - server.execute("create-home", player, "second").assertSucceeded(); + assertTrue(server.execute("create-home", player, "second").hasSucceeded()); assertEquals(1, new HomesDao().getAll(player.getUniqueId()).size()); } @@ -86,7 +86,7 @@ void aHolderOfTheBlacklistNodeCreatesAHomeInABlacklistedWorld() { HomeFixtures.blacklist(nether.getName()); player.teleport(new Location(nether, 10, 70, 10)); - server.execute("create-home", player, "base").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base").hasSucceeded()); assertEquals(1, new HomesDao().getAll(player.getUniqueId()).size()); } @@ -97,7 +97,7 @@ void withoutTheBlacklistNodeCreatingInABlacklistedWorldIsRefused() { HomeFixtures.blacklist(nether.getName()); player.teleport(new Location(nether, 10, 70, 10)); - server.execute("create-home", player, "base").assertSucceeded(); + assertTrue(server.execute("create-home", player, "base").hasSucceeded()); assertTrue(player.nextMessage().contains("blacklisted")); assertTrue(new HomesDao().getAll(player.getUniqueId()).isEmpty()); @@ -111,7 +111,7 @@ void aHolderOfTheBlacklistNodeMovesAHomeIntoABlacklistedWorld() { HomeFixtures.blacklist(nether.getName()); player.teleport(new Location(nether, 10, 70, 10)); - server.execute("move-home", player, "base").assertSucceeded(); + assertTrue(server.execute("move-home", player, "base").hasSucceeded()); Home moved = new HomesDao(true).getAll(player.getUniqueId()).get(0); assertEquals(nether.getUID().toString(), moved.getWorld()); @@ -125,7 +125,7 @@ void withoutTheBlacklistNodeMovingIntoABlacklistedWorldIsRefused() { HomeFixtures.blacklist(nether.getName()); player.teleport(new Location(nether, 10, 70, 10)); - server.execute("move-home", player, "base").assertSucceeded(); + assertTrue(server.execute("move-home", player, "base").hasSucceeded()); assertTrue(player.nextMessage().contains("blacklisted")); assertEquals(overworld.getUID().toString(), @@ -142,7 +142,7 @@ void aHolderOfTheBlacklistNodeTeleportsToAHomeInABlacklistedWorld() { HomeFixtures.persist(HomeFixtures.home(player, "far", new Location(nether, 100, 70, 100))); HomeFixtures.blacklist(nether.getName()); - server.execute("go-home", player, "far").assertSucceeded(); + assertTrue(server.execute("go-home", player, "far").hasSucceeded()); server.getScheduler().performTicks(100L); assertEquals(nether.getName(), player.getWorld().getName()); @@ -158,7 +158,7 @@ void withoutTheBlacklistNodeTheHomeStaysUnreachable() { HomeFixtures.persist(HomeFixtures.home(player, "far", new Location(nether, 100, 70, 100))); HomeFixtures.blacklist(nether.getName()); - server.execute("go-home", player, "far").assertSucceeded(); + assertTrue(server.execute("go-home", player, "far").hasSucceeded()); server.getScheduler().performTicks(100L); assertEquals(overworld.getName(), player.getWorld().getName()); @@ -174,7 +174,7 @@ void aHolderOfTheDelayNodeArrivesWithoutWaitingOutTheCountdown() { player.teleport(new Location(overworld, 0, 64, 0)); HomeFixtures.persist(HomeFixtures.home(player, "base", new Location(overworld, 100, 70, 100))); - server.execute("go-home", player, "base").assertSucceeded(); + assertTrue(server.execute("go-home", player, "base").hasSucceeded()); server.getScheduler().performOneTick(); assertEquals(100, player.getLocation().getBlockX()); @@ -188,7 +188,7 @@ void withoutTheDelayNodeTheCountdownStillRuns() { player.teleport(new Location(overworld, 0, 64, 0)); HomeFixtures.persist(HomeFixtures.home(player, "base", new Location(overworld, 100, 70, 100))); - server.execute("go-home", player, "base").assertSucceeded(); + assertTrue(server.execute("go-home", player, "base").hasSucceeded()); server.getScheduler().performOneTick(); assertEquals(0, player.getLocation().getBlockX()); From 39adae4e6a9bccb4ebe86dd97c1ceb1acfd5f3cb Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sun, 16 Aug 2026 14:30:03 -0400 Subject: [PATCH 46/75] feat: developer switch to keep test servers off bStats Starting a server with -Dsethomestwo.metrics.disabled=true stops the reporter from constructing bStats, so end-to-end and staging servers we run ourselves never reach the public dashboard. It is a system property on purpose, not a config.yml key: server owners keep no per-plugin switch, only bStats' own server wide one, which still applies. One info line is logged when the property is set. --- .../sethomestwo/metrics/MetricsReporter.java | 26 ++++++++++++++++--- .../metrics/MetricsReporterTest.java | 23 ++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java b/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java index 02db037..8f95850 100644 --- a/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java +++ b/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java @@ -30,6 +30,13 @@ public class MetricsReporter { /** Keeps bStats off the boot path, same as the update check. */ public static final long STARTUP_DELAY_TICKS = 100L; + /** + * Developer switch: start the server with -Dsethomestwo.metrics.disabled=true + * and nothing is reported. For test and end-to-end servers, so their traffic + * never reaches the public dashboard. Deliberately not a config.yml key. + */ + public static final String DISABLE_PROPERTY = "sethomestwo.metrics.disabled"; + private final SetHomesTwo plugin; private final int pluginId; private final BooleanSupplier enabled; @@ -40,14 +47,27 @@ public class MetricsReporter { public MetricsReporter(SetHomesTwo plugin) { this(plugin, PLUGIN_ID, - () -> bStatsEnabledGlobally(plugin.getDataFolder().getParentFile()), + () -> shouldReport(plugin.getDataFolder().getParentFile()), counters -> new BStatsHandle(plugin, counters)); } + /** + * Whether a live server should report: not switched off by the developer + * property, and not disabled through bStats' own server-wide config. + */ + static boolean shouldReport(File pluginsDir) { + if (Boolean.getBoolean(DISABLE_PROPERTY)) { + Bukkit.getLogger().info("SetHomesTwo metrics are off: " + DISABLE_PROPERTY + " is set."); + return false; + } + return bStatsEnabledGlobally(pluginsDir); + } + /** * bStats' server-wide switch, plugins/bStats/config.yml. Metrics has no - * per-plugin toggle, so this is the one opt-out and it is honoured before - * bStats is built at all. A missing file means enabled, as bStats itself treats it. + * per-plugin toggle, so this is the one owner-facing opt-out and it is + * honoured before bStats is built at all. A missing file means enabled, as + * bStats itself treats it. */ static boolean bStatsEnabledGlobally(File pluginsDir) { File config = new File(new File(pluginsDir, "bStats"), "config.yml"); diff --git a/src/test/java/com/samleighton/sethomestwo/metrics/MetricsReporterTest.java b/src/test/java/com/samleighton/sethomestwo/metrics/MetricsReporterTest.java index 9c48095..559c6fb 100644 --- a/src/test/java/com/samleighton/sethomestwo/metrics/MetricsReporterTest.java +++ b/src/test/java/com/samleighton/sethomestwo/metrics/MetricsReporterTest.java @@ -144,4 +144,27 @@ void everyDeclaredCommandGetsAChartId() { } assertEquals(14, plugin.getDescription().getCommands().size()); } + @Test + void theDeveloperPropertyTurnsReportingOff(@TempDir File pluginsDir) { + // No bStats config in this folder, so only the property can say no. + assertTrue(MetricsReporter.shouldReport(pluginsDir)); + + System.setProperty(MetricsReporter.DISABLE_PROPERTY, "true"); + try { + assertFalse(MetricsReporter.shouldReport(pluginsDir)); + } finally { + System.clearProperty(MetricsReporter.DISABLE_PROPERTY); + } + + assertTrue(MetricsReporter.shouldReport(pluginsDir)); + } + + @Test + void theGlobalSwitchStillWinsWithoutTheProperty(@TempDir File pluginsDir) throws IOException { + File bStatsDir = new File(pluginsDir, "bStats"); + assertTrue(bStatsDir.mkdirs()); + Files.writeString(new File(bStatsDir, "config.yml").toPath(), "enabled: false\n"); + + assertFalse(MetricsReporter.shouldReport(pluginsDir)); + } } From 3a19acf35f440ce7cdf6a65569229013c634c29c Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sun, 16 Aug 2026 14:46:39 -0400 Subject: [PATCH 47/75] fix: share one counter window per submission whatever order bStats asks bStats keeps custom charts in a hash set, so chart callbacks run in arbitrary order. WindowShare assumed the bar chart drained first and the line charts read its parked copy; when a line chart ran first it drained the family itself and the bar chart then drained again, empty, and overwrote the parked window. The result was an empty command ranking and a zero total in the same window where a per command line showed a count. The first chart to ask now drains and parks the window with a timestamp, every chart asking within 60 seconds reads that copy, and the next submission thirty minutes later drains fresh. --- .../sethomestwo/metrics/MetricsReporter.java | 6 +- .../sethomestwo/metrics/WindowShare.java | 45 ++++++++------ .../sethomestwo/metrics/WindowShareTest.java | 59 ++++++++++++------- 3 files changed, 68 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java b/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java index 8f95850..1571966 100644 --- a/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java +++ b/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java @@ -135,9 +135,9 @@ private static final class BStatsHandle implements AutoCloseable { // bStats keeps history (and time filters) only for line charts, and a // line chart carries one number, so each usage family gets a bar chart - // for the ranking of the last window plus line charts for trends. The - // bar chart drains the family and WindowShare hands the same window to - // the line charts registered after it, so nothing is read twice. + // for the ranking of the last window plus line charts for trends. + // WindowShare hands every chart the same drained window whatever order + // bStats calls them in, so nothing is read twice. WindowShare share = new WindowShare(counters); metrics.addCustomChart(new AdvancedBarChart("command_usage", () -> share.bars(UsageCounters.Family.COMMAND))); diff --git a/src/main/java/com/samleighton/sethomestwo/metrics/WindowShare.java b/src/main/java/com/samleighton/sethomestwo/metrics/WindowShare.java index 94d81ac..fba42d4 100644 --- a/src/main/java/com/samleighton/sethomestwo/metrics/WindowShare.java +++ b/src/main/java/com/samleighton/sethomestwo/metrics/WindowShare.java @@ -3,51 +3,62 @@ import java.util.EnumMap; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.LongSupplier; /** * Lets several charts report the same submission window of one counter family - * without draining it more than once. bStats runs every chart callback in - * registration order within one submission, so the bar chart drains and parks - * the window, and the line charts registered after it read the parked copy. + * without draining it more than once. bStats keeps its charts in a hash set, + * so callback order is arbitrary: the first reader of a family in a submission + * drains and parks the window, and every reader within {@link #SAME_SUBMISSION} + * of that gets the parked copy. Submissions are 30 minutes apart and all + * callbacks of one submission run within milliseconds, so the span cannot + * bridge two windows. */ final class WindowShare { + static final long SAME_SUBMISSION = TimeUnit.SECONDS.toNanos(60); + private final UsageCounters counters; + private final LongSupplier nanoTime; private final Map> parked = new EnumMap<>(UsageCounters.Family.class); + private final Map parkedAt = new EnumMap<>(UsageCounters.Family.class); WindowShare(UsageCounters counters) { + this(counters, System::nanoTime); + } + + WindowShare(UsageCounters counters, LongSupplier nanoTime) { this.counters = counters; + this.nanoTime = nanoTime; } - /** - * Drains the family into the one-value-per-bar shape AdvancedBarChart wants - * and parks the window for {@link #count} and {@link #total}. - */ + /** The window in the one-value-per-bar shape AdvancedBarChart wants. */ Map bars(UsageCounters.Family family) { - Map window = counters.snapshotAndReset(family); - parked.put(family, window); Map out = new HashMap<>(); - window.forEach((key, count) -> out.put(key, new int[]{count})); + window(family).forEach((key, count) -> out.put(key, new int[]{count})); return out; } - /** One key's count in the parked window, 0 when absent. */ + /** One key's count in the window, 0 when absent. */ int count(UsageCounters.Family family, String key) { return window(family).getOrDefault(key, 0); } - /** Sum of every key in the parked window. */ + /** Sum of every key in the window. */ int total(UsageCounters.Family family) { int sum = 0; for (int value : window(family).values()) sum += value; return sum; } - /** - * The parked window, draining the live counters if no bar chart parked one - * first so a line chart registered on its own still reports. - */ private Map window(UsageCounters.Family family) { - return parked.computeIfAbsent(family, counters::snapshotAndReset); + long now = nanoTime.getAsLong(); + Long at = parkedAt.get(family); + if (at == null || now - at > SAME_SUBMISSION) { + parked.put(family, counters.snapshotAndReset(family)); + parkedAt.put(family, now); + } + return parked.get(family); } } diff --git a/src/test/java/com/samleighton/sethomestwo/metrics/WindowShareTest.java b/src/test/java/com/samleighton/sethomestwo/metrics/WindowShareTest.java index 9150363..e34c775 100644 --- a/src/test/java/com/samleighton/sethomestwo/metrics/WindowShareTest.java +++ b/src/test/java/com/samleighton/sethomestwo/metrics/WindowShareTest.java @@ -3,6 +3,8 @@ import org.junit.jupiter.api.Test; import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import static com.samleighton.sethomestwo.metrics.UsageCounters.Family.COMMAND; import static com.samleighton.sethomestwo.metrics.UsageCounters.Family.GUI_ACTION; @@ -12,60 +14,73 @@ class WindowShareTest { + private final AtomicLong nanos = new AtomicLong(TimeUnit.HOURS.toNanos(1)); + + private WindowShare share(UsageCounters counters) { + return new WindowShare(counters, nanos::get); + } + + private void later(long seconds) { + nanos.addAndGet(TimeUnit.SECONDS.toNanos(seconds)); + } + @Test - void barsDrainTheFamilyAndParkTheWindowForTheLineReaders() { + void everyReaderInOneSubmissionSeesTheSameWindowWhateverTheOrder() { UsageCounters counters = new UsageCounters(); counters.increment(COMMAND, "go-home"); counters.increment(COMMAND, "go-home"); counters.increment(COMMAND, "create-home"); - WindowShare share = new WindowShare(counters); + WindowShare share = share(counters); + + // A line chart happens to run first: bStats keeps charts in a hash set. + assertEquals(2, share.count(COMMAND, "go-home")); + assertTrue(counters.snapshot(COMMAND).isEmpty(), "the first reader drains the family"); Map bars = share.bars(COMMAND); assertArrayEquals(new int[]{2}, bars.get("go-home")); assertArrayEquals(new int[]{1}, bars.get("create-home")); - assertTrue(counters.snapshot(COMMAND).isEmpty(), "bars must reset the window"); - assertEquals(2, share.count(COMMAND, "go-home")); + assertEquals(3, share.total(COMMAND)); assertEquals(1, share.count(COMMAND, "create-home")); assertEquals(0, share.count(COMMAND, "delete-home")); - assertEquals(3, share.total(COMMAND)); - - // Any number of readers may look at the same parked window. - assertEquals(2, share.count(COMMAND, "go-home")); - assertEquals(3, share.total(COMMAND)); } @Test - void theNextBarsCallReplacesTheParkedWindow() { + void theNextSubmissionDrainsAFreshWindow() { UsageCounters counters = new UsageCounters(); counters.increment(COMMAND, "go-home"); - WindowShare share = new WindowShare(counters); - share.bars(COMMAND); + WindowShare share = share(counters); + assertEquals(1, share.total(COMMAND)); counters.increment(COMMAND, "create-home"); - share.bars(COMMAND); + later(59); + assertEquals(1, share.total(COMMAND), "still the same submission"); - assertEquals(0, share.count(COMMAND, "go-home")); + later(2); + assertEquals(1, share.total(COMMAND), "next submission: only what arrived since"); assertEquals(1, share.count(COMMAND, "create-home")); - assertEquals(1, share.total(COMMAND)); + assertEquals(0, share.count(COMMAND, "go-home")); + assertTrue(share.bars(COMMAND).containsKey("create-home")); } @Test - void aReaderWithoutAPrecedingDrainDrainsTheLiveWindowItself() { + void anEmptyWindowIsStillOneWindow() { UsageCounters counters = new UsageCounters(); - counters.increment(GUI_ACTION, "back"); - WindowShare share = new WindowShare(counters); + WindowShare share = share(counters); - assertEquals(1, share.total(GUI_ACTION)); - assertTrue(counters.snapshot(GUI_ACTION).isEmpty()); - assertEquals(1, share.count(GUI_ACTION, "back"), "the drained window stays parked"); + assertTrue(share.bars(COMMAND).isEmpty()); + counters.increment(COMMAND, "go-home"); + assertEquals(0, share.total(COMMAND), "arrived after this submission drained"); + + later(120); + assertEquals(1, share.total(COMMAND)); } @Test void familiesDoNotLeakIntoEachOther() { UsageCounters counters = new UsageCounters(); counters.increment(COMMAND, "go-home"); - WindowShare share = new WindowShare(counters); + WindowShare share = share(counters); share.bars(COMMAND); assertEquals(0, share.total(GUI_ACTION)); From 93a1480c6feec808b1ba8b38a6a27ef28a70b74c Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sun, 16 Aug 2026 18:21:42 -0400 Subject: [PATCH 48/75] feat: line chart per alias, drop the bar and teleport source charts Adds a Single Line chart per alias declared in plugin.yml, so a rarely typed spelling can be retired on evidence over time. Removes the command and alias bar charts, the teleport source bar and its total, and with them the whole teleport source counter family, since nothing reads it any more. Drops the README section on usage statistics and the changeset sentence that pointed at it. --- .changeset/kind-ravens-gather.md | 2 +- README.md | 17 --------- .../sethomestwo/commands/GoHome.java | 3 -- .../sethomestwo/commands/GoPlayerHome.java | 3 -- .../samleighton/sethomestwo/gui/HomesGui.java | 4 +-- .../sethomestwo/metrics/MetricsReporter.java | 36 ++++++++++++++++--- .../sethomestwo/metrics/UsageCounters.java | 5 +-- .../sethomestwo/commands/GoHomeTest.java | 4 +-- .../commands/PlayerHomeAdminCommandsTest.java | 4 +-- .../sethomestwo/gui/HomesGuiClickTest.java | 4 +-- .../metrics/MetricsReporterTest.java | 19 ++++++++++ 11 files changed, 57 insertions(+), 44 deletions(-) diff --git a/.changeset/kind-ravens-gather.md b/.changeset/kind-ravens-gather.md index 5fd5032..840503c 100644 --- a/.changeset/kind-ravens-gather.md +++ b/.changeset/kind-ravens-gather.md @@ -2,4 +2,4 @@ bump: patch --- -The plugin now sends anonymous usage counts to bStats, so the maintainers can see which commands, menu buttons and settings are used, and whether database errors are happening in the wild. Nothing personal is sent, and the bStats switch in plugins/bStats/config.yml turns it off. The README lists exactly what is collected. +The plugin now sends anonymous usage counts to bStats, so the maintainers can see which commands, menu buttons and settings are used, and whether database errors are happening in the wild. Nothing personal is sent, and the bStats switch in plugins/bStats/config.yml turns it off. diff --git a/README.md b/README.md index 9251c05..b2cf4d6 100644 --- a/README.md +++ b/README.md @@ -223,23 +223,6 @@ To pick one up, copy the key you want out of [`default-config.yml`](https://gith
-### Anonymous usage statistics - -Set Homes Two reports anonymous usage counts to [bStats](https://bstats.org/plugin/bukkit/SetHomesTwo/33420), the same service most Bukkit plugins use. It tells the maintainers which commands and menu buttons are actually used, so the plugin can be trimmed and improved based on real use rather than guesswork. - -Every 30 minutes the plugin sends totals for that window, and nothing else: - -- how many times each command was run, and which spelling was typed (for example `/home` versus `/go-home`) -- how many times each button in the homes menu and the management menu was clicked -- how many teleports started from the menu versus a command, and how each ended (completed, cancelled by moving, refused because a teleport was already counting down, blocked by the blacklist, moved to a safe spot, cancelled as unsafe) -- which settings are on: home limits and their type, cancel on move, teleport safety, the delay as a range, whether the compass and default icon items are still the defaults, and whether LuckPerms is installed -- the number of homes on the server and the average per player, both as ranges (for example 51 to 500) -- how many times the plugin hit a database or item-data error, by kind only (a count of failed writes, never the message or the data) - -bStats itself adds the things it collects for every plugin: server software and Minecraft version, Java version, player count range, online mode, and country. No player names, UUIDs, coordinates, home names or server address are ever sent, and the aggregated charts are public. - -There is no per-plugin switch. To turn bStats off, set `enabled: false` in `plugins/bStats/config.yml` and restart; that file is shared by every plugin on the server that uses bStats, and Set Homes Two honours it before sending anything. - ## Coming from EssentialsX or Set Homes v1 Your players keep their homes. The old plugin does not even need to be running, because the importer reads its data files directly. diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java b/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java index 6ab6a0a..fd0aca0 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GoHome.java @@ -1,10 +1,8 @@ package com.samleighton.sethomestwo.commands; -import com.samleighton.sethomestwo.SetHomesTwo; import com.samleighton.sethomestwo.dao.Dao; import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.enums.UserError; -import com.samleighton.sethomestwo.metrics.UsageCounters; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.utils.ChatUtils; import com.samleighton.sethomestwo.utils.ConfigUtil; @@ -62,7 +60,6 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command } // Teleport player to home - SetHomesTwo.instance().getUsageCounters().increment(UsageCounters.Family.TELEPORT_SOURCE, UsageCounters.SOURCE_COMMAND); homeToTeleportTo.teleport(player); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java b/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java index af77098..7af2ede 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/GoPlayerHome.java @@ -1,10 +1,8 @@ package com.samleighton.sethomestwo.commands; -import com.samleighton.sethomestwo.SetHomesTwo; import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.enums.UserError; import com.samleighton.sethomestwo.enums.UserInfo; -import com.samleighton.sethomestwo.metrics.UsageCounters; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.utils.ChatUtils; import com.samleighton.sethomestwo.utils.ConfigUtil; @@ -64,7 +62,6 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command home.setCanTeleport(false); } - SetHomesTwo.instance().getUsageCounters().increment(UsageCounters.Family.TELEPORT_SOURCE, UsageCounters.SOURCE_COMMAND); home.teleport(admin); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java b/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java index 3039c61..38e1cc6 100644 --- a/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java +++ b/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java @@ -250,9 +250,7 @@ public void onClick(InventoryClickEvent event, GuiSession session) { player.closeInventory(); - UsageCounters counters = SetHomesTwo.instance().getUsageCounters(); - counters.increment(UsageCounters.Family.GUI_ACTION, UsageCounters.GUI_TELEPORT); - counters.increment(UsageCounters.Family.TELEPORT_SOURCE, UsageCounters.SOURCE_GUI); + SetHomesTwo.instance().getUsageCounters().increment(UsageCounters.Family.GUI_ACTION, UsageCounters.GUI_TELEPORT); // Teleport player to home home.teleport(player); diff --git a/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java b/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java index 1571966..4a44562 100644 --- a/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java +++ b/src/main/java/com/samleighton/sethomestwo/metrics/MetricsReporter.java @@ -14,7 +14,10 @@ import org.bukkit.configuration.file.YamlConfiguration; import java.io.File; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.function.BooleanSupplier; import java.util.function.Function; @@ -123,6 +126,31 @@ static String commandChartId(String commandName) { return "command_" + commandName.toLowerCase(Locale.ROOT).replace('-', '_'); } + /** + * bStats chart id for one alias's line chart, `alias_` plus the alias with + * hyphens as underscores. Each id has to be registered on the plugin's + * bStats page. + */ + static String aliasChartId(String alias) { + return "alias_" + alias.toLowerCase(Locale.ROOT).replace('-', '_'); + } + + /** + * Every alias declared in plugin.yml, lower-cased, in declaration order. + */ + static List declaredAliases(SetHomesTwo plugin) { + List aliases = new ArrayList<>(); + for (Map command : plugin.getDescription().getCommands().values()) { + Object declared = command.get("aliases"); + if (declared instanceof String) { + aliases.add(((String) declared).toLowerCase(Locale.ROOT)); + } else if (declared instanceof Iterable) { + for (Object alias : (Iterable) declared) aliases.add(String.valueOf(alias).toLowerCase(Locale.ROOT)); + } + } + return aliases; + } + /** * The real bStats wiring. Constructed only on a live server; the relocation * check inside Metrics throws under an unshaded classpath. @@ -140,14 +168,16 @@ private static final class BStatsHandle implements AutoCloseable { // bStats calls them in, so nothing is read twice. WindowShare share = new WindowShare(counters); - metrics.addCustomChart(new AdvancedBarChart("command_usage", () -> share.bars(UsageCounters.Family.COMMAND))); for (String command : plugin.getDescription().getCommands().keySet()) { metrics.addCustomChart(new SingleLineChart(commandChartId(command), () -> share.count(UsageCounters.Family.COMMAND, command))); } metrics.addCustomChart(new SingleLineChart("commands_total", () -> share.total(UsageCounters.Family.COMMAND))); - metrics.addCustomChart(new AdvancedBarChart("command_alias_usage", () -> share.bars(UsageCounters.Family.ALIAS))); + for (String alias : declaredAliases(plugin)) { + metrics.addCustomChart(new SingleLineChart(aliasChartId(alias), + () -> share.count(UsageCounters.Family.ALIAS, alias))); + } metrics.addCustomChart(new AdvancedBarChart("gui_action_usage", () -> share.bars(UsageCounters.Family.GUI_ACTION))); metrics.addCustomChart(new SingleLineChart("gui_actions_total", () -> share.total(UsageCounters.Family.GUI_ACTION))); @@ -155,8 +185,6 @@ private static final class BStatsHandle implements AutoCloseable { metrics.addCustomChart(new AdvancedBarChart("errors", () -> share.bars(UsageCounters.Family.ERROR))); metrics.addCustomChart(new SingleLineChart("errors_total", () -> share.total(UsageCounters.Family.ERROR))); - metrics.addCustomChart(new AdvancedBarChart("teleport_source", () -> share.bars(UsageCounters.Family.TELEPORT_SOURCE))); - metrics.addCustomChart(new SingleLineChart("teleports_total", () -> share.total(UsageCounters.Family.TELEPORT_SOURCE))); metrics.addCustomChart(new AdvancedPie("teleport_outcome", () -> counters.snapshotAndReset(UsageCounters.Family.TELEPORT_OUTCOME))); diff --git a/src/main/java/com/samleighton/sethomestwo/metrics/UsageCounters.java b/src/main/java/com/samleighton/sethomestwo/metrics/UsageCounters.java index 7c74905..6a5ca9c 100644 --- a/src/main/java/com/samleighton/sethomestwo/metrics/UsageCounters.java +++ b/src/main/java/com/samleighton/sethomestwo/metrics/UsageCounters.java @@ -12,10 +12,7 @@ */ public class UsageCounters { - public enum Family { COMMAND, ALIAS, GUI_ACTION, TELEPORT_SOURCE, TELEPORT_OUTCOME, ERROR } - - public static final String SOURCE_GUI = "gui"; - public static final String SOURCE_COMMAND = "command"; + public enum Family { COMMAND, ALIAS, GUI_ACTION, TELEPORT_OUTCOME, ERROR } public static final String OUTCOME_BLACKLISTED = "blacklisted"; public static final String OUTCOME_ALREADY_TELEPORTING = "already-teleporting"; diff --git a/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java b/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java index ed5d1b7..6a17789 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/GoHomeTest.java @@ -191,7 +191,7 @@ void withoutTheTeleportNodeNoRouteToAHomeWorks() { } @Test - void aCompletedCommandTeleportCountsSourceAndOutcome() { + void aCompletedCommandTeleportCountsTheOutcome() { TestPlayer player = addTestPlayer("traveller"); player.teleport(new Location(overworld, 0, 64, 0)); HomeFixtures.persist(HomeFixtures.home(player, "base", new Location(overworld, 44, 70, 44))); @@ -200,7 +200,6 @@ void aCompletedCommandTeleportCountsSourceAndOutcome() { assertTrue(server.execute("go-home", player, "base").hasSucceeded()); server.getScheduler().performTicks(100L); - assertEquals(1, plugin.getUsageCounters().snapshot(UsageCounters.Family.TELEPORT_SOURCE).get(UsageCounters.SOURCE_COMMAND)); assertEquals(1, outcomes().get(UsageCounters.OUTCOME_COMPLETED)); assertEquals(1, outcomes().size()); } @@ -233,6 +232,5 @@ void aSecondTeleportWhileOneIsRunningCountsAlreadyTeleporting() { assertTrue(server.execute("go-home", player, "base").hasSucceeded()); assertEquals(1, outcomes().get(UsageCounters.OUTCOME_ALREADY_TELEPORTING)); - assertEquals(2, plugin.getUsageCounters().snapshot(UsageCounters.Family.TELEPORT_SOURCE).get(UsageCounters.SOURCE_COMMAND)); } } diff --git a/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java b/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java index 683fb95..84ce506 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/PlayerHomeAdminCommandsTest.java @@ -284,9 +284,7 @@ void aBlacklistedTeleportCountsTheBlacklistedOutcome() { assertTrue(server.execute("go-player-home", admin, "Steve", "hideout").hasSucceeded()); server.getScheduler().performTicks(100L); - UsageCounters counters = plugin.getUsageCounters(); - assertEquals(1, counters.snapshot(UsageCounters.Family.TELEPORT_OUTCOME).get(UsageCounters.OUTCOME_BLACKLISTED)); - assertEquals(1, counters.snapshot(UsageCounters.Family.TELEPORT_SOURCE).get(UsageCounters.SOURCE_COMMAND)); + assertEquals(1, plugin.getUsageCounters().snapshot(UsageCounters.Family.TELEPORT_OUTCOME).get(UsageCounters.OUTCOME_BLACKLISTED)); } @Test diff --git a/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiClickTest.java b/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiClickTest.java index d78fbe8..39d4096 100644 --- a/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiClickTest.java +++ b/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiClickTest.java @@ -122,9 +122,7 @@ void leftClickCountsAGuiTeleport() { HomesGui gui = openOwnList(player); click(gui, new GuiSession(gui), player, 0, ClickType.LEFT); - UsageCounters counters = plugin.getUsageCounters(); - assertEquals(1, counters.snapshot(UsageCounters.Family.GUI_ACTION).get(UsageCounters.GUI_TELEPORT)); - assertEquals(1, counters.snapshot(UsageCounters.Family.TELEPORT_SOURCE).get(UsageCounters.SOURCE_GUI)); + assertEquals(1, plugin.getUsageCounters().snapshot(UsageCounters.Family.GUI_ACTION).get(UsageCounters.GUI_TELEPORT)); } @Test diff --git a/src/test/java/com/samleighton/sethomestwo/metrics/MetricsReporterTest.java b/src/test/java/com/samleighton/sethomestwo/metrics/MetricsReporterTest.java index 559c6fb..586fc2f 100644 --- a/src/test/java/com/samleighton/sethomestwo/metrics/MetricsReporterTest.java +++ b/src/test/java/com/samleighton/sethomestwo/metrics/MetricsReporterTest.java @@ -144,6 +144,7 @@ void everyDeclaredCommandGetsAChartId() { } assertEquals(14, plugin.getDescription().getCommands().size()); } + @Test void theDeveloperPropertyTurnsReportingOff(@TempDir File pluginsDir) { // No bStats config in this folder, so only the property can say no. @@ -167,4 +168,22 @@ void theGlobalSwitchStillWinsWithoutTheProperty(@TempDir File pluginsDir) throws assertFalse(MetricsReporter.shouldReport(pluginsDir)); } + + @Test + void aliasChartIdsAreStableAndUnderscored() { + assertEquals("alias_sethome", MetricsReporter.aliasChartId("sethome")); + assertEquals("alias_home_of", MetricsReporter.aliasChartId("home-of")); + assertEquals("alias_get_blacklisted_dimensions", MetricsReporter.aliasChartId("Get-Blacklisted-Dimensions")); + } + + @Test + void everyDeclaredAliasGetsAChartId() { + java.util.List aliases = MetricsReporter.declaredAliases(plugin); + assertEquals(11, aliases.size(), aliases.toString()); + assertTrue(aliases.contains("sethome")); + assertTrue(aliases.contains("get-blacklisted-dimensions")); + for (String alias : aliases) { + assertTrue(MetricsReporter.aliasChartId(alias).matches("alias_[a-z_]+"), alias); + } + } } From 2aee4caf5d765eed24bcc288998c6e68bdcfbf14 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Sun, 16 Aug 2026 23:02:30 -0400 Subject: [PATCH 49/75] fix: import case-only duplicate homes instead of dropping them Closes #48 --- .changeset/happy-otters-dance.md | 5 + README.md | 2 + .../samleighton/sethomestwo/dao/HomesDao.java | 29 +++ .../sethomestwo/importers/ImportReport.java | 5 +- .../importers/SetHomesV1Importer.java | 87 ++++++- .../sethomestwo/dao/HomesDaoTest.java | 20 ++ .../importers/ImportReportTest.java | 33 +++ .../importers/SetHomesV1ImporterTest.java | 222 ++++++++++++++++++ 8 files changed, 395 insertions(+), 8 deletions(-) create mode 100644 .changeset/happy-otters-dance.md create mode 100644 src/test/java/com/samleighton/sethomestwo/importers/ImportReportTest.java diff --git a/.changeset/happy-otters-dance.md b/.changeset/happy-otters-dance.md new file mode 100644 index 0000000..52eed20 --- /dev/null +++ b/.changeset/happy-otters-dance.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +Importing from Set Homes v1 no longer loses a home whose name differs only in capitalisation. Set Homes v1 allowed one player to hold both 'base' and 'Base', while home names here ignore case, so the second one is now imported under the next free name such as 'Base2' instead of being silently dropped. The import report and the server log both name it, and the preview now reports the same numbers as the confirm that follows it. diff --git a/README.md b/README.md index b2cf4d6..ed4d22b 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,8 @@ Your players keep their homes. The old plugin does not even need to be running, Existing homes are never overwritten, so re-running the import is always safe. Homes in worlds that no longer exist are skipped with a warning naming the world. +Set Homes v1 told `base` and `Base` apart, while home names here ignore case. A player holding both keeps both: the second one is imported under the next free name, so `Base` arrives as `Base2`, and the report and the server log name it. No home is dropped for a name clash. +
What else the Set Homes v1 import brings across diff --git a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java index a8154af..a06aab5 100644 --- a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java +++ b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java @@ -91,6 +91,35 @@ public List getAll(Object... keys) { return playerHomes; } + /** + * Every home name this player owns, with the casing it was stored under. + * Reads the name column alone, so it resolves no worlds and builds no + * {@link Home}, and works on a database holding a home in a world that no + * longer exists. + * + * @param playerUUID The owner + * @return The names, in no particular order; empty when the player has none + */ + public List namesFor(UUID playerUUID) { + List names = new ArrayList<>(); + + String sql = "select name from %s where player_uuid = ?"; + ResultSet rs = DatabaseUtil.fetch(this.conn, String.format(sql, TABLE_NAME), playerUUID.toString()); + + if (rs == null) return names; + + try { + while (rs.next()) { + names.add(rs.getString("name")); + } + } catch (SQLException e) { + Bukkit.getLogger().severe("There was an issue reading home names for player " + playerUUID); + Bukkit.getLogger().info(e.getMessage()); + } + + return names; + } + /** * Look a home up by owner and name. The name match ignores case, which is * safe because {@link #nameExists} makes names unique per player ignoring diff --git a/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java b/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java index d01d643..3ecbb17 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/ImportReport.java @@ -5,6 +5,7 @@ public class ImportReport { public int imported = 0; + public int renamed = 0; public int skippedExisting = 0; public int skippedWorldMissing = 0; public int failed = 0; @@ -17,8 +18,8 @@ public class ImportReport { public String summary(boolean dryRun) { String verb = dryRun ? "Would import" : "Imported"; String base = String.format( - "%s %d homes (%d skipped: name exists, %d skipped: world missing, %d failed).", - verb, imported, skippedExisting, skippedWorldMissing, failed + "%s %d homes (%d renamed to avoid a name clash, %d skipped: already imported, %d skipped: world missing, %d failed).", + verb, imported, renamed, skippedExisting, skippedWorldMissing, failed ); if (namesResolved > 0) { base += String.format(" %d home(s) matched an owner name.", namesResolved); diff --git a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java index 644798f..fd18ab8 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java @@ -11,8 +11,11 @@ import org.bukkit.configuration.file.YamlConfiguration; import java.io.File; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; public class SetHomesV1Importer implements HomesImporter { @@ -35,6 +38,7 @@ public ImportReport run(boolean dryRun) { YamlConfiguration source = YamlConfiguration.loadConfiguration(homesFile); HomesDao homesDao = new HomesDao(); + NameLedger ledger = new NameLedger(homesDao); Map cachedNames = HomesImporter.cachedNames(); // Named homes: allNamedHomes...{world,x,y,z,pitch,yaw,desc} @@ -44,7 +48,7 @@ public ImportReport run(boolean dryRun) { ConfigurationSection playerSection = allNamed.getConfigurationSection(uuid); if (playerSection == null) continue; for (String homeName : playerSection.getKeys(false)) { - importOne(homesDao, report, playerSection.getConfigurationSection(homeName), uuid, homeName, cachedNames, dryRun); + importOne(homesDao, ledger, report, playerSection.getConfigurationSection(homeName), uuid, homeName, cachedNames, dryRun); } } } @@ -53,7 +57,7 @@ public ImportReport run(boolean dryRun) { ConfigurationSection unknown = source.getConfigurationSection("unknownHomes"); if (unknown != null) { for (String uuid : unknown.getKeys(false)) { - importOne(homesDao, report, unknown.getConfigurationSection(uuid), uuid, "default", cachedNames, dryRun); + importOne(homesDao, ledger, report, unknown.getConfigurationSection(uuid), uuid, "default", cachedNames, dryRun); } } @@ -63,7 +67,7 @@ public ImportReport run(boolean dryRun) { return report; } - private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSection home, String playerUUID, String homeName, Map cachedNames, boolean dryRun) { + private void importOne(HomesDao homesDao, NameLedger ledger, ImportReport report, ConfigurationSection home, String playerUUID, String homeName, Map cachedNames, boolean dryRun) { try { if (home == null) { report.failed++; @@ -77,11 +81,23 @@ private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSect return; } - if (homesDao.get(UUID.fromString(playerUUID), homeName) != null) { + UUID owner = UUID.fromString(playerUUID); + + if (ledger.importedBefore(owner, homeName)) { report.skippedExisting++; return; } + String storedName = ledger.claim(owner, homeName); + if (!storedName.equals(homeName)) { + report.renamed++; + String note = String.format( + "Home '%s' for player %s differs only in capitalisation from another of that player's homes, which Set Homes v1 allowed. It takes the name '%s' here, so both locations are kept.", + homeName, playerUUID, storedName); + report.warnings.add(note); + if (!dryRun) Bukkit.getLogger().warning(note); + } + Location location = new Location( world, home.getDouble("x"), @@ -91,7 +107,7 @@ private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSect (float) home.getDouble("pitch") ); - String playerName = cachedNames.get(UUID.fromString(playerUUID)); + String playerName = cachedNames.get(owner); if (playerName != null) report.namesResolved++; if (!dryRun) { @@ -99,7 +115,7 @@ private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSect playerUUID, HomesImporter.defaultMaterial(), location, - homeName, + storedName, home.getString("desc"), world.getEnvironment().toString() ); @@ -119,6 +135,65 @@ private void importOne(HomesDao homesDao, ImportReport report, ConfigurationSect } } + /** + * Which home names are taken for each player, so a name that v1 allowed but + * this plugin cannot store twice is renamed instead of dropped. Names here + * are unique per player ignoring case, while v1 matched case sensitively, so + * one player could hold both 'base' and 'Base'. + *

+ * Names already in the database are snapshotted per player before that + * player's first import, and names taken during the run are tracked + * separately. That split is what lets a dry run, which writes nothing, + * report the same numbers as the confirm that follows it. + */ + private static final class NameLedger { + private final HomesDao homesDao; + private final Map> beforeThisRun = new HashMap<>(); + private final Map> takenThisRun = new HashMap<>(); + + private NameLedger(HomesDao homesDao) { + this.homesDao = homesDao; + } + + /** True when this player already had a home of this name before the run started. */ + private boolean importedBefore(UUID player, String name) { + return namesBeforeThisRun(player).contains(name.toLowerCase()); + } + + /** + * Take a name for this player: the one asked for, or the first free + * numbered variant of it. + * + * @return The name actually taken, equal to the one asked for when it was free + */ + private String claim(UUID player, String wanted) { + String candidate = wanted; + + for (int suffix = 2; isTaken(player, candidate); suffix++) { + candidate = wanted + suffix; + } + + takenThisRun.computeIfAbsent(player, p -> new HashSet<>()).add(candidate.toLowerCase()); + return candidate; + } + + private boolean isTaken(UUID player, String name) { + String lowered = name.toLowerCase(); + return namesBeforeThisRun(player).contains(lowered) + || takenThisRun.getOrDefault(player, Set.of()).contains(lowered); + } + + private Set namesBeforeThisRun(UUID player) { + return beforeThisRun.computeIfAbsent(player, p -> { + Set names = new HashSet<>(); + for (String name : homesDao.namesFor(p)) { + names.add(name.toLowerCase()); + } + return names; + }); + } + } + /** * v1's world_blacklist.yml holds a flat blacklisted_worlds list. Missing or * empty is normal, not an error. A world absent from this server is still diff --git a/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoTest.java b/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoTest.java index e29f205..fe2cc39 100644 --- a/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoTest.java +++ b/src/test/java/com/samleighton/sethomestwo/dao/HomesDaoTest.java @@ -24,6 +24,26 @@ class HomesDaoTest extends ServerTestBase { + @Test + void namesForReturnsEveryHomeNameWithItsStoredCasing() { + PlayerMock player = addPlayer(); + HomeFixtures.persist(player, "Base"); + HomeFixtures.persist(player, "shop"); + + List names = new HomesDao().namesFor(player.getUniqueId()); + + assertEquals(2, names.size()); + assertTrue(names.contains("Base")); + assertTrue(names.contains("shop")); + } + + @Test + void namesForIsEmptyForAPlayerWithNoHomes() { + PlayerMock player = addPlayer(); + + assertTrue(new HomesDao().namesFor(player.getUniqueId()).isEmpty()); + } + @Test void savedHomeComesBackFromGetAll() { PlayerMock player = addPlayer(); diff --git a/src/test/java/com/samleighton/sethomestwo/importers/ImportReportTest.java b/src/test/java/com/samleighton/sethomestwo/importers/ImportReportTest.java new file mode 100644 index 0000000..eb1c6b1 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/importers/ImportReportTest.java @@ -0,0 +1,33 @@ +package com.samleighton.sethomestwo.importers; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ImportReportTest { + + @Test + void theSummaryTellsARenameApartFromAHomeThatWasAlreadyImported() { + ImportReport report = new ImportReport(); + report.imported = 11; + report.renamed = 1; + report.skippedExisting = 3; + + String summary = report.summary(false); + + assertTrue(summary.contains("1 renamed"), summary); + assertTrue(summary.contains("3 skipped: already imported"), summary); + } + + @Test + void theSummaryKeepsItsShapeOnADryRun() { + ImportReport report = new ImportReport(); + report.imported = 11; + report.renamed = 1; + + String summary = report.summary(true); + + assertTrue(summary.startsWith("Would import 11 homes"), summary); + assertTrue(summary.contains("1 renamed"), summary); + } +} diff --git a/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java index e22405b..0abe702 100644 --- a/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java +++ b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java @@ -2,16 +2,22 @@ import com.samleighton.sethomestwo.dao.BlacklistDao; import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; +import com.samleighton.sethomestwo.support.TestPlayer; +import org.bukkit.Bukkit; import org.bukkit.configuration.file.YamlConfiguration; import org.junit.jupiter.api.Test; import org.mockbukkit.mockbukkit.entity.PlayerMock; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.UUID; +import java.util.logging.Handler; +import java.util.logging.LogRecord; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -248,6 +254,222 @@ void theConfigReportNeverWritesToConfigYml() throws IOException { assertEquals(before, new File(plugin.getDataFolder(), "config.yml").lastModified()); } + // -- Case-only duplicates from v1 (issue #48) -------------------------- + // + // v1 matched home names case sensitively, so one player could hold 'base' + // and 'Base' as two separate homes. Names here are unique per player + // ignoring case, so the second one is renamed rather than dropped. + + private final YamlConfiguration v1Homes = new YamlConfiguration(); + + /** Adds one entry to allNamedHomes. Distinct x values tell the rows apart. */ + private void addV1Home(UUID owner, String homeName, String worldName, double x) { + String path = "allNamedHomes." + owner + "." + homeName + "."; + v1Homes.set(path + "world", worldName); + v1Homes.set(path + "x", x); + v1Homes.set(path + "y", 64.0); + v1Homes.set(path + "z", 0.0); + v1Homes.set(path + "pitch", 0.0); + v1Homes.set(path + "yaw", 0.0); + } + + /** Adds the player's unnamed v1 home, which imports under the name 'default'. */ + private void addV1UnnamedHome(UUID owner, double x) { + String path = "unknownHomes." + owner + "."; + v1Homes.set(path + "world", "world"); + v1Homes.set(path + "x", x); + v1Homes.set(path + "y", 64.0); + v1Homes.set(path + "z", 0.0); + v1Homes.set(path + "pitch", 0.0); + v1Homes.set(path + "yaw", 0.0); + } + + private void saveV1Homes() throws IOException { + v1Homes.save(new File(setHomesDir(), "homes.yml")); + } + + private String counts(ImportReport report) { + return String.format("imported=%d renamed=%d skippedExisting=%d skippedWorldMissing=%d failed=%d namesResolved=%d", + report.imported, report.renamed, report.skippedExisting, + report.skippedWorldMissing, report.failed, report.namesResolved); + } + + private List homeNamesOf(UUID owner) { + return new HomesDao().getAll(owner).stream().map(Home::getName).toList(); + } + + @Test + void aCaseOnlyDuplicateIsImportedUnderAFreeNameRatherThanDropped() throws IOException { + UUID owner = UUID.randomUUID(); + addV1Home(owner, "base", "world", 10.0); + addV1Home(owner, "Base", "world", 20.0); + saveV1Homes(); + + ImportReport report = importer.run(false); + + assertEquals(2, report.imported); + assertEquals(1, report.renamed); + assertEquals(0, report.skippedExisting); + assertEquals(List.of("base", "Base2"), homeNamesOf(owner)); + } + + @Test + void theRenamedHomeKeepsItsOwnLocation() throws IOException { + UUID owner = UUID.randomUUID(); + addV1Home(owner, "base", "world", 10.0); + addV1Home(owner, "Base", "world", 20.0); + saveV1Homes(); + + importer.run(false); + + HomesDao dao = new HomesDao(); + assertEquals(10.0, dao.get(owner, "base").getX()); + assertEquals(20.0, dao.get(owner, "Base2").getX()); + } + + @Test + void theRenameIsReportedWithBothTheOldAndTheNewName() throws IOException { + UUID owner = UUID.randomUUID(); + addV1Home(owner, "base", "world", 10.0); + addV1Home(owner, "Base", "world", 20.0); + saveV1Homes(); + + ImportReport report = importer.run(false); + + assertTrue(report.warnings.stream().anyMatch(w -> w.contains("'Base'") && w.contains("'Base2'")), + "no warning naming the old and new name, got: " + report.warnings); + } + + @Test + void theRenameIsRecordedInTheServerLog() throws IOException { + UUID owner = UUID.randomUUID(); + addV1Home(owner, "base", "world", 10.0); + addV1Home(owner, "Base", "world", 20.0); + saveV1Homes(); + + List logged = new ArrayList<>(); + Handler capture = new Handler() { + @Override public void publish(LogRecord record) { logged.add(record.getMessage()); } + @Override public void flush() { } + @Override public void close() { } + }; + Bukkit.getLogger().addHandler(capture); + try { + importer.run(false); + } finally { + Bukkit.getLogger().removeHandler(capture); + } + + assertTrue(logged.stream().anyMatch(m -> m.contains("'Base'") && m.contains("'Base2'")), + "the rename was not logged, got: " + logged); + } + + @Test + void aGenuineReImportRenamesNothingAndImportsNothing() throws IOException { + UUID owner = UUID.randomUUID(); + addV1Home(owner, "base", "world", 10.0); + addV1Home(owner, "Base", "world", 20.0); + saveV1Homes(); + importer.run(false); + + ImportReport second = importer.run(false); + + assertEquals(0, second.imported); + assertEquals(0, second.renamed); + assertEquals(2, second.skippedExisting); + assertEquals(2, new HomesDao().getAll(owner).size()); + } + + @Test + void theDryRunReportsTheSameNumbersAsTheConfirmThatFollowsIt() throws IOException { + PlayerMock steve = addPlayer("Steve"); + steve.disconnect(); + addV1Home(steve.getUniqueId(), "base", "world", 10.0); + addV1Home(steve.getUniqueId(), "Base", "world", 20.0); + addV1Home(steve.getUniqueId(), "gone", "world_deleted", 30.0); + saveV1Homes(); + + ImportReport dryRun = importer.run(true); + ImportReport confirm = importer.run(false); + + assertEquals(counts(dryRun), counts(confirm)); + } + + @Test + void aDryRunDetectsACaseOnlyDuplicateWithoutWritingAnything() throws IOException { + UUID owner = UUID.randomUUID(); + addV1Home(owner, "base", "world", 10.0); + addV1Home(owner, "Base", "world", 20.0); + saveV1Homes(); + + ImportReport report = importer.run(true); + + assertEquals(1, report.renamed); + assertTrue(new HomesDao().getAll(owner).isEmpty()); + } + + @Test + void noHomeIsLostWhenTheDisambiguatedNameIsItselfTakenLaterInTheFile() throws IOException { + UUID owner = UUID.randomUUID(); + addV1Home(owner, "base", "world", 10.0); + addV1Home(owner, "Base", "world", 20.0); + addV1Home(owner, "base2", "world", 30.0); + saveV1Homes(); + + importer.run(false); + + List imported = new HomesDao().getAll(owner).stream().map(Home::getX).sorted().toList(); + assertEquals(List.of(10.0, 20.0, 30.0), imported); + List lowered = homeNamesOf(owner).stream().map(String::toLowerCase).distinct().toList(); + assertEquals(3, lowered.size(), "names must stay unique ignoring case, got: " + homeNamesOf(owner)); + } + + @Test + void anUnnamedHomeSurvivesAPlayerAlreadyHavingAHomeCalledDefault() throws IOException { + UUID owner = UUID.randomUUID(); + addV1Home(owner, "default", "world", 10.0); + addV1UnnamedHome(owner, 20.0); + saveV1Homes(); + + ImportReport report = importer.run(false); + + assertEquals(2, report.imported); + assertEquals(1, report.renamed); + assertEquals(2, new HomesDao().getAll(owner).size()); + } + + @Test + void theFreeNameSearchKeepsGoingPastANameAlreadyInTheDatabase() throws IOException { + PlayerMock owner = addPlayer(); + HomeFixtures.persist(owner, "Base2"); + addV1Home(owner.getUniqueId(), "base", "world", 10.0); + addV1Home(owner.getUniqueId(), "Base", "world", 20.0); + saveV1Homes(); + + importer.run(false); + + assertEquals(List.of("Base2", "base", "Base3"), homeNamesOf(owner.getUniqueId())); + } + + @Test + void bothHomesOfACaseOnlyPairAreReachableByCommand() throws IOException { + TestPlayer traveller = addTestPlayer("traveller"); + plugin.getConfig().set("teleportSafety", false); + plugin.getConfig().set("delay", 0); + addV1Home(traveller.getUniqueId(), "base", "world", 10.0); + addV1Home(traveller.getUniqueId(), "Base", "world", 20.0); + saveV1Homes(); + importer.run(false); + + server.execute("go-home", traveller, "base").assertSucceeded(); + server.getScheduler().performTicks(100L); + assertEquals(10, traveller.getLocation().getBlockX()); + + server.execute("go-home", traveller, "Base2").assertSucceeded(); + server.getScheduler().performTicks(100L); + assertEquals(20, traveller.getLocation().getBlockX()); + } + private void writeV1Config(java.util.function.Consumer body) throws IOException { YamlConfiguration yaml = new YamlConfiguration(); body.accept(yaml); From 081efc912ba71c3d8cb794ea0b1e607eb84dd04a Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 15:51:55 -0400 Subject: [PATCH 50/75] chore: shorten comments added by the v1 duplicate-name import fix --- .../samleighton/sethomestwo/dao/HomesDao.java | 8 +++---- .../importers/SetHomesV1Importer.java | 23 ++++--------------- .../importers/SetHomesV1ImporterTest.java | 10 +++----- 3 files changed, 11 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java index a06aab5..11fdacc 100644 --- a/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java +++ b/src/main/java/com/samleighton/sethomestwo/dao/HomesDao.java @@ -92,13 +92,11 @@ public List getAll(Object... keys) { } /** - * Every home name this player owns, with the casing it was stored under. - * Reads the name column alone, so it resolves no worlds and builds no - * {@link Home}, and works on a database holding a home in a world that no - * longer exists. + * Every home name this player owns, as stored. Reads only the name column, + * so it works when a home's world no longer exists. * * @param playerUUID The owner - * @return The names, in no particular order; empty when the player has none + * @return The names, unordered; empty when the player has none */ public List namesFor(UUID playerUUID) { List names = new ArrayList<>(); diff --git a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java index fd18ab8..39c36ca 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java @@ -135,17 +135,9 @@ private void importOne(HomesDao homesDao, NameLedger ledger, ImportReport report } } - /** - * Which home names are taken for each player, so a name that v1 allowed but - * this plugin cannot store twice is renamed instead of dropped. Names here - * are unique per player ignoring case, while v1 matched case sensitively, so - * one player could hold both 'base' and 'Base'. - *

- * Names already in the database are snapshotted per player before that - * player's first import, and names taken during the run are tracked - * separately. That split is what lets a dry run, which writes nothing, - * report the same numbers as the confirm that follows it. - */ + // v1 allowed case-only duplicate names ('base' and 'Base'); v2 does not, so + // the second one is stored as 'Base2' instead of dropped. Names taken this + // run are tracked in memory so a dry run reports the same as the confirm. private static final class NameLedger { private final HomesDao homesDao; private final Map> beforeThisRun = new HashMap<>(); @@ -155,17 +147,12 @@ private NameLedger(HomesDao homesDao) { this.homesDao = homesDao; } - /** True when this player already had a home of this name before the run started. */ + // True when the player had a home of this name before the run started. private boolean importedBefore(UUID player, String name) { return namesBeforeThisRun(player).contains(name.toLowerCase()); } - /** - * Take a name for this player: the one asked for, or the first free - * numbered variant of it. - * - * @return The name actually taken, equal to the one asked for when it was free - */ + // Returns the name asked for, or the first free numbered variant of it. private String claim(UUID player, String wanted) { String candidate = wanted; diff --git a/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java index 0abe702..28516ef 100644 --- a/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java +++ b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java @@ -254,15 +254,11 @@ void theConfigReportNeverWritesToConfigYml() throws IOException { assertEquals(before, new File(plugin.getDataFolder(), "config.yml").lastModified()); } - // -- Case-only duplicates from v1 (issue #48) -------------------------- - // - // v1 matched home names case sensitively, so one player could hold 'base' - // and 'Base' as two separate homes. Names here are unique per player - // ignoring case, so the second one is renamed rather than dropped. + // Case-only duplicates from v1: 'base' and 'Base' were two homes there. private final YamlConfiguration v1Homes = new YamlConfiguration(); - /** Adds one entry to allNamedHomes. Distinct x values tell the rows apart. */ + // Adds one entry to allNamedHomes. Distinct x values tell the rows apart. private void addV1Home(UUID owner, String homeName, String worldName, double x) { String path = "allNamedHomes." + owner + "." + homeName + "."; v1Homes.set(path + "world", worldName); @@ -273,7 +269,7 @@ private void addV1Home(UUID owner, String homeName, String worldName, double x) v1Homes.set(path + "yaw", 0.0); } - /** Adds the player's unnamed v1 home, which imports under the name 'default'. */ + // Adds the player's unnamed v1 home, which imports as 'default'. private void addV1UnnamedHome(UUID owner, double x) { String path = "unknownHomes." + owner + "."; v1Homes.set(path + "world", "world"); From f3a557abff818fa1224ad6dde4331de9cec01847 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 16:11:50 -0400 Subject: [PATCH 51/75] docs: call the plugin Set Homes, with v2 only where it sits beside v1 The banner now reads Set Homes with a v2 tag under the tagline, and the README uses plain Set Homes for the product and v2 only in the migration section. --- README.md | 34 +++++++++++++++++----------------- docs/img/logo.png | Bin 190349 -> 203311 bytes 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ed4d22b..a562f56 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -![Set Homes Two](docs/img/logo.png) +![Set Homes](docs/img/logo.png) -**Set Homes Two gives every player a menu of their homes.** Left-click one to teleport. Right-click it to rename it, move it, change its icon, or delete it. +**Set Homes gives every player a menu of their homes.** Left-click one to teleport. Right-click it to rename it, move it, change its icon, or delete it. [Source](https://github.com/Blockframe-Studios/SetHomesTwo) | [Report a bug](https://github.com/Blockframe-Studios/SetHomesTwo/issues) | [Donate](https://www.paypal.com/donate/?business=8LXCRFX27B37C&no_recurring=0&item_name=Thanks+for+your+support.+It+helps+keep+this+plugin+up+to+date+%3A%29¤cy_code=USD) ![The homes menu, with each home shown as its own item](docs/img/homes-menu.png) -## Why Set Homes Two +## Why Set Homes - **A menu, or a list of commands.** Homes live in a chest-style GUI. Players open it with `/homes` or by right-clicking the configured "Home Item". - **Every home gets its own icon.** Pick any Minecraft item when you create a home, or change it later to whatever you are holding. A base, a mine and a farm stop looking identical. - **Rename, move and delete in-game.** Right-click any home to manage it. Deleting always asks first, so nobody loses a base to a misclick. -- **Teleports that do not kill you.** Set Homes Two checks the destination and relocates you to the nearest safe spot rather than dropping you into blocks, lava, or a fall. +- **Teleports that do not kill you.** Set Homes checks the destination and relocates you to the nearest safe spot rather than dropping you into blocks, lava, or a fall. - **Switch without losing anything.** One command imports every home from EssentialsX or Set Homes v1, and shows you exactly what it will do before it does it. - **Per-rank home limits.** Give donors more homes than default players with LuckPerms groups, or set one server-wide limit. - **Permissions you can change from the config.** Every `sh2.*` node has a sensible default, and any of them can be moved in `config.yml`. No permissions plugin required. @@ -102,7 +102,7 @@ By default players wait three seconds before a teleport fires, and moving cancel ![Instant teleport](docs/img/teleport-instant.gif) -Before it drops anyone anywhere, Set Homes Two checks the destination is safe to stand in. If a home has been built over, flooded with lava, or left hanging above a drop, the player is moved to the nearest safe spot instead, or the teleport is cancelled and they are told why. Turn it off with `teleportSafety: false`. +Before it drops anyone anywhere, Set Homes checks the destination is safe to stand in. If a home has been built over, flooded with lava, or left hanging above a drop, the player is moved to the nearest safe spot instead, or the teleport is cancelled and they are told why. Turn it off with `teleportSafety: false`. ## Permissions @@ -217,7 +217,7 @@ That table is only the common settings. For the complete list, see [`default-con

Upgrading? Your existing config.yml will not gain the new settings -Set Homes Two never touches a `config.yml` that already exists, so settings added in a later release do not appear in a file written by an earlier one. Any missing setting quietly falls back to its default, so nothing breaks, but you cannot change a setting you cannot see. +Set Homes never touches a `config.yml` that already exists, so settings added in a later release do not appear in a file written by an earlier one. Any missing setting quietly falls back to its default, so nothing breaks, but you cannot change a setting you cannot see. To pick one up, copy the key you want out of [`default-config.yml`](https://github.com/Blockframe-Studios/SetHomesTwo/blob/master/src/main/resources/default-config.yml) into your file and restart. To start clean, rename your `config.yml` and restart. A fresh one is written with everything in it, and you can copy your old values across. @@ -229,7 +229,7 @@ Your players keep their homes. The old plugin does not even need to be running, 1. Run `/import-homes essentialsx` (or `/import-homes sethomes`). This is a **preview only**. It reports how many homes it would import and warns about any it would skip, and changes nothing. 2. Happy with the numbers? Run it again with `confirm` on the end. -3. Remove the old jar. Set Homes Two provides `/sethome`, `/home` and `/delhome`, and two plugins claiming the same commands will fight over them. +3. Remove the old jar. This plugin provides `/sethome`, `/home` and `/delhome`, and two plugins claiming the same commands will fight over them. Existing homes are never overwritten, so re-running the import is always safe. Homes in worlds that no longer exist are skipped with a warning naming the world. @@ -238,7 +238,7 @@ Set Homes v1 told `base` and `Base` apart, while home names here ignore case. A
What else the Set Homes v1 import brings across -- **The v1 world blacklist**, added to your Set Homes Two blacklist alongside the homes. Re-running never adds a world twice. +- **The v1 world blacklist**, added to your Set Homes v2 blacklist alongside the homes. Re-running never adds a world twice. - **A report of your v1 `config.yml`**, listing any setting that has an equivalent here and the key to put it under. Nothing is written to `config.yml` automatically. The table further down has the same mapping for pasting in by hand. - **Player names**, read from the server's own player list. That means `/get-player-homes`, `/home-of`, `/delhome-of` and `/uhome-of` work on an imported player straight away, for anyone this server has seen before. A player the server has never seen imports with no name and is picked up automatically on their first join. @@ -247,7 +247,7 @@ Set Homes v1 told `base` and `Base` apart, while home names here ignore case. A
Set Homes v1: what each command and permission became -| Set Homes v1 | Set Homes Two | +| Set Homes v1 | Set Homes v2 | | --- | --- | | `/sethome [name] [description]` | `/sethome [name] [icon] [description]` | | `/home [name]` | `/home [name]` | @@ -260,7 +260,7 @@ Set Homes v1 told `base` and `Base` apart, while home names here ignore case. A | `/blacklist ` | `/blacklist ` | | `/setmax ` | `/setmax`, or the long form `/set-max-homes` | -| v1 permission | Set Homes Two permission | +| v1 permission | v2 permission | | --- | --- | | `homes.home` | `sh2.go-home`, plus `sh2.teleport` to actually arrive | | `homes.sethome` | `sh2.create-home` | @@ -280,25 +280,25 @@ Set Homes v1 told `base` and `Base` apart, while home names here ignore case. A Worth knowing before you copy a permissions file across: -- **`homes.config_bypass` is three nodes now.** In v1 it let a player exceed the home limit, set homes in blacklisted worlds, and skip the teleport delay and cooldown, all at once. Grant all three `sh2.bypass-*` nodes to reproduce that. Nothing is lost on the cooldown, because Set Homes Two has no cooldown feature. +- **`homes.config_bypass` is three nodes now.** In v1 it let a player exceed the home limit, set homes in blacklisted worlds, and skip the teleport delay and cooldown, all at once. Grant all three `sh2.bypass-*` nodes to reproduce that. Nothing is lost on the cooldown, because v2 has no cooldown feature. - **Your v1 unnamed home is called `default`.** The importer files it under that name, and a bare `/sethome` or `/home` uses the same name, so both keep working exactly as they did. `/home-of steve default` reaches an imported unnamed home. -- **`/sethome` takes a description straight after the name again**, as it did in v1. Set Homes Two adds an optional icon in between, so a second word naming a real item is read as the icon. `/sethome base d my main base` forces the default icon and keeps the whole phrase. +- **`/sethome` takes a description straight after the name again**, as it did in v1. v2 adds an optional icon in between, so a second word naming a real item is read as the icon. `/sethome base d my main base` forces the default icon and keeps the whole phrase. - **The one-letter aliases are not provided.** v1 registered `/h`, `/sh`, `/dh`, `/lh`, `/ho`, `/dho`, `/uh`, `/uho`, `/bl` and `/sm`. If your players are used to them, map them yourself in the server's own `commands.yml`. -- **`/homes` means something different.** In v1 it printed a chat list. In Set Homes Two it opens the homes menu, and `/list-homes` prints the chat list. +- **`/homes` means something different.** In v1 it printed a chat list. In v2 it opens the homes menu, and `/list-homes` prints the chat list.
-Set Homes v1: config.yml settings and their Set Homes Two equivalent +Set Homes v1: config.yml settings and their v2 equivalent -| v1 `config.yml` | Set Homes Two `config.yml` | Note | +| v1 `config.yml` | v2 `config.yml` | Note | | --- | --- | --- | | `tp-delay` | `delay` | direct | | `tp-cancelOnMove` | `cancelOnMove` | direct | -| `max-homes.` | `maxHomes.` | also set `maxHomesType: groups` and `maxHomeEnabled: true`. A v1 value of `0` means unlimited; leave that group out of `maxHomes` in Set Homes Two rather than setting it to `0`, which would cap it at zero homes instead. | +| `max-homes.` | `maxHomes.` | also set `maxHomesType: groups` and `maxHomeEnabled: true`. A v1 value of `0` means unlimited; leave that group out of `maxHomes` in v2 rather than setting it to `0`, which would cap it at zero homes instead. | | `max-homes-msg` | `maxHomesReached` | direct. v1's `§` colour codes paste in unchanged | | `tp-cancelOnMove-msg` | `movedWhileTeleporting` | direct. v1's `§` colour codes paste in unchanged | -| `tp-cooldown` | none | Set Homes Two has no cooldown feature | +| `tp-cooldown` | none | v2 has no cooldown feature | | `tp-cooldown-msg` | none | follows the above |
diff --git a/docs/img/logo.png b/docs/img/logo.png index 5d39ca2fb2cedcc1d0bf728ef61003c063c20f6b..df22b6b57f2f737610a8941206a7acd9ad117d2e 100644 GIT binary patch literal 203311 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
4;L2~o3zjL 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 z

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 z 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

_e69Q;6{0a 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 s#D6{P<9ed$$cy3 Date: Mon, 17 Aug 2026 16:31:12 -0400 Subject: [PATCH 52/75] updated listing pages for bukkit, modrinth, spigotmc --- docs/generated/listing-bukkitdev.html | 174 ------------------------ docs/generated/listing-bukkitdev.md | 114 ++++++++++++++++ docs/generated/listing-modrinth.md | 177 ++++++------------------- docs/generated/listing-spigotmc.bbcode | 157 ++++++---------------- 4 files changed, 202 insertions(+), 420 deletions(-) delete mode 100644 docs/generated/listing-bukkitdev.html create mode 100644 docs/generated/listing-bukkitdev.md diff --git a/docs/generated/listing-bukkitdev.html b/docs/generated/listing-bukkitdev.html deleted file mode 100644 index 94f5cba..0000000 --- a/docs/generated/listing-bukkitdev.html +++ /dev/null @@ -1,174 +0,0 @@ -

Set Homes Two

- -

Set Homes Two gives every player a menu of their homes. Left-click one to teleport. Right-click it to rename it, move it, change its icon, or delete it.

- -
- -

The homes menu, with each home shown as its own item

- -

Why Set Homes Two

- -
    -
  • A menu, or a list of commands. Homes live in a chest-style GUI. Players open it with /homes or by right-clicking the configured "Home Item".
  • -
  • Every home gets its own icon. Pick any Minecraft item when you create a home, or change it later to whatever you are holding. A base, a mine and a farm stop looking identical.
  • -
  • Rename, move and delete in-game. Right-click any home to manage it. Deleting always asks first, so nobody loses a base to a misclick.
  • -
  • Teleports that do not kill you. Set Homes Two checks the destination and relocates you to the nearest safe spot rather than dropping you into blocks, lava, or a fall.
  • -
  • Switch without losing anything. One command imports every home from EssentialsX or Set Homes v1, and shows you exactly what it will do before it does it.
  • -
  • Per-rank home limits. Give donors more homes than default players with LuckPerms groups, or set one server-wide limit.
  • -
- -

Quick start

- -
    -
  1. Drop the jar into your plugins folder and restart the server.
  2. -
  3. Run /sethome base where you are standing.
  4. -
  5. Run /homes and click it.
  6. -
- -

That is genuinely the whole setup. Since 1.1.0 the player-facing permissions default to granted, so your players can create and use homes the moment the plugin loads. You only need a permissions plugin if you want per-rank home limits.

- -

Managing homes

- -

The per-home management menu

- -

Open your homes with /homes, or right-click the homes item. Then:

- - - - - - - - - -
ActionWhat happens
Left-click a homeTeleports you there
Right-click a homeOpens the management menu below
RenameOpens an anvil prompt - type the new name
Move home hereRepoints the home at where you are standing
Set icon to held itemThe home's icon becomes whatever you are holding
DeleteAsks for confirmation first
- -

Right-clicking a home to rename it

- -

Home names are unique per player and ignore case, so base and Base are the same home. Management is controlled by sh2.manage-homes, which defaults to granted.

- -

Changing a home's icon works the same way - hold the item you want and click Set icon to held item:

- -

Changing a home's icon to the item being held

- -

Teleporting

- -

The stand-still countdown before a teleport

- -

By default players wait three seconds before a teleport fires, and moving cancels it - so a home is not a free escape from a fight. Set delay: 0 for instant teleports, or cancelOnMove: false to let players walk during the countdown.

- -

Instant teleport

- -

Before it drops anyone anywhere, Set Homes Two checks the destination is safe to stand in. If a home has been built over, flooded with lava, or left hanging above a drop, the player is moved to the nearest safe spot instead, or the teleport is cancelled and they are told why. Turn it off with teleportSafety: false.

- -

Coming from EssentialsX or Set Homes v1

- -

Your players keep their homes. The old plugin does not even need to be running - the importer reads its data files directly.

- -
    -
  • Run /import-homes essentialsx (or /import-homes sethomes). This is a preview only. It reports how many homes it would import and warns about any it would skip, and changes nothing.
  • -
  • Happy with the numbers? Run it again with confirm on the end.
  • -
  • Existing homes are never overwritten, so re-running it is always safe. Homes in worlds that no longer exist are skipped with a warning naming the world.
  • -
- -

Afterwards, remove the old jar. Set Homes Two provides /sethome, /home and /delhome, and two plugins claiming the same commands will fight over them.

- -

Commands

- - - - - - - - - -
CommandLong formWhat it does
/sethome <name> [item] [description]/create-homeCreates a home where you stand. The optional item becomes its icon.
/home <name>/go-homeTeleports you to a home.
/homes-Opens the homes menu.
/delhome <name>/delete-homeDeletes a home.
/list-homes-Lists your homes in chat. Click a name to teleport.
/give-homes-item-Gives you the item that opens the menu.
- -

Admin commands

- - - - - - - - -
CommandWhat it does
/set-max-homes [group] <number>Sets the home limit, per LuckPerms group or server-wide.
/get-player-homes <player>Lists another player's homes.
/add-to-blacklist <dimension...>Stops homes being set in a dimension.
/remove-from-blacklist <dimension...>Lifts the restriction again.
/get-blacklisted-dimensionsShows which dimensions are blacklisted.
/import-homes <sethomes|essentialsx> [confirm]Imports homes from another plugin. Dry-run unless confirm is given.
- -

Permissions

- -

Full permission list

- - - - - - - - - - - - - - - -
PermissionDefaultAllows
sh2.create-homeeveryoneCreating homes
sh2.go-homeeveryoneUsing the go-home command
sh2.teleporteveryoneActually teleporting to a home
sh2.list-homeseveryoneListing homes in chat, and /homes
sh2.delete-homeeveryoneDeleting your own homes
sh2.give-homes-itemeveryoneGetting the menu item
sh2.manage-homeseveryoneRenaming, moving, re-iconing and deleting from the GUI
sh2.set-max-homesOPSetting home limits
sh2.get-player-homesOPViewing another player's homes
sh2.add-to-blacklistOPBlacklisting a dimension
sh2.remove-from-blacklistOPUn-blacklisting a dimension
sh2.get-blacklisted-dimensionsOPListing blacklisted dimensions
sh2.import-homesOPImporting from another plugin
- -

Configuration

- -

Settings live in plugins/SetHomesTwo/config.yml on your server, written the first time the plugin starts. Edit it in any text editor, save, then restart the server - there is no in-game reload command, so changes do not apply until the server comes back up.

- -

The file is commented throughout, and every message the plugin sends can be rewritten in it. These are the settings most servers actually change:

- - - - - - - - - - - - -
SettingDefaultWhat it does
delay3Seconds you must stand still before teleporting. 0 is instant.
cancelOnMovetrueCancel the teleport if the player moves during the countdown.
teleportSafetytrueRelocate to the nearest safe spot instead of teleporting into danger.
maxHomeEnabledfalseTurn home limits on.
maxHomesTypegroupssingular for one server-wide limit, groups for per-rank limits.
openHomeItemcompassThe item players right-click to open the menu.
defaultHomeItemwhite_woolIcon used when a home is created without one.
inventoryTitleYour homesTitle of the homes menu.
maxHomeNameLength32Longest home name allowed.
- -

Per-rank limits need LuckPerms and maxHomesType: groups. The full annotated config is on GitHub.

- -

Upgrading? Your existing config.yml will not gain the new settings

-

Set Homes Two never touches a config.yml that already exists, so settings added in a later release do not appear in a file written by an earlier one. Any missing setting quietly falls back to its default, so nothing breaks - but you cannot change a setting you cannot see.

- -

To pick one up, copy the key you want out of the full config into your file and restart. To start clean, rename your config.yml and restart - a fresh one is written with everything in it, and you can copy your old values across.

- -

FAQ

- -

How do my players teleport home?

-

Three ways, all equivalent: /home <name>, opening /homes and left-clicking, or right-clicking the assigned "Home Item" from /give-homes-item.

- -

Only OPs can create homes. How do I let everyone in?

-

Update to 1.1.0 or later. On older versions every permission defaulted to OP; they now default to granted for players. If you use a permissions plugin that denies unlisted nodes, grant sh2.create-home, sh2.go-home and sh2.teleport.

- -

How do I give donors more homes than everyone else?

-

Install LuckPerms, set maxHomeEnabled: true and maxHomesType: groups, then run /set-max-homes <group> <number> for each rank.

- -

Can I run it alongside EssentialsX?

-

Not comfortably - both register /sethome, /home and /delhome, and whichever loads last wins. Import your homes, then remove EssentialsX.

- -

Where are homes stored?

-

In a SQLite database in plugins/SetHomesTwo/. Nothing external to install and nothing to configure.

- -

Requirements

- -
    -
  • Paper or Spigot 1.21+
  • -
  • Java 21, which Minecraft 1.21 servers already require
  • -
  • Optional: LuckPerms, only for per-rank home limits
  • -
- -

Support

- -

Found a bug or want a feature? Open an issue on GitHub - it gets seen faster than a comment on this page.

- -

Source | Report a bug | Donate

diff --git a/docs/generated/listing-bukkitdev.md b/docs/generated/listing-bukkitdev.md new file mode 100644 index 0000000..49bdd40 --- /dev/null +++ b/docs/generated/listing-bukkitdev.md @@ -0,0 +1,114 @@ +![Set Homes](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/logo.png) + +**Set Homes gives every player a menu of their homes.** Left-click one to teleport. Right-click it to rename it, move it, change its icon, or delete it. + +[Full documentation](https://github.com/Blockframe-Studios/SetHomesTwo#readme) | [Report a bug](https://github.com/Blockframe-Studios/SetHomesTwo/issues) | [Donate](https://www.paypal.com/donate/?business=8LXCRFX27B37C&no_recurring=0&item_name=Thanks+for+your+support.+It+helps+keep+this+plugin+up+to+date+%3A%29¤cy_code=USD) + +![The homes menu, with each home shown as its own item](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/homes-menu.png) + +## Why Set Homes + +- **A menu, or a list of commands.** Homes live in a chest-style GUI. Players open it with `/homes` or by right-clicking the configured "Home Item". +- **Every home gets its own icon.** Pick any Minecraft item when you create a home, or change it later to whatever you are holding. A base, a mine and a farm stop looking identical. +- **Rename, move and delete in-game.** Right-click any home to manage it. Deleting always asks first, so nobody loses a base to a misclick. +- **Teleports that do not kill you.** Set Homes checks the destination and relocates you to the nearest safe spot rather than dropping you into blocks, lava, or a fall. +- **Switch without losing anything.** One command imports every home from EssentialsX or Set Homes v1, and shows you exactly what it will do before it does it. +- **Per-rank home limits.** Give donors more homes than default players with LuckPerms groups, or set one server-wide limit. +- **Permissions you can change from the config.** Every `sh2.*` node has a sensible default, and any of them can be moved in `config.yml`. No permissions plugin required. + +## Quick start + +1. Drop the jar into your `plugins` folder and restart the server. +2. Run `/sethome base` where you are standing. +3. Run `/homes` and click it. + +That is genuinely the whole setup. Player permissions default to granted, so your players can create and use homes the moment the plugin loads. + +## Commands + +| Command | What it does | +| --- | --- | +| `/sethome [name]` | Creates a home where you stand. | +| `/home [name]` | Teleports you to a home. | +| `/homes` | Opens the homes menu. | +| `/delhome ` | Deletes a home. | +| `/uhome ` | Moves one of your homes to where you are standing. | + +Names are optional on `/sethome` and `/home`. Leave the name off and both use a home called `default`. + +Admins also get `/set-max-homes`, `/blacklist`, `/import-homes`, and commands to view, visit, move and delete other players' homes. The [full command list](https://github.com/Blockframe-Studios/SetHomesTwo#commands), with long forms and aliases, is on GitHub. + +## Managing homes + +![The per-home management menu](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/manage-menu.png) + +Open your homes with `/homes`, or right-click the homes item. Left-click a home to teleport, right-click it to rename it, move it to where you are standing, set its icon to the item you are holding, or delete it (with a confirmation). + +![Right-clicking a home to rename it](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/rename.gif) + +![Changing a home's icon to the item being held](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/change-icon.gif) + +## Teleporting + +By default players wait three seconds before a teleport fires, and moving cancels it, so a home is not a free escape from a fight. Set `delay: 0` for instant teleports, or `cancelOnMove: false` to let players walk during the countdown. + +Before it drops anyone anywhere, Set Homes checks the destination is safe to stand in. If a home has been built over, flooded with lava, or left hanging above a drop, the player is moved to the nearest safe spot instead. Turn it off with `teleportSafety: false`. + +## Permissions + +Nothing here needs a permissions plugin. Out of the box, every player can create, list, teleport to and manage their own homes, and operators get everything else. Two bundles cover it: + +| Bundle | Default | Contains | +| --- | --- | --- | +| `sh2.player` | everyone | Every node an ordinary player needs | +| `sh2.admin` | OP | `sh2.player`, plus every admin and bypass node | + +To change a default without a permissions plugin, uncomment the `permissions:` block in `config.yml`: + +```yaml +permissions: + sh2.manage-homes: false + sh2.get-player-homes: true + sh2.import-homes: op +``` + +Accepted values are `true`, `false`, `op` and `not-op`. If you run LuckPerms or similar, an explicit grant or deny there still wins. The [full permission list](https://github.com/Blockframe-Studios/SetHomesTwo#permissions) is on GitHub. + +## Configuration + +Settings live in `plugins/SetHomesTwo/config.yml`, written the first time the plugin starts. Edit it, save, then restart the server. The file is commented throughout, and every message the plugin sends can be rewritten in it. The settings most servers change: + +| Setting | Default | What it does | +| --- | --- | --- | +| `delay` | `3` | Seconds you must stand still before teleporting. `0` is instant. | +| `cancelOnMove` | `true` | Cancel the teleport if the player moves during the countdown. | +| `teleportSafety` | `true` | Relocate to the nearest safe spot instead of teleporting into danger. | +| `maxHomeEnabled` | `false` | Turn home limits on. | +| `maxHomesType` | `groups` | `singular` for one server-wide limit, `groups` for per-rank limits. | +| `openHomeItem` | `compass` | The item players right-click to open the menu. | + +Per-rank limits need [LuckPerms](https://luckperms.net/download). Everything else is in [`default-config.yml`](https://github.com/Blockframe-Studios/SetHomesTwo/blob/master/src/main/resources/default-config.yml), the file your `config.yml` is first written from. + +## Coming from EssentialsX or Set Homes v1 + +Your players keep their homes. The old plugin does not even need to be running, because the importer reads its data files directly. + +1. Run `/import-homes essentialsx` (or `/import-homes sethomes`). This is a **preview only**. It reports what it would import and skip, and changes nothing. +2. Happy with the numbers? Run it again with `confirm` on the end. +3. Remove the old jar. This plugin provides `/sethome`, `/home` and `/delhome`, and two plugins claiming the same commands will fight over them. + +Existing homes are never overwritten, so re-running the import is always safe. Coming from Set Homes v1, the world blacklist comes across too, and the [migration guide](https://github.com/Blockframe-Studios/SetHomesTwo#coming-from-essentialsx-or-set-homes-v1) on GitHub maps every v1 command, permission and config setting to its v2 equivalent. + +## Requirements + +- Paper or Spigot **1.21+** +- **Java 21**, which Minecraft 1.21 servers already require +- Optional: [LuckPerms](https://luckperms.net/download), only for per-rank home limits + +## Support + +The [README on GitHub](https://github.com/Blockframe-Studios/SetHomesTwo#readme) has the full command and permission lists, the migration tables, an FAQ and the changelog. + +Found a bug or want a feature? Open an issue on [GitHub](https://github.com/Blockframe-Studios/SetHomesTwo/issues). It gets seen faster than a comment on this page. + +[Source](https://github.com/Blockframe-Studios/SetHomesTwo) | [Report a bug](https://github.com/Blockframe-Studios/SetHomesTwo/issues) | [Donate](https://www.paypal.com/donate/?business=8LXCRFX27B37C&no_recurring=0&item_name=Thanks+for+your+support.+It+helps+keep+this+plugin+up+to+date+%3A%29¤cy_code=USD) diff --git a/docs/generated/listing-modrinth.md b/docs/generated/listing-modrinth.md index d13571c..5020336 100644 --- a/docs/generated/listing-modrinth.md +++ b/docs/generated/listing-modrinth.md @@ -1,19 +1,20 @@ -![Set Homes Two](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/master/docs/img/logo.png) +![Set Homes](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/logo.png) -**Set Homes Two gives every player a menu of their homes.** Left-click one to teleport. Right-click it to rename it, move it, change its icon, or delete it. +**Set Homes gives every player a menu of their homes.** Left-click one to teleport. Right-click it to rename it, move it, change its icon, or delete it. -[Source](https://github.com/Blockframe-Studios/SetHomesTwo) | [Report a bug](https://github.com/Blockframe-Studios/SetHomesTwo/issues) | [Donate](https://www.paypal.com/donate/?business=8LXCRFX27B37C&no_recurring=0&item_name=Thanks+for+your+support.+It+helps+keep+this+plugin+up+to+date+%3A%29¤cy_code=USD) +[Full documentation](https://github.com/Blockframe-Studios/SetHomesTwo#readme) | [Report a bug](https://github.com/Blockframe-Studios/SetHomesTwo/issues) | [Donate](https://www.paypal.com/donate/?business=8LXCRFX27B37C&no_recurring=0&item_name=Thanks+for+your+support.+It+helps+keep+this+plugin+up+to+date+%3A%29¤cy_code=USD) -![The homes menu, with each home shown as its own item](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/master/docs/img/homes-menu.png) +![The homes menu, with each home shown as its own item](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/homes-menu.png) -## Why Set Homes Two +## Why Set Homes - **A menu, or a list of commands.** Homes live in a chest-style GUI. Players open it with `/homes` or by right-clicking the configured "Home Item". - **Every home gets its own icon.** Pick any Minecraft item when you create a home, or change it later to whatever you are holding. A base, a mine and a farm stop looking identical. - **Rename, move and delete in-game.** Right-click any home to manage it. Deleting always asks first, so nobody loses a base to a misclick. -- **Teleports that do not kill you.** Set Homes Two checks the destination and relocates you to the nearest safe spot rather than dropping you into blocks, lava, or a fall. -- **Switch without losing anything.** One command imports every home from EssentialsX or Set Homes v1, and shows you exactly what it will do before it does it. +- **Teleports that do not kill you.** Set Homes checks the destination and relocates you to the nearest safe spot rather than dropping you into blocks, lava, or a fall. +- **Switch without losing anything.** `/import-homes essentialsx` brings every EssentialsX home across, and previews exactly what it will do before it does it. Then remove EssentialsX, since both plugins claim `/sethome` and `/home`. - **Per-rank home limits.** Give donors more homes than default players with LuckPerms groups, or set one server-wide limit. +- **Permissions you can change from the config.** Every `sh2.*` node has a sensible default, and any of them can be moved in `config.yml`. No permissions plugin required. ## Quick start @@ -21,104 +22,61 @@ 2. Run `/sethome base` where you are standing. 3. Run `/homes` and click it. -That is genuinely the whole setup. Since 1.1.0 the player-facing permissions default to granted, so your players can create and use homes the moment the plugin loads. You only need a permissions plugin if you want per-rank home limits. - -## Managing homes - -![The per-home management menu](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/master/docs/img/manage-menu.png) +That is genuinely the whole setup. Player permissions default to granted, so your players can create and use homes the moment the plugin loads. -Open your homes with `/homes`, or right-click the homes item. Then: +## Commands -| Action | What happens | +| Command | What it does | | --- | --- | -| Left-click a home | Teleports you there | -| Right-click a home | Opens the management menu below | -| Rename | Opens an anvil prompt - type the new name | -| Move home here | Repoints the home at where you are standing | -| Set icon to held item | The home's icon becomes whatever you are holding | -| Delete | Asks for confirmation first | +| `/sethome [name]` | Creates a home where you stand. | +| `/home [name]` | Teleports you to a home. | +| `/homes` | Opens the homes menu. | +| `/delhome ` | Deletes a home. | +| `/uhome ` | Moves one of your homes to where you are standing. | -![Right-clicking a home to rename it](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/master/docs/img/rename.gif) +Names are optional on `/sethome` and `/home`. Leave the name off and both use a home called `default`. -Home names are unique per player and ignore case, so `base` and `Base` are the same home. Management is controlled by `sh2.manage-homes`, which defaults to granted. +Admins also get `/set-max-homes`, `/blacklist`, `/import-homes`, and commands to view, visit, move and delete other players' homes. The [full command list](https://github.com/Blockframe-Studios/SetHomesTwo#commands), with long forms and aliases, is on GitHub. -Changing a home's icon works the same way - hold the item you want and click **Set icon to held item**: - -![Changing a home's icon to the item being held](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/master/docs/img/change-icon.gif) - -## Teleporting +## Managing homes -![The stand-still countdown before a teleport](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/master/docs/img/teleport-delay.gif) +![The per-home management menu](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/manage-menu.png) -By default players wait three seconds before a teleport fires, and moving cancels it - so a home is not a free escape from a fight. Set `delay: 0` for instant teleports, or `cancelOnMove: false` to let players walk during the countdown. +Open your homes with `/homes`, or right-click the homes item. Left-click a home to teleport, right-click it to rename it, move it to where you are standing, set its icon to the item you are holding, or delete it (with a confirmation). -![Instant teleport](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/master/docs/img/teleport-instant.gif) +![Right-clicking a home to rename it](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/rename.gif) -Before it drops anyone anywhere, Set Homes Two checks the destination is safe to stand in. If a home has been built over, flooded with lava, or left hanging above a drop, the player is moved to the nearest safe spot instead, or the teleport is cancelled and they are told why. Turn it off with `teleportSafety: false`. +![Changing a home's icon to the item being held](https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/change-icon.gif) -## Coming from EssentialsX or Set Homes v1 +## Teleporting -Your players keep their homes. The old plugin does not even need to be running - the importer reads its data files directly. +By default players wait three seconds before a teleport fires, and moving cancels it, so a home is not a free escape from a fight. Set `delay: 0` for instant teleports, or `cancelOnMove: false` to let players walk during the countdown. -- Run `/import-homes essentialsx` (or `/import-homes sethomes`). This is a **preview only**. It reports how many homes it would import and warns about any it would skip, and changes nothing. -- Happy with the numbers? Run it again with `confirm` on the end. -- Existing homes are never overwritten, so re-running it is always safe. Homes in worlds that no longer exist are skipped with a warning naming the world. +Before it drops anyone anywhere, Set Homes checks the destination is safe to stand in. If a home has been built over, flooded with lava, or left hanging above a drop, the player is moved to the nearest safe spot instead. Turn it off with `teleportSafety: false`. -Afterwards, remove the old jar. Set Homes Two provides `/sethome`, `/home` and `/delhome`, and two plugins claiming the same commands will fight over them. +## Permissions -## Commands +Nothing here needs a permissions plugin. Out of the box, every player can create, list, teleport to and manage their own homes, and operators get everything else. Two bundles cover it: -| Command | Long form | What it does | +| Bundle | Default | Contains | | --- | --- | --- | -| `/sethome [item] [description]` | `/create-home` | Creates a home where you stand. The optional item becomes its icon. | -| `/home ` | `/go-home` | Teleports you to a home. | -| `/homes` | - | Opens the homes menu. | -| `/delhome ` | `/delete-home` | Deletes a home. | -| `/list-homes` | - | Lists your homes in chat. Click a name to teleport. | -| `/give-homes-item` | - | Gives you the item that opens the menu. | - -
-Admin commands - -| Command | What it does | -| --- | --- | -| `/set-max-homes [group] ` | Sets the home limit, per LuckPerms group or server-wide. | -| `/get-player-homes ` | Lists another player's homes. | -| `/add-to-blacklist ` | Stops homes being set in a dimension. | -| `/remove-from-blacklist ` | Lifts the restriction again. | -| `/get-blacklisted-dimensions` | Shows which dimensions are blacklisted. | -| `/import-homes [confirm]` | Imports homes from another plugin. Dry-run unless `confirm` is given. | +| `sh2.player` | everyone | Every node an ordinary player needs | +| `sh2.admin` | OP | `sh2.player`, plus every admin and bypass node | -
+To change a default without a permissions plugin, uncomment the `permissions:` block in `config.yml`: -## Permissions +```yaml +permissions: + sh2.manage-homes: false + sh2.get-player-homes: true + sh2.import-homes: op +``` -
-Full permission list - -| Permission | Default | Allows | -| --- | --- | --- | -| `sh2.create-home` | everyone | Creating homes | -| `sh2.go-home` | everyone | Using the go-home command | -| `sh2.teleport` | everyone | Actually teleporting to a home | -| `sh2.list-homes` | everyone | Listing homes in chat, and `/homes` | -| `sh2.delete-home` | everyone | Deleting your own homes | -| `sh2.give-homes-item` | everyone | Getting the menu item | -| `sh2.manage-homes` | everyone | Renaming, moving, re-iconing and deleting from the GUI | -| `sh2.set-max-homes` | OP | Setting home limits | -| `sh2.get-player-homes` | OP | Viewing another player's homes | -| `sh2.add-to-blacklist` | OP | Blacklisting a dimension | -| `sh2.remove-from-blacklist` | OP | Un-blacklisting a dimension | -| `sh2.get-blacklisted-dimensions` | OP | Listing blacklisted dimensions | -| `sh2.import-homes` | OP | Importing from another plugin | - -
+Accepted values are `true`, `false`, `op` and `not-op`. If you run LuckPerms or similar, an explicit grant or deny there still wins. The [full permission list](https://github.com/Blockframe-Studios/SetHomesTwo#permissions) is on GitHub. ## Configuration -Settings live in **`plugins/SetHomesTwo/config.yml`** on your server, written the first time the plugin starts. Edit it in any text editor, save, then **restart the server** - there is no in-game reload command, so changes do not apply until the server comes back up. - -The file is commented throughout, and every message the plugin sends can be rewritten in it. These are the settings most servers actually change: +Settings live in `plugins/SetHomesTwo/config.yml`, written the first time the plugin starts. Edit it, save, then restart the server. The file is commented throughout, and every message the plugin sends can be rewritten in it. The settings most servers change: | Setting | Default | What it does | | --- | --- | --- | @@ -128,57 +86,8 @@ The file is commented throughout, and every message the plugin sends can be rewr | `maxHomeEnabled` | `false` | Turn home limits on. | | `maxHomesType` | `groups` | `singular` for one server-wide limit, `groups` for per-rank limits. | | `openHomeItem` | `compass` | The item players right-click to open the menu. | -| `defaultHomeItem` | `white_wool` | Icon used when a home is created without one. | -| `inventoryTitle` | `Your homes` | Title of the homes menu. | -| `maxHomeNameLength` | `32` | Longest home name allowed. | -Per-rank limits need [LuckPerms](https://luckperms.net/download) and `maxHomesType: groups`. The [full annotated config](https://github.com/Blockframe-Studios/SetHomesTwo#example-config) is on GitHub. - -
-Upgrading? Your existing config.yml will not gain the new settings - -Set Homes Two never touches a `config.yml` that already exists, so settings added in a later release do not appear in a file written by an earlier one. Any missing setting quietly falls back to its default, so nothing breaks - but you cannot change a setting you cannot see. - -To pick one up, copy the key you want out of the [full config](https://github.com/Blockframe-Studios/SetHomesTwo#example-config) into your file and restart. To start clean, rename your `config.yml` and restart - a fresh one is written with everything in it, and you can copy your old values across. - -
- -## FAQ - -
-How do my players teleport home? - -Three ways, all equivalent: `/home `, opening `/homes` and left-clicking, or right-clicking the assigned "Home Item" from `/give-homes-item`. - -
- -
-Only OPs can create homes. How do I let everyone in? - -Update to 1.1.0 or later. On older versions every permission defaulted to OP; they now default to granted for players. If you use a permissions plugin that denies unlisted nodes, grant `sh2.create-home`, `sh2.go-home` and `sh2.teleport`. - -
- -
-How do I give donors more homes than everyone else? - -Install LuckPerms, set `maxHomeEnabled: true` and `maxHomesType: groups`, then run `/set-max-homes ` for each rank. - -
- -
-Can I run it alongside EssentialsX? - -Not comfortably - both register `/sethome`, `/home` and `/delhome`, and whichever loads last wins. Import your homes, then remove EssentialsX. - -
- -
-Where are homes stored? - -In a SQLite database in `plugins/SetHomesTwo/`. Nothing external to install and nothing to configure. - -
+Per-rank limits need [LuckPerms](https://luckperms.net/download). Everything else is in [`default-config.yml`](https://github.com/Blockframe-Studios/SetHomesTwo/blob/master/src/main/resources/default-config.yml), the file your `config.yml` is first written from. ## Requirements @@ -188,6 +97,8 @@ In a SQLite database in `plugins/SetHomesTwo/`. Nothing external to install and ## Support -Found a bug or want a feature? Open an issue on [GitHub](https://github.com/Blockframe-Studios/SetHomesTwo/issues) - it gets seen faster than a comment on this page. +The [README on GitHub](https://github.com/Blockframe-Studios/SetHomesTwo#readme) has the full command and permission lists, the EssentialsX import guide, an FAQ and the changelog. + +Found a bug or want a feature? Open an issue on [GitHub](https://github.com/Blockframe-Studios/SetHomesTwo/issues). It gets seen faster than a comment on this page. [Source](https://github.com/Blockframe-Studios/SetHomesTwo) | [Report a bug](https://github.com/Blockframe-Studios/SetHomesTwo/issues) | [Donate](https://www.paypal.com/donate/?business=8LXCRFX27B37C&no_recurring=0&item_name=Thanks+for+your+support.+It+helps+keep+this+plugin+up+to+date+%3A%29¤cy_code=USD) diff --git a/docs/generated/listing-spigotmc.bbcode b/docs/generated/listing-spigotmc.bbcode index 8290acd..e7237fe 100644 --- a/docs/generated/listing-spigotmc.bbcode +++ b/docs/generated/listing-spigotmc.bbcode @@ -1,20 +1,21 @@ -[IMG]https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/master/docs/img/logo.png[/IMG] +[IMG]https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/logo.png[/IMG] -[B]Set Homes Two gives every player a menu of their homes.[/B] Left-click one to teleport. Right-click it to rename it, move it, change its icon, or delete it. +[B]Set Homes gives every player a menu of their homes.[/B] Left-click one to teleport. Right-click it to rename it, move it, change its icon, or delete it. -[URL=https://github.com/Blockframe-Studios/SetHomesTwo]Source[/URL] | [URL=https://github.com/Blockframe-Studios/SetHomesTwo/issues]Report a bug[/URL] | [URL=https://www.paypal.com/donate/?business=8LXCRFX27B37C&no_recurring=0&item_name=Thanks+for+your+support.+It+helps+keep+this+plugin+up+to+date+%3A%29¤cy_code=USD]Donate[/URL] +[URL=https://github.com/Blockframe-Studios/SetHomesTwo#readme]Full documentation[/URL] | [URL=https://github.com/Blockframe-Studios/SetHomesTwo/issues]Report a bug[/URL] | [URL=https://www.paypal.com/donate/?business=8LXCRFX27B37C&no_recurring=0&item_name=Thanks+for+your+support.+It+helps+keep+this+plugin+up+to+date+%3A%29¤cy_code=USD]Donate[/URL] -[IMG]https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/master/docs/img/homes-menu.png[/IMG] +[IMG]https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/homes-menu.png[/IMG] -[SIZE=5][B]Why Set Homes Two[/B][/SIZE] +[SIZE=5][B]Why Set Homes[/B][/SIZE] [LIST] [*][B]A menu, or a list of commands.[/B] Homes live in a chest-style GUI. Players open it with /homes or by right-clicking the configured "Home Item". [*][B]Every home gets its own icon.[/B] Pick any Minecraft item when you create a home, or change it later to whatever you are holding. A base, a mine and a farm stop looking identical. [*][B]Rename, move and delete in-game.[/B] Right-click any home to manage it. Deleting always asks first, so nobody loses a base to a misclick. -[*][B]Teleports that do not kill you.[/B] Set Homes Two checks the destination and relocates you to the nearest safe spot rather than dropping you into blocks, lava, or a fall. -[*][B]Switch without losing anything.[/B] One command imports every home from EssentialsX or Set Homes v1, and shows you exactly what it will do before it does it. +[*][B]Teleports that do not kill you.[/B] Set Homes checks the destination and relocates you to the nearest safe spot rather than dropping you into blocks, lava, or a fall. +[*][B]Switch without losing anything.[/B] /import-homes essentialsx brings every EssentialsX home across, and previews exactly what it will do before it does it. Then remove EssentialsX, since both plugins claim /sethome and /home. [*][B]Per-rank home limits.[/B] Give donors more homes than default players with LuckPerms groups, or set one server-wide limit. +[*][B]Permissions you can change from the config.[/B] Every sh2.* node has a sensible default, and any of them can be moved in config.yml. No permissions plugin required. [/LIST] [SIZE=5][B]Quick start[/B][/SIZE] @@ -25,104 +26,63 @@ [*]Run /homes and click it. [/LIST] -That is genuinely the whole setup. Since 1.1.0 the player-facing permissions default to granted, so your players can create and use homes the moment the plugin loads. You only need a permissions plugin if you want per-rank home limits. - -[SIZE=5][B]Managing homes[/B][/SIZE] - -[IMG]https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/master/docs/img/manage-menu.png[/IMG] +That is genuinely the whole setup. Player permissions default to granted, so your players can create and use homes the moment the plugin loads. -Open your homes with /homes, or right-click the homes item. Then: +[SIZE=5][B]Commands[/B][/SIZE] [TABLE] -[TR][TH]Action[/TH][TH]What happens[/TH][/TR] -[TR][TD]Left-click a home[/TD][TD]Teleports you there[/TD][/TR] -[TR][TD]Right-click a home[/TD][TD]Opens the management menu below[/TD][/TR] -[TR][TD]Rename[/TD][TD]Opens an anvil prompt - type the new name[/TD][/TR] -[TR][TD]Move home here[/TD][TD]Repoints the home at where you are standing[/TD][/TR] -[TR][TD]Set icon to held item[/TD][TD]The home's icon becomes whatever you are holding[/TD][/TR] -[TR][TD]Delete[/TD][TD]Asks for confirmation first[/TD][/TR] +[TR][TH]Command[/TH][TH]What it does[/TH][/TR] +[TR][TD]/sethome [name][/TD][TD]Creates a home where you stand.[/TD][/TR] +[TR][TD]/home [name][/TD][TD]Teleports you to a home.[/TD][/TR] +[TR][TD]/homes[/TD][TD]Opens the homes menu.[/TD][/TR] +[TR][TD]/delhome [/TD][TD]Deletes a home.[/TD][/TR] +[TR][TD]/uhome [/TD][TD]Moves one of your homes to where you are standing.[/TD][/TR] [/TABLE] -[IMG]https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/master/docs/img/rename.gif[/IMG] - -Home names are unique per player and ignore case, so base and Base are the same home. Management is controlled by sh2.manage-homes, which defaults to granted. - -Changing a home's icon works the same way - hold the item you want and click [B]Set icon to held item[/B]: +Names are optional on /sethome and /home. Leave the name off and both use a home called default. -[IMG]https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/master/docs/img/change-icon.gif[/IMG] +Admins also get /set-max-homes, /blacklist, /import-homes, and commands to view, visit, move and delete other players' homes. The [URL=https://github.com/Blockframe-Studios/SetHomesTwo#commands]full command list[/URL], with long forms and aliases, is on GitHub. -[SIZE=5][B]Teleporting[/B][/SIZE] +[SIZE=5][B]Managing homes[/B][/SIZE] -[IMG]https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/master/docs/img/teleport-delay.gif[/IMG] +[IMG]https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/manage-menu.png[/IMG] -By default players wait three seconds before a teleport fires, and moving cancels it - so a home is not a free escape from a fight. Set delay: 0 for instant teleports, or cancelOnMove: false to let players walk during the countdown. +Open your homes with /homes, or right-click the homes item. Left-click a home to teleport, right-click it to rename it, move it to where you are standing, set its icon to the item you are holding, or delete it (with a confirmation). -[IMG]https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/master/docs/img/teleport-instant.gif[/IMG] +[IMG]https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/rename.gif[/IMG] -Before it drops anyone anywhere, Set Homes Two checks the destination is safe to stand in. If a home has been built over, flooded with lava, or left hanging above a drop, the player is moved to the nearest safe spot instead, or the teleport is cancelled and they are told why. Turn it off with teleportSafety: false. +[IMG]https://raw.githubusercontent.com/Blockframe-Studios/SetHomesTwo/dev/docs/img/change-icon.gif[/IMG] -[SIZE=5][B]Coming from EssentialsX or Set Homes v1[/B][/SIZE] +[SIZE=5][B]Teleporting[/B][/SIZE] -Your players keep their homes. The old plugin does not even need to be running - the importer reads its data files directly. +By default players wait three seconds before a teleport fires, and moving cancels it, so a home is not a free escape from a fight. Set delay: 0 for instant teleports, or cancelOnMove: false to let players walk during the countdown. -[LIST] -[*]Run /import-homes essentialsx (or /import-homes sethomes). This is a [B]preview only[/B]. It reports how many homes it would import and warns about any it would skip, and changes nothing. -[*]Happy with the numbers? Run it again with confirm on the end. -[*]Existing homes are never overwritten, so re-running it is always safe. Homes in worlds that no longer exist are skipped with a warning naming the world. -[/LIST] +Before it drops anyone anywhere, Set Homes checks the destination is safe to stand in. If a home has been built over, flooded with lava, or left hanging above a drop, the player is moved to the nearest safe spot instead. Turn it off with teleportSafety: false. -Afterwards, remove the old jar. Set Homes Two provides /sethome, /home and /delhome, and two plugins claiming the same commands will fight over them. +[SIZE=5][B]Permissions[/B][/SIZE] -[SIZE=5][B]Commands[/B][/SIZE] +Nothing here needs a permissions plugin. Out of the box, every player can create, list, teleport to and manage their own homes, and operators get everything else. Two bundles cover it: [TABLE] -[TR][TH]Command[/TH][TH]Long form[/TH][TH]What it does[/TH][/TR] -[TR][TD]/sethome [item] [description][/TD][TD]/create-home[/TD][TD]Creates a home where you stand. The optional item becomes its icon.[/TD][/TR] -[TR][TD]/home [/TD][TD]/go-home[/TD][TD]Teleports you to a home.[/TD][/TR] -[TR][TD]/homes[/TD][TD]-[/TD][TD]Opens the homes menu.[/TD][/TR] -[TR][TD]/delhome [/TD][TD]/delete-home[/TD][TD]Deletes a home.[/TD][/TR] -[TR][TD]/list-homes[/TD][TD]-[/TD][TD]Lists your homes in chat. Click a name to teleport.[/TD][/TR] -[TR][TD]/give-homes-item[/TD][TD]-[/TD][TD]Gives you the item that opens the menu.[/TD][/TR] +[TR][TH]Bundle[/TH][TH]Default[/TH][TH]Contains[/TH][/TR] +[TR][TD]sh2.player[/TD][TD]everyone[/TD][TD]Every node an ordinary player needs[/TD][/TR] +[TR][TD]sh2.admin[/TD][TD]OP[/TD][TD]sh2.player, plus every admin and bypass node[/TD][/TR] [/TABLE] -[SPOILER=Admin commands] -[TABLE] -[TR][TH]Command[/TH][TH]What it does[/TH][/TR] -[TR][TD]/set-max-homes [group] [/TD][TD]Sets the home limit, per LuckPerms group or server-wide.[/TD][/TR] -[TR][TD]/get-player-homes [/TD][TD]Lists another player's homes.[/TD][/TR] -[TR][TD]/add-to-blacklist [/TD][TD]Stops homes being set in a dimension.[/TD][/TR] -[TR][TD]/remove-from-blacklist [/TD][TD]Lifts the restriction again.[/TD][/TR] -[TR][TD]/get-blacklisted-dimensions[/TD][TD]Shows which dimensions are blacklisted.[/TD][/TR] -[TR][TD]/import-homes [confirm][/TD][TD]Imports homes from another plugin. Dry-run unless confirm is given.[/TD][/TR] -[/TABLE] -[/SPOILER] +To change a default without a permissions plugin, uncomment the permissions: block in config.yml: -[SIZE=5][B]Permissions[/B][/SIZE] +[CODE] +permissions: + sh2.manage-homes: false + sh2.get-player-homes: true + sh2.import-homes: op +[/CODE] -[SPOILER=Full permission list] -[TABLE] -[TR][TH]Permission[/TH][TH]Default[/TH][TH]Allows[/TH][/TR] -[TR][TD]sh2.create-home[/TD][TD]everyone[/TD][TD]Creating homes[/TD][/TR] -[TR][TD]sh2.go-home[/TD][TD]everyone[/TD][TD]Using the go-home command[/TD][/TR] -[TR][TD]sh2.teleport[/TD][TD]everyone[/TD][TD]Actually teleporting to a home[/TD][/TR] -[TR][TD]sh2.list-homes[/TD][TD]everyone[/TD][TD]Listing homes in chat, and /homes[/TD][/TR] -[TR][TD]sh2.delete-home[/TD][TD]everyone[/TD][TD]Deleting your own homes[/TD][/TR] -[TR][TD]sh2.give-homes-item[/TD][TD]everyone[/TD][TD]Getting the menu item[/TD][/TR] -[TR][TD]sh2.manage-homes[/TD][TD]everyone[/TD][TD]Renaming, moving, re-iconing and deleting from the GUI[/TD][/TR] -[TR][TD]sh2.set-max-homes[/TD][TD]OP[/TD][TD]Setting home limits[/TD][/TR] -[TR][TD]sh2.get-player-homes[/TD][TD]OP[/TD][TD]Viewing another player's homes[/TD][/TR] -[TR][TD]sh2.add-to-blacklist[/TD][TD]OP[/TD][TD]Blacklisting a dimension[/TD][/TR] -[TR][TD]sh2.remove-from-blacklist[/TD][TD]OP[/TD][TD]Un-blacklisting a dimension[/TD][/TR] -[TR][TD]sh2.get-blacklisted-dimensions[/TD][TD]OP[/TD][TD]Listing blacklisted dimensions[/TD][/TR] -[TR][TD]sh2.import-homes[/TD][TD]OP[/TD][TD]Importing from another plugin[/TD][/TR] -[/TABLE] -[/SPOILER] +Accepted values are true, false, op and not-op. If you run LuckPerms or similar, an explicit grant or deny there still wins. The [URL=https://github.com/Blockframe-Studios/SetHomesTwo#permissions]full permission list[/URL] is on GitHub. [SIZE=5][B]Configuration[/B][/SIZE] -Settings live in [B]plugins/SetHomesTwo/config.yml[/B] on your server, written the first time the plugin starts. Edit it in any text editor, save, then [B]restart the server[/B] - there is no in-game reload command, so changes do not apply until the server comes back up. - -The file is commented throughout, and every message the plugin sends can be rewritten in it. These are the settings most servers actually change: +Settings live in plugins/SetHomesTwo/config.yml, written the first time the plugin starts. Edit it, save, then restart the server. The file is commented throughout, and every message the plugin sends can be rewritten in it. The settings most servers change: [TABLE] [TR][TH]Setting[/TH][TH]Default[/TH][TH]What it does[/TH][/TR] @@ -132,40 +92,9 @@ The file is commented throughout, and every message the plugin sends can be rewr [TR][TD]maxHomeEnabled[/TD][TD]false[/TD][TD]Turn home limits on.[/TD][/TR] [TR][TD]maxHomesType[/TD][TD]groups[/TD][TD]singular for one server-wide limit, groups for per-rank limits.[/TD][/TR] [TR][TD]openHomeItem[/TD][TD]compass[/TD][TD]The item players right-click to open the menu.[/TD][/TR] -[TR][TD]defaultHomeItem[/TD][TD]white_wool[/TD][TD]Icon used when a home is created without one.[/TD][/TR] -[TR][TD]inventoryTitle[/TD][TD]Your homes[/TD][TD]Title of the homes menu.[/TD][/TR] -[TR][TD]maxHomeNameLength[/TD][TD]32[/TD][TD]Longest home name allowed.[/TD][/TR] [/TABLE] -Per-rank limits need [URL=https://luckperms.net/download]LuckPerms[/URL] and maxHomesType: groups. The [URL=https://github.com/Blockframe-Studios/SetHomesTwo#example-config]full annotated config[/URL] is on GitHub. - -[SPOILER=Upgrading? Your existing config.yml will not gain the new settings] -Set Homes Two never touches a config.yml that already exists, so settings added in a later release do not appear in a file written by an earlier one. Any missing setting quietly falls back to its default, so nothing breaks - but you cannot change a setting you cannot see. - -To pick one up, copy the key you want out of the [URL=https://github.com/Blockframe-Studios/SetHomesTwo#example-config]full config[/URL] into your file and restart. To start clean, rename your config.yml and restart - a fresh one is written with everything in it, and you can copy your old values across. -[/SPOILER] - -[SIZE=5][B]FAQ[/B][/SIZE] - -[SPOILER=How do my players teleport home?] -Three ways, all equivalent: /home , opening /homes and left-clicking, or right-clicking the assigned "Home Item" from /give-homes-item. -[/SPOILER] - -[SPOILER=Only OPs can create homes. How do I let everyone in?] -Update to 1.1.0 or later. On older versions every permission defaulted to OP; they now default to granted for players. If you use a permissions plugin that denies unlisted nodes, grant sh2.create-home, sh2.go-home and sh2.teleport. -[/SPOILER] - -[SPOILER=How do I give donors more homes than everyone else?] -Install LuckPerms, set maxHomeEnabled: true and maxHomesType: groups, then run /set-max-homes for each rank. -[/SPOILER] - -[SPOILER=Can I run it alongside EssentialsX?] -Not comfortably - both register /sethome, /home and /delhome, and whichever loads last wins. Import your homes, then remove EssentialsX. -[/SPOILER] - -[SPOILER=Where are homes stored?] -In a SQLite database in plugins/SetHomesTwo/. Nothing external to install and nothing to configure. -[/SPOILER] +Per-rank limits need [URL=https://luckperms.net/download]LuckPerms[/URL]. Everything else is in [URL=https://github.com/Blockframe-Studios/SetHomesTwo/blob/master/src/main/resources/default-config.yml]default-config.yml[/URL], the file your config.yml is first written from. [SIZE=5][B]Requirements[/B][/SIZE] @@ -177,6 +106,8 @@ In a SQLite database in plugins/SetHomesTwo/. Nothing external to install and no [SIZE=5][B]Support[/B][/SIZE] -Found a bug or want a feature? Open an issue on [URL=https://github.com/Blockframe-Studios/SetHomesTwo/issues]GitHub[/URL] - it gets seen faster than a comment on this page. +The [URL=https://github.com/Blockframe-Studios/SetHomesTwo#readme]README on GitHub[/URL] has the full command and permission lists, the EssentialsX import guide, an FAQ and the changelog. + +Found a bug or want a feature? Open an issue on [URL=https://github.com/Blockframe-Studios/SetHomesTwo/issues]GitHub[/URL]. It gets seen faster than a comment on this page. [URL=https://github.com/Blockframe-Studios/SetHomesTwo]Source[/URL] | [URL=https://github.com/Blockframe-Studios/SetHomesTwo/issues]Report a bug[/URL] | [URL=https://www.paypal.com/donate/?business=8LXCRFX27B37C&no_recurring=0&item_name=Thanks+for+your+support.+It+helps+keep+this+plugin+up+to+date+%3A%29¤cy_code=USD]Donate[/URL] From 00cdb398977a4f3bde546bc23a67b59a52486114 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 16:48:06 -0400 Subject: [PATCH 53/75] fix: keep v1 colour codes out of the import report and unify the no-homes notice Closes #49 --- .changeset/tidy-tigers-wonder.md | 5 +++ .../sethomestwo/commands/ListHomes.java | 4 +- .../sethomestwo/commands/OpenHomesGui.java | 3 +- .../sethomestwo/enums/UserError.java | 1 - .../sethomestwo/enums/UserInfo.java | 2 +- .../samleighton/sethomestwo/gui/HomesGui.java | 4 +- .../importers/SetHomesV1Importer.java | 18 ++++++-- .../sethomestwo/commands/ListHomesTest.java | 25 +++++++++++ .../commands/OpenHomesGuiTest.java | 6 ++- .../sethomestwo/gui/HomesGuiClickTest.java | 5 ++- .../importers/SetHomesV1ImporterTest.java | 44 ++++++++++++++++++- 11 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 .changeset/tidy-tigers-wonder.md create mode 100644 src/test/java/com/samleighton/sethomestwo/commands/ListHomesTest.java diff --git a/.changeset/tidy-tigers-wonder.md b/.changeset/tidy-tigers-wonder.md new file mode 100644 index 0000000..2d2eedf --- /dev/null +++ b/.changeset/tidy-tigers-wonder.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +The Set Homes v1 import report no longer applies the colour codes found in v1 messages, so a coloured v1 message no longer turns the rest of the advice line red or unreadable. Codes are shown as an ampersand instead, and the note says where to copy the exact original from. Also, the "you have no homes yet" notice now uses the same wording and is shown as plain information rather than red from /homes, /list-homes and the homes menu alike. diff --git a/src/main/java/com/samleighton/sethomestwo/commands/ListHomes.java b/src/main/java/com/samleighton/sethomestwo/commands/ListHomes.java index 39479ec..f1ad07c 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/ListHomes.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/ListHomes.java @@ -4,6 +4,7 @@ import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.enums.UserError; import com.samleighton.sethomestwo.enums.UserInfo; +import com.samleighton.sethomestwo.utils.ConfigUtil; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.utils.ChatUtils; import net.md_5.bungee.api.chat.ClickEvent; @@ -46,8 +47,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command // Player has no homes guard if (playersHomes.isEmpty()) { - ChatUtils.sendInfo(player, UserInfo.NO_HOMES.getValue()); - ChatUtils.sendInfo(player, UserInfo.CREATE_HOME_USAGE.getValue()); + ChatUtils.sendInfo(player, ConfigUtil.getConfig().getString("noHomes", UserInfo.NO_HOMES.getValue())); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/commands/OpenHomesGui.java b/src/main/java/com/samleighton/sethomestwo/commands/OpenHomesGui.java index 3aaaf6e..9bc9a95 100644 --- a/src/main/java/com/samleighton/sethomestwo/commands/OpenHomesGui.java +++ b/src/main/java/com/samleighton/sethomestwo/commands/OpenHomesGui.java @@ -4,6 +4,7 @@ import com.samleighton.sethomestwo.dao.Dao; import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.enums.UserError; +import com.samleighton.sethomestwo.enums.UserInfo; import com.samleighton.sethomestwo.gui.GuiSession; import com.samleighton.sethomestwo.gui.HomesGui; import com.samleighton.sethomestwo.models.Home; @@ -46,7 +47,7 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command // Guard for no homes yet if (playersHomes == null || playersHomes.isEmpty()) { - ChatUtils.sendInfo(player, ConfigUtil.getConfig().getString("noHomes", UserError.NO_HOMES.getValue())); + ChatUtils.sendInfo(player, ConfigUtil.getConfig().getString("noHomes", UserInfo.NO_HOMES.getValue())); return true; } diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserError.java b/src/main/java/com/samleighton/sethomestwo/enums/UserError.java index f86d955..b07ea0f 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserError.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserError.java @@ -22,7 +22,6 @@ public enum UserError { DELETE_HOME_USAGE("Usage: /%s [name]"), INVALID_MATERIAL("The material you entered is not valid, please try a different one."), PLAYER_NOT_FOUND("No player by that name is online or has any saved homes."), - NO_HOMES("You have not created any homes yet. Use /create-home."), PLAYERS_ONLY("Only players may execute this command."), DIMENSION_ALREADY_BLACKLISTED("The %s dimension has already been blacklisted. You cannot add it again."), GROUP_DOES_NOT_EXIST("Group does not exist. Use /get-max-homes-groups to see all groups."), diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java b/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java index 3822712..3b6bb79 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java @@ -4,7 +4,7 @@ public enum UserInfo { GET_PLAYER_HOMES_USAGE("Usage: /%s [playerName]"), BLACKLIST_USAGE("Usage: /%s [world]"), CREATE_HOME_USAGE("Usage: /create-home [name] [icon material, or d for the default icon] [description]. Omit the name and the home is called 'default'."), - NO_HOMES("You have not setup any homes yet, you can use the /create-home command to create one."), + NO_HOMES("You have not created any homes yet. Use /create-home to make your first one."), NO_MAX_HOMES("There is no max number of homes."), NO_BLACKLISTED_DIMENSIONS("No dimensions are blacklisted"), MOVED_TO_SAFE_SPOT("Your home was not safe to stand in, so you were moved to the nearest safe spot."), diff --git a/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java b/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java index 38e1cc6..75441b6 100644 --- a/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java +++ b/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java @@ -4,6 +4,7 @@ import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.datatypes.PersistentHome; import com.samleighton.sethomestwo.enums.UserError; +import com.samleighton.sethomestwo.enums.UserInfo; import com.samleighton.sethomestwo.metrics.UsageCounters; import com.samleighton.sethomestwo.models.Home; import com.samleighton.sethomestwo.utils.ChatUtils; @@ -94,8 +95,7 @@ public void displayInventory(Player player) { if (homesForDisplay == null || homesForDisplay.isEmpty()) { player.closeInventory(); - String noHomesError = ConfigUtil.getConfig().getString("noHomes", UserError.NO_HOMES.getValue()); - ChatUtils.sendError(player, noHomesError); + ChatUtils.sendInfo(player, ConfigUtil.getConfig().getString("noHomes", UserInfo.NO_HOMES.getValue())); return; } diff --git a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java index 39c36ca..8e6b5d0 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java @@ -5,6 +5,7 @@ import com.samleighton.sethomestwo.dao.HomesDao; import com.samleighton.sethomestwo.models.Home; import org.bukkit.Bukkit; +import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.configuration.ConfigurationSection; @@ -247,11 +248,11 @@ private void reportConfig(File pluginsDir, ImportReport report) { } if (v1.isSet("max-homes-msg")) { - report.configNotes.add(String.format("v1 max-homes-msg: '%s' -> set maxHomesReached: '%s' in config.yml", v1.getString("max-homes-msg"), v1.getString("max-homes-msg"))); + report.configNotes.add(messageNote("max-homes-msg", "maxHomesReached", v1.getString("max-homes-msg"))); } if (v1.isSet("tp-cancelOnMove-msg")) { - report.configNotes.add(String.format("v1 tp-cancelOnMove-msg: '%s' -> set movedWhileTeleporting: '%s' in config.yml", v1.getString("tp-cancelOnMove-msg"), v1.getString("tp-cancelOnMove-msg"))); + report.configNotes.add(messageNote("tp-cancelOnMove-msg", "movedWhileTeleporting", v1.getString("tp-cancelOnMove-msg"))); } ConfigurationSection maxHomes = v1.getConfigurationSection("max-homes"); @@ -267,7 +268,18 @@ private void reportConfig(File pluginsDir, ImportReport report) { } if (v1.isSet("tp-cooldown")) { - report.configNotes.add(String.format("v1 tp-cooldown: %s has no Set Homes Two equivalent; teleport cooldown is not supported.", v1.get("tp-cooldown"))); + report.configNotes.add(String.format("v1 tp-cooldown: %s has no v2 equivalent; teleport cooldown is not supported.", v1.get("tp-cooldown"))); } } + + // A v1 message may carry section-sign colour codes, which chat would apply + // to the rest of the line. Show them as & so the note stays legible. + private static String messageNote(String v1Key, String v2Key, String value) { + String shown = value.replace(ChatColor.COLOR_CHAR, '&'); + String note = String.format("v1 %s -> set %s: '%s' in config.yml", v1Key, v2Key, shown); + if (!shown.equals(value)) { + note += " (colour codes shown as &; copy the original from plugins/SetHomes/config.yml to keep them)"; + } + return note; + } } diff --git a/src/test/java/com/samleighton/sethomestwo/commands/ListHomesTest.java b/src/test/java/com/samleighton/sethomestwo/commands/ListHomesTest.java new file mode 100644 index 0000000..2083053 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/commands/ListHomesTest.java @@ -0,0 +1,25 @@ +package com.samleighton.sethomestwo.commands; + +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.ChatColor; +import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.entity.PlayerMock; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ListHomesTest extends ServerTestBase { + + @Test + void aPlayerWithNoHomesGetsTheSameNoticeAsTheMenu() { + PlayerMock player = addPlayer(); + + assertTrue(server.execute("list-homes", player).hasSucceeded()); + + String message = player.nextMessage(); + assertTrue(message.contains("You have not created any homes yet. Use /create-home to make your first one.")); + assertFalse(message.contains(ChatColor.RED.toString()), "having no homes is not an error"); + assertNull(player.nextMessage(), "one line is enough"); + } +} diff --git a/src/test/java/com/samleighton/sethomestwo/commands/OpenHomesGuiTest.java b/src/test/java/com/samleighton/sethomestwo/commands/OpenHomesGuiTest.java index e4ab0f7..4acef3a 100644 --- a/src/test/java/com/samleighton/sethomestwo/commands/OpenHomesGuiTest.java +++ b/src/test/java/com/samleighton/sethomestwo/commands/OpenHomesGuiTest.java @@ -3,11 +3,13 @@ import com.samleighton.sethomestwo.gui.GuiSession; import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.ChatColor; import org.junit.jupiter.api.Test; import org.mockbukkit.mockbukkit.entity.PlayerMock; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class OpenHomesGuiTest extends ServerTestBase { @@ -25,7 +27,9 @@ void aPlayerWithNoHomesIsTold() { assertTrue(server.execute("homes", player).hasSucceeded()); - assertTrue(player.nextMessage().contains("You have not created any homes yet.")); + String message = player.nextMessage(); + assertTrue(message.contains("You have not created any homes yet.")); + assertFalse(message.contains(ChatColor.RED.toString()), "having no homes is not an error"); } @Test diff --git a/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiClickTest.java b/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiClickTest.java index 39d4096..e96d3ba 100644 --- a/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiClickTest.java +++ b/src/test/java/com/samleighton/sethomestwo/gui/HomesGuiClickTest.java @@ -6,6 +6,7 @@ import com.samleighton.sethomestwo.support.HomeFixtures; import com.samleighton.sethomestwo.support.ServerTestBase; import com.samleighton.sethomestwo.support.TestPlayer; +import org.bukkit.ChatColor; import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.InventoryAction; import org.bukkit.event.inventory.InventoryClickEvent; @@ -144,6 +145,8 @@ void anEmptyHomeListClosesTheMenuAndExplainsWhy() { gui.setHomes(new HomesDao().getAll(player.getUniqueId())); gui.displayInventory(player); - assertTrue(player.nextMessage().contains("You have not created any homes yet.")); + String message = player.nextMessage(); + assertTrue(message.contains("You have not created any homes yet.")); + assertFalse(message.contains(ChatColor.RED.toString()), "having no homes is not an error"); } } diff --git a/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java index 28516ef..812b376 100644 --- a/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java +++ b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1ImporterTest.java @@ -206,7 +206,49 @@ void tpCooldownIsCalledOutAsHavingNoEquivalent() throws IOException { ImportReport report = importer.run(true); - assertTrue(report.configNotes.stream().anyMatch(n -> n.contains("tp-cooldown") && n.contains("no Set Homes Two equivalent"))); + assertTrue(report.configNotes.stream().anyMatch(n -> n.contains("tp-cooldown") && n.contains("no v2 equivalent"))); + } + + @Test + void v1MessagesAreShownWithTheirColourCodesAsAmpersands() throws IOException { + writeEmptyHomesFile(); + writeV1Config(v1 -> { + v1.set("max-homes-msg", "§4You have reached the maximum amount of saved homes!"); + v1.set("tp-cancelOnMove-msg", "§kMovement detected!"); + }); + + ImportReport report = importer.run(true); + + for (String note : report.configNotes) { + assertFalse(note.contains("§"), "section sign leaked into: " + note); + } + assertTrue(report.configNotes.stream().anyMatch(n -> + n.contains("maxHomesReached") && n.contains("&4You have reached the maximum amount of saved homes!"))); + assertTrue(report.configNotes.stream().anyMatch(n -> + n.contains("movedWhileTeleporting") && n.contains("&kMovement detected!"))); + } + + @Test + void aColourCodedV1MessageSaysWhereTheExactOriginalLives() throws IOException { + writeEmptyHomesFile(); + writeV1Config(v1 -> v1.set("max-homes-msg", "§4Too many homes!")); + + ImportReport report = importer.run(true); + + assertTrue(report.configNotes.stream().anyMatch(n -> + n.contains("maxHomesReached") && n.contains("plugins/SetHomes/config.yml"))); + } + + @Test + void aPlainV1MessageIsShownAsIsWithoutTheColourCodeCaveat() throws IOException { + writeEmptyHomesFile(); + writeV1Config(v1 -> v1.set("max-homes-msg", "Too many homes!")); + + ImportReport report = importer.run(true); + + String note = report.configNotes.stream().filter(n -> n.contains("maxHomesReached")).findFirst().orElseThrow(); + assertTrue(note.contains("'Too many homes!'")); + assertFalse(note.contains("plugins/SetHomes/config.yml")); } @Test From c3c76f958776510bb003f725423954f9f3d1a930 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 18:21:15 -0400 Subject: [PATCH 54/75] feat: refuse to enable when Set Homes v1 is installed alongside Both plugins declare /sethome, /home and /delhome. v1 declares them as primary names while we declare them as aliases, and SimpleCommandMap never lets an alias displace anything, so v1 takes all six whatever the load order. Homes created after an upgrade would land in v1's homes.yml while /homes read our database, and neither plugin logs a word about it. The guard runs as the first statement of onEnable, before any directory or config is written, so a refused boot leaves the server exactly as it was and v1 keeps serving its commands. --- .changeset/warm-badgers-wonder.md | 5 + README.md | 4 +- .../samleighton/sethomestwo/SetHomesTwo.java | 39 +++++ .../sethomestwo/SetHomesV1ClashTest.java | 150 ++++++++++++++++++ 4 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 .changeset/warm-badgers-wonder.md create mode 100644 src/test/java/com/samleighton/sethomestwo/SetHomesV1ClashTest.java diff --git a/.changeset/warm-badgers-wonder.md b/.changeset/warm-badgers-wonder.md new file mode 100644 index 0000000..9e5f9d0 --- /dev/null +++ b/.changeset/warm-badgers-wonder.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +Set Homes now refuses to start when a Set Homes v1 jar is still in your plugins folder, and the console says which file to move and what to run next. Both plugins provide /sethome, /home and /delhome and v1 wins those names, so running the two side by side used to split your players' homes between them with nothing in the log to show for it. diff --git a/README.md b/README.md index a562f56..7d5cb14 100644 --- a/README.md +++ b/README.md @@ -227,9 +227,11 @@ To pick one up, copy the key you want out of [`default-config.yml`](https://gith Your players keep their homes. The old plugin does not even need to be running, because the importer reads its data files directly. +**Coming from Set Homes v1, move the old jar out of `plugins/` first and keep it.** Both plugins provide `/sethome`, `/home` and `/delhome`, and v1 wins those names whatever the load order, so homes created after the upgrade would go into v1's files while the menu read ours. Rather than let that happen quietly, Set Homes refuses to start while a Set Homes v1 jar is installed, and prints what to do in the console. Your server keeps running v1 exactly as before until you move the jar. Leave the `plugins/SetHomes/` folder itself alone; the importer reads it and never writes to it. + 1. Run `/import-homes essentialsx` (or `/import-homes sethomes`). This is a **preview only**. It reports how many homes it would import and warns about any it would skip, and changes nothing. 2. Happy with the numbers? Run it again with `confirm` on the end. -3. Remove the old jar. This plugin provides `/sethome`, `/home` and `/delhome`, and two plugins claiming the same commands will fight over them. +3. Move the old jar out of `plugins/`. Keep it somewhere safe rather than deleting it, so you can go back if you want to. Existing homes are never overwritten, so re-running the import is always safe. Homes in worlds that no longer exist are skipped with a warning naming the world. diff --git a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java index 9346997..07602f2 100644 --- a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java +++ b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java @@ -38,8 +38,12 @@ import java.util.Map; import java.util.Objects; import java.util.UUID; +import java.util.logging.Logger; public class SetHomesTwo extends JavaPlugin { + // Set Homes v1's plugin.yml name. Ours differs, so Bukkit loads both happily. + private static final String SET_HOMES_V1 = "SetHomes"; + private final ConnectionManager connectionManager = new ConnectionManager(); private final Map guiSessionMap = new HashMap<>(); private final UsageCounters usageCounters = new UsageCounters(); @@ -56,6 +60,12 @@ public static SetHomesTwo instance() { @Override public void onEnable() { + // Before anything touches disk, so a refused boot leaves the server as it was. + if (Bukkit.getPluginManager().getPlugin(SET_HOMES_V1) != null) { + refuseToRunAlongsideV1(); + return; + } + // Create the directories for the plugin createDirectories(); @@ -109,6 +119,35 @@ public void onEnable() { } } + /** + * Logs why we are not starting and disables us. The text is hardcoded because + * this runs before initConfig, so there is no config to override it from. + */ + private void refuseToRunAlongsideV1() { + Logger log = Bukkit.getLogger(); + log.severe("============================================================"); + log.severe("Set Homes v2 did not start: Set Homes v1 is installed too."); + log.severe(""); + log.severe("Both plugins claim /sethome, /home and /delhome, and v1 takes"); + log.severe("them whatever the load order. Left alone, your players' homes"); + log.severe("would be split between the two plugins with nothing to show"); + log.severe("for it in the logs."); + log.severe(""); + log.severe("To finish the upgrade:"); + log.severe(" 1. Stop the server."); + log.severe(" 2. Move the old SetHomes jar file out of plugins/ and keep it"); + log.severe(" until you have migrated to v2."); + log.severe(" It is how you roll back if you change your mind."); + log.severe(" 3. Leave plugins/SetHomes/ folder where it is. Nothing ever"); + log.severe(" writes to it, but is needed for migrating homes to v2."); + log.severe(" 4. Start the server, then run /import-homes sethomes."); + log.severe(""); + log.severe("Set Homes v1 is still running, exactly as it was."); + log.severe("============================================================"); + + getServer().getPluginManager().disablePlugin(this); + } + @Override public void onDisable() { // Clear teleport attempts for all players diff --git a/src/test/java/com/samleighton/sethomestwo/SetHomesV1ClashTest.java b/src/test/java/com/samleighton/sethomestwo/SetHomesV1ClashTest.java new file mode 100644 index 0000000..93d6d9d --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/SetHomesV1ClashTest.java @@ -0,0 +1,150 @@ +package com.samleighton.sethomestwo; + +import com.samleighton.sethomestwo.support.FailOnUnimplemented; +import org.bukkit.Bukkit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Boots the plugin by hand rather than through ServerTestBase, because the v1 + * plugin has to be registered before our onEnable runs. + */ +@ExtendWith(FailOnUnimplemented.class) +class SetHomesV1ClashTest { + + private ServerMock server; + + @BeforeEach + void startServer() { + server = MockBukkit.mock(); + } + + @AfterEach + void stopServer() { + MockBukkit.unmock(); + } + + @Test + void refusesToEnableWhenSetHomesV1IsLoaded() { + MockBukkit.createMockPlugin("SetHomes"); + + SetHomesTwo plugin = MockBukkit.load(SetHomesTwo.class); + + assertFalse(plugin.isEnabled(), + "both jars installed splits /sethome and /homes between the two plugins, so we must not enable"); + } + + @Test + void theRefusalSaysWhichFileToMoveAndWhatToRunAfterwards() { + MockBukkit.createMockPlugin("SetHomes"); + + String block = severeText(captureLog(() -> MockBukkit.load(SetHomesTwo.class))); + + assertTrue(block.contains("plugins/"), "should name where the old jar is"); + assertTrue(block.contains("/import-homes sethomes"), "should name the command to run once it is gone"); + assertTrue(block.contains("keep"), "should say to keep the old jar so a rollback stays possible"); + assertFalse(block.contains("delete"), "deleting the old jar throws away the rollback"); + } + + @Test + void aRefusedBootCreatesNothingInOurDataFolder() { + MockBukkit.createMockPlugin("SetHomes"); + + SetHomesTwo plugin = MockBukkit.load(SetHomesTwo.class); + + assertFalse(new File(plugin.getDataFolder(), "config.yml").exists(), + "the guard must run before initConfig"); + assertFalse(new File(plugin.getDataFolder(), "database").exists(), + "the guard must run before createDirectories"); + } + + @Test + void enablesNormallyWhenSetHomesV1IsAbsent() { + assertTrue(loadPlugin().isEnabled()); + } + + @Test + void aPluginWhoseNameOnlyStartsWithSetHomesIsNotV1() { + MockBukkit.createMockPlugin("SetHomesThree"); + + assertTrue(loadPlugin().isEnabled()); + } + + @Test + void theNameMatchIsCaseSensitive() { + MockBukkit.createMockPlugin("sethomes"); + + assertTrue(loadPlugin().isEnabled()); + } + + /** + * Loads for the cases that expect a normal boot, with the same two switches + * ServerTestBase applies so a drained scheduler cannot reach the network. + */ + private SetHomesTwo loadPlugin() { + SetHomesTwo plugin = MockBukkit.load(SetHomesTwo.class); + plugin.getConfig().set("checkForUpdates", false); + + File bStatsDir = new File(plugin.getDataFolder().getParentFile(), "bStats"); + if (!bStatsDir.isDirectory() && !bStatsDir.mkdirs()) + throw new IllegalStateException("could not create " + bStatsDir); + try { + Files.writeString(new File(bStatsDir, "config.yml").toPath(), "enabled: false\n"); + } catch (IOException e) { + throw new IllegalStateException("could not write the bStats opt-out", e); + } + return plugin; + } + + private List captureLog(Runnable action) { + List captured = new ArrayList<>(); + Handler handler = new Handler() { + @Override + public void publish(LogRecord record) { + captured.add(record); + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + }; + + Logger logger = Bukkit.getLogger(); + logger.addHandler(handler); + try { + action.run(); + } finally { + logger.removeHandler(handler); + } + return captured; + } + + private String severeText(List records) { + return records.stream() + .filter(record -> record.getLevel() == Level.SEVERE) + .map(LogRecord::getMessage) + .collect(Collectors.joining("\n")); + } +} From 568a6a6b7ea348891511f094f61903f5f03cd781 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 19:24:20 -0400 Subject: [PATCH 55/75] feat: announce v1 homes waiting to be imported An admin who upgrades from Set Homes v1 and reads nothing sees an empty homes list on first boot, while plugins/SetHomes/homes.yml sits there full of homes with nothing saying so. Auto-import on enable was the original proposal and was rejected, so this replaces it. onEnable logs a warning block naming the file, how many homes are waiting and the command to run. Anyone holding sh2.import-homes gets the same reminder in chat on join, because plenty of admins never read the console. The condition is our own database being empty, re-read every time, so both stop for good once any home exists and no marker file is needed. --- .changeset/clever-tigers-wander.md | 5 + README.md | 2 + .../samleighton/sethomestwo/SetHomesTwo.java | 23 ++ .../sethomestwo/enums/UserInfo.java | 3 +- .../sethomestwo/events/PlayerJoin.java | 19 ++ .../importers/PendingV1Import.java | 54 +++++ src/main/resources/default-config.yml | 6 + .../PendingV1ImportNoticeTest.java | 228 ++++++++++++++++++ .../sethomestwo/support/PluginBoot.java | 37 +++ 9 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 .changeset/clever-tigers-wander.md create mode 100644 src/main/java/com/samleighton/sethomestwo/importers/PendingV1Import.java create mode 100644 src/test/java/com/samleighton/sethomestwo/PendingV1ImportNoticeTest.java create mode 100644 src/test/java/com/samleighton/sethomestwo/support/PluginBoot.java diff --git a/.changeset/clever-tigers-wander.md b/.changeset/clever-tigers-wander.md new file mode 100644 index 0000000..304011c --- /dev/null +++ b/.changeset/clever-tigers-wander.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +If Set Homes v1 homes are sitting in plugins/SetHomes/homes.yml and you have not imported them yet, the console now says so at startup, naming the file, how many homes are waiting and the command to run. Anyone with sh2.import-homes gets the same reminder in chat when they join. Both stop on their own as soon as any home exists here, so an upgraded server can no longer look empty without explanation. diff --git a/README.md b/README.md index 7d5cb14..60ba699 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,8 @@ Your players keep their homes. The old plugin does not even need to be running, 2. Happy with the numbers? Run it again with `confirm` on the end. 3. Move the old jar out of `plugins/`. Keep it somewhere safe rather than deleting it, so you can go back if you want to. +**You will not silently end up with an empty homes list.** Once the old jar is out and Set Homes starts, if `plugins/SetHomes/homes.yml` still holds homes and none have been imported here yet, the console says so at startup, naming the file, how many are waiting and the command to run. Anyone holding `sh2.import-homes` gets the same reminder in chat when they join, because plenty of admins never read the console. Both stop for good the moment any home exists here, so there is nothing to switch off afterwards. To reword the chat line, set `v1ImportPending` in `config.yml`. + Existing homes are never overwritten, so re-running the import is always safe. Homes in worlds that no longer exist are skipped with a warning naming the world. Set Homes v1 told `base` and `Base` apart, while home names here ignore case. A player holding both keeps both: the second one is imported under the next free name, so `Base` arrives as `Base2`, and the report and the server log name it. No home is dropped for a name clash. diff --git a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java index 07602f2..b23823f 100644 --- a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java +++ b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java @@ -13,6 +13,7 @@ import com.samleighton.sethomestwo.events.RightClickHomeItem; import com.samleighton.sethomestwo.gui.GuiSession; import com.samleighton.sethomestwo.gui.HomesGui; +import com.samleighton.sethomestwo.importers.PendingV1Import; import com.samleighton.sethomestwo.models.TeleportAttempt; import com.samleighton.sethomestwo.updates.GitHubReleaseSource; import com.samleighton.sethomestwo.updates.UpdateChecker; @@ -117,6 +118,28 @@ public void onEnable() { } else { Bukkit.getLogger().severe("Could not create database connection!"); } + + // Last, because it asks the database whether anything has been imported. + announcePendingV1Import(); + } + + /** + * Says so when v1 homes are sitting there unimported. Silent once any home + * exists here, so it needs no marker file. + */ + private void announcePendingV1Import() { + int waiting = PendingV1Import.waitingToBeImported(); + if (waiting == 0) return; + + Logger log = Bukkit.getLogger(); + log.warning("============================================================"); + log.warning("Set Homes found " + waiting + " home(s) in " + PendingV1Import.SOURCE_PATH); + log.warning("and none of its own, so your players cannot see theirs yet."); + log.warning(""); + log.warning("Run /import-homes sethomes for a preview that changes"); + log.warning("nothing, then /import-homes sethomes confirm to bring"); + log.warning("them across."); + log.warning("============================================================"); } /** diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java b/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java index 3b6bb79..29e584e 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserInfo.java @@ -11,7 +11,8 @@ public enum UserInfo { MOVE_HOME_USAGE("Usage: /%s "), GO_PLAYER_HOME_USAGE("Usage: /%s "), DELETE_PLAYER_HOME_USAGE("Usage: /%s "), - MOVE_PLAYER_HOME_USAGE("Usage: /%s "); + MOVE_PLAYER_HOME_USAGE("Usage: /%s "), + V1_IMPORT_PENDING("Set Homes v1 has %s home(s) waiting to be imported. Run /import-homes sethomes for a preview, then add confirm to bring them across."); private final String value; diff --git a/src/main/java/com/samleighton/sethomestwo/events/PlayerJoin.java b/src/main/java/com/samleighton/sethomestwo/events/PlayerJoin.java index 4c0bbae..f973bf7 100644 --- a/src/main/java/com/samleighton/sethomestwo/events/PlayerJoin.java +++ b/src/main/java/com/samleighton/sethomestwo/events/PlayerJoin.java @@ -2,8 +2,12 @@ import com.samleighton.sethomestwo.SetHomesTwo; import com.samleighton.sethomestwo.dao.HomesDao; +import com.samleighton.sethomestwo.enums.UserInfo; import com.samleighton.sethomestwo.gui.GuiSession; import com.samleighton.sethomestwo.gui.HomesGui; +import com.samleighton.sethomestwo.importers.PendingV1Import; +import com.samleighton.sethomestwo.utils.ChatUtils; +import com.samleighton.sethomestwo.utils.ConfigUtil; import com.samleighton.sethomestwo.updates.UpdateChecker; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -28,5 +32,20 @@ public void onPlayerJoin(PlayerJoinEvent event){ new HomesDao().refreshPlayerName(player.getUniqueId(), player.getName()); updateChecker.notifyIfUpdateAvailable(player); + notifyPendingV1Import(player); + } + + /** + * Tells an admin that v1 homes are waiting. The permission is checked first + * so an ordinary join never queries the database or reads v1's file. + */ + private void notifyPendingV1Import(Player player) { + if (!player.hasPermission("sh2.import-homes")) return; + + int waiting = PendingV1Import.waitingToBeImported(); + if (waiting == 0) return; + + ChatUtils.sendInfo(player, String.format(ConfigUtil.getConfig().getString( + "v1ImportPending", UserInfo.V1_IMPORT_PENDING.getValue()), waiting)); } } diff --git a/src/main/java/com/samleighton/sethomestwo/importers/PendingV1Import.java b/src/main/java/com/samleighton/sethomestwo/importers/PendingV1Import.java new file mode 100644 index 0000000..cbf0239 --- /dev/null +++ b/src/main/java/com/samleighton/sethomestwo/importers/PendingV1Import.java @@ -0,0 +1,54 @@ +package com.samleighton.sethomestwo.importers; + +import com.samleighton.sethomestwo.SetHomesTwo; +import com.samleighton.sethomestwo.dao.HomesDao; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; + +/** + * Whether a Set Homes v1 homes.yml is sitting there waiting to be imported. + * Reads the same file and layout as {@link SetHomesV1Importer}. + */ +public final class PendingV1Import { + + public static final String SOURCE_PATH = "plugins/SetHomes/homes.yml"; + + private PendingV1Import() { + } + + /** + * Homes waiting in v1's file while our own database is still empty, or 0 + * when there is nothing to announce. The database is checked first, so a + * server that has already imported never touches the disk. + * + * @return the number of homes waiting, 0 if none or if we already hold homes + */ + public static int waitingToBeImported() { + if (new HomesDao().countAll() > 0) return 0; + + File homesFile = new File(SetHomesTwo.instance().getDataFolder().getParentFile(), "SetHomes/homes.yml"); + if (!homesFile.exists()) return 0; + + return countHomes(YamlConfiguration.loadConfiguration(homesFile)); + } + + private static int countHomes(YamlConfiguration source) { + int total = 0; + + ConfigurationSection allNamed = source.getConfigurationSection("allNamedHomes"); + if (allNamed != null) { + for (String uuid : allNamed.getKeys(false)) { + ConfigurationSection playerSection = allNamed.getConfigurationSection(uuid); + if (playerSection != null) total += playerSection.getKeys(false).size(); + } + } + + // One unnamed home per player, which the importer brings across as "default". + ConfigurationSection unknown = source.getConfigurationSection("unknownHomes"); + if (unknown != null) total += unknown.getKeys(false).size(); + + return total; + } +} diff --git a/src/main/resources/default-config.yml b/src/main/resources/default-config.yml index 1198576..bbf7546 100644 --- a/src/main/resources/default-config.yml +++ b/src/main/resources/default-config.yml @@ -73,6 +73,12 @@ homeItemLore: "Right click this item to open your home's list." playerHomeDeleted: "%s's home '%s' has been deleted." playerHomeMoved: "%s's home '%s' has been moved to your location." +# Shown on join to anyone holding sh2.import-homes while Set Homes v1 homes are +# sitting in plugins/SetHomes/homes.yml and none have been imported here yet. +# The single %s is how many are waiting. It stops on its own once any home +# exists here, so there is nothing to switch off after the import. +v1ImportPending: "Set Homes v1 has %s home(s) waiting to be imported. Run /import-homes sethomes for a preview, then add confirm to bring them across." + # -- ERROR MESSAGES -- # Vestigial. No code path reaches this message any more: create-home treats a diff --git a/src/test/java/com/samleighton/sethomestwo/PendingV1ImportNoticeTest.java b/src/test/java/com/samleighton/sethomestwo/PendingV1ImportNoticeTest.java new file mode 100644 index 0000000..9093739 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/PendingV1ImportNoticeTest.java @@ -0,0 +1,228 @@ +package com.samleighton.sethomestwo; + +import com.samleighton.sethomestwo.support.FailOnUnimplemented; +import com.samleighton.sethomestwo.support.HomeFixtures; +import com.samleighton.sethomestwo.support.PluginBoot; +import com.samleighton.sethomestwo.support.TestPlayer; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Boots the plugin by hand rather than through ServerTestBase, because v1's + * homes.yml has to be on disk before our onEnable looks for it. + */ +@ExtendWith(FailOnUnimplemented.class) +class PendingV1ImportNoticeTest { + + private ServerMock server; + + @BeforeEach + void startServer() { + server = MockBukkit.mock(); + + // Same overworld, nether, end order ServerTestBase uses: ServerUtil maps + // environments onto worlds by list position. + server.addSimpleWorld("world").setEnvironment(World.Environment.NORMAL); + server.addSimpleWorld("world_nether").setEnvironment(World.Environment.NETHER); + server.addSimpleWorld("world_the_end").setEnvironment(World.Environment.THE_END); + } + + @AfterEach + void stopServer() { + MockBukkit.unmock(); + } + + @Test + void saysSoAtStartupWhenV1HomesAreWaitingAndNothingIsImported() { + writeV1Homes(2, 0); + + String block = warningText(captureLog(PluginBoot::load)); + + assertTrue(block.contains("plugins/SetHomes/homes.yml"), "should name the file it found"); + assertTrue(block.contains("2"), "should say how many homes are waiting"); + assertTrue(block.contains("/import-homes sethomes"), "should name the command to run"); + } + + @Test + void v1UnnamedHomesAreCountedToo() { + writeV1Homes(1, 1); + + String block = warningText(captureLog(PluginBoot::load)); + + assertTrue(block.contains("2 home"), + "v1 keeps a player's unnamed home under unknownHomes, and it imports like any other"); + } + + @Test + void aJoiningAdminIsToldTheImportIsWaiting() { + writeV1Homes(2, 0); + SetHomesTwo plugin = PluginBoot.load(); + + TestPlayer admin = join(plugin, "Admin", true); + + assertTrue(messagesTo(admin).contains("/import-homes sethomes"), + "plenty of admins never read the console, so the notice has to reach them in chat"); + } + + @Test + void aJoiningPlayerWithoutTheImportPermissionIsNotTold() { + writeV1Homes(2, 0); + SetHomesTwo plugin = PluginBoot.load(); + + TestPlayer player = join(plugin, "Regular", false); + + assertFalse(messagesTo(player).contains("import-homes"), + "only someone who can run the import has any use for the notice"); + } + + @Test + void theNoticeRepeatsOnEveryJoin() { + writeV1Homes(2, 0); + SetHomesTwo plugin = PluginBoot.load(); + TestPlayer admin = join(plugin, "Admin", true); + messagesTo(admin); + admin.disconnect(); + + // MockBukkit drops the attachment on disconnect; a real op or permissions + // group survives a reconnect, so grant it again before the second join. + admin.addAttachment(plugin, "sh2.import-homes", true); + server.addPlayer(admin); + + assertTrue(messagesTo(admin).contains("/import-homes sethomes"), + "nothing is remembered per player, so an admin who missed it sees it next time"); + } + + @Test + void theNoticeStopsOnceAHomeExistsHere() { + writeV1Homes(2, 0); + SetHomesTwo plugin = PluginBoot.load(); + TestPlayer first = join(plugin, "First", true); + HomeFixtures.persist(first, "base"); + + TestPlayer second = join(plugin, "Second", true); + + assertFalse(messagesTo(second).contains("import-homes"), + "an empty database is the whole condition, so one home ends the notice"); + } + + @Test + void nothingIsSaidWhenThereIsNoV1File() { + String block = warningText(captureLog(PluginBoot::load)); + + assertFalse(block.contains("import-homes"), "most servers have never had v1 installed"); + } + + @Test + void nothingIsSaidWhenTheV1FileHoldsNoHomes() { + writeV1Homes(0, 0); + + String block = warningText(captureLog(PluginBoot::load)); + + assertFalse(block.contains("import-homes"), "an empty homes.yml has nothing to offer"); + } + + /** + * Joins a player, granting sh2.import-homes outright rather than opping, so + * the tests pin the permission the notice is actually gated on. + */ + private TestPlayer join(SetHomesTwo plugin, String name, boolean mayImport) { + TestPlayer player = new TestPlayer(server, name); + if (mayImport) player.addAttachment(plugin, "sh2.import-homes", true); + server.addPlayer(player); + return player; + } + + private String messagesTo(TestPlayer player) { + StringBuilder all = new StringBuilder(); + String message; + while ((message = player.nextMessage()) != null) all.append(message).append("\n"); + return all.toString(); + } + + /** + * Writes a v1 homes.yml in the layout SetHomesV1Importer reads: named homes + * under allNamedHomes.uuid.name, unnamed ones under unknownHomes.uuid. + */ + private void writeV1Homes(int named, int unnamed) { + YamlConfiguration source = new YamlConfiguration(); + for (int i = 0; i < named; i++) { + writeOne(source, "allNamedHomes." + UUID.randomUUID() + ".home" + i); + } + for (int i = 0; i < unnamed; i++) { + writeOne(source, "unknownHomes." + UUID.randomUUID()); + } + + File v1Dir = new File(server.getPluginsFolder(), "SetHomes"); + if (!v1Dir.isDirectory() && !v1Dir.mkdirs()) + throw new IllegalStateException("could not create " + v1Dir); + try { + source.save(new File(v1Dir, "homes.yml")); + } catch (IOException e) { + throw new IllegalStateException("could not write the v1 fixture", e); + } + } + + private void writeOne(YamlConfiguration source, String path) { + source.set(path + ".world", "world"); + source.set(path + ".x", 1.0); + source.set(path + ".y", 64.0); + source.set(path + ".z", 1.0); + source.set(path + ".pitch", 0.0); + source.set(path + ".yaw", 0.0); + } + + private List captureLog(Runnable action) { + List captured = new ArrayList<>(); + Handler handler = new Handler() { + @Override + public void publish(LogRecord record) { + captured.add(record); + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + }; + + Logger logger = Bukkit.getLogger(); + logger.addHandler(handler); + try { + action.run(); + } finally { + logger.removeHandler(handler); + } + return captured; + } + + private String warningText(List records) { + return records.stream() + .filter(record -> record.getLevel() == Level.WARNING) + .map(LogRecord::getMessage) + .collect(Collectors.joining("\n")); + } +} diff --git a/src/test/java/com/samleighton/sethomestwo/support/PluginBoot.java b/src/test/java/com/samleighton/sethomestwo/support/PluginBoot.java new file mode 100644 index 0000000..7936590 --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/support/PluginBoot.java @@ -0,0 +1,37 @@ +package com.samleighton.sethomestwo.support; + +import com.samleighton.sethomestwo.SetHomesTwo; +import org.mockbukkit.mockbukkit.MockBukkit; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +/** + * Loads the plugin for tests that have to arrange state before onEnable runs. + * ServerTestBase cannot do that: it loads the plugin in its own @BeforeEach. + */ +public final class PluginBoot { + + private PluginBoot() { + } + + /** + * Loads the plugin with the same two switches ServerTestBase applies, so a + * drained scheduler can never reach the GitHub API or construct bStats. + */ + public static SetHomesTwo load() { + SetHomesTwo plugin = MockBukkit.load(SetHomesTwo.class); + plugin.getConfig().set("checkForUpdates", false); + + File bStatsDir = new File(plugin.getDataFolder().getParentFile(), "bStats"); + if (!bStatsDir.isDirectory() && !bStatsDir.mkdirs()) + throw new IllegalStateException("could not create " + bStatsDir); + try { + Files.writeString(new File(bStatsDir, "config.yml").toPath(), "enabled: false\n"); + } catch (IOException e) { + throw new IllegalStateException("could not write the bStats opt-out", e); + } + return plugin; + } +} From b470419bd2b03bdd669d2f8ed83ced65a6fa710d Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 19:37:21 -0400 Subject: [PATCH 56/75] test: assert the v1 import never writes to plugins/SetHomes/ The migration is reversible only because v2 never writes, renames or deletes anything under plugins/SetHomes/. Put the old jar back and the server is as it was. That guarantee is why taking the name SetHomes was rejected, but nothing in the suite held the importer to it. Fingerprints every path under the v1 folder as SHA-256 before and after, for the dry run and for confirm, and compares the whole map so an edit, an addition and a deletion all fail alike. Directories are recorded in their own right, or an added empty folder would contribute no entries and pass unnoticed. No changeset: src/test is exempt from the gate and nothing here ships. --- .../SetHomesV1SourceUntouchedTest.java | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1SourceUntouchedTest.java diff --git a/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1SourceUntouchedTest.java b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1SourceUntouchedTest.java new file mode 100644 index 0000000..62a3e3f --- /dev/null +++ b/src/test/java/com/samleighton/sethomestwo/importers/SetHomesV1SourceUntouchedTest.java @@ -0,0 +1,142 @@ +package com.samleighton.sethomestwo.importers; + +import com.samleighton.sethomestwo.support.ServerTestBase; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The migration is reversible only because v2 never writes, renames or deletes + * anything under plugins/SetHomes/. Put the old jar back and the server is as + * it was. Nothing else in the suite holds the importer to that. + */ +class SetHomesV1SourceUntouchedTest extends ServerTestBase { + + private final SetHomesV1Importer importer = new SetHomesV1Importer(); + + private File v1Dir; + + @BeforeEach + void writeV1Source() throws IOException { + v1Dir = new File(plugin.getDataFolder().getParentFile(), "SetHomes"); + if (!v1Dir.isDirectory() && !v1Dir.mkdirs()) + throw new IllegalStateException("could not create " + v1Dir); + + writeHomes(); + writeBlacklist(); + writeConfig(); + } + + @Test + void aDryRunLeavesEveryV1FileExactlyAsItWas() throws IOException { + Map before = fingerprint(v1Dir); + + importer.run(true); + + assertEquals(before, fingerprint(v1Dir), + "a preview that writes to v1's folder is not a preview"); + } + + @Test + void aConfirmedImportLeavesEveryV1FileExactlyAsItWas() throws IOException { + Map before = fingerprint(v1Dir); + + ImportReport report = importer.run(false); + + // Without this the test would also pass for an importer that did nothing. + assertTrue(report.imported > 0, "the import should have brought homes across"); + assertTrue(report.renamed > 0, "the fixture holds a case-only duplicate"); + assertTrue(report.skippedWorldMissing > 0, "the fixture holds a home in a missing world"); + + assertEquals(before, fingerprint(v1Dir), + "rolling back to v1 depends on its files being untouched"); + } + + /** + * Path to SHA-256 for every file under the directory. Comparing the whole + * map catches an edited file, a new one and a deleted one alike. + */ + private Map fingerprint(File dir) throws IOException { + Map digests = new TreeMap<>(); + File[] entries = dir.listFiles(); + if (entries == null) throw new IllegalStateException("not a directory: " + dir); + + for (File entry : entries) { + if (entry.isDirectory()) { + // Recorded in its own right, or an added empty directory would + // contribute no entries and slip through unnoticed. + digests.put(entry.getName() + "/", "directory"); + fingerprint(entry).forEach((path, digest) -> digests.put(entry.getName() + "/" + path, digest)); + } else { + digests.put(entry.getName(), sha256(entry)); + } + } + return digests; + } + + private String sha256(File file) throws IOException { + try { + byte[] digest = MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(file.toPath())); + StringBuilder hex = new StringBuilder(); + for (byte b : digest) hex.append(String.format("%02x", b)); + return hex.toString(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 is required of every JVM", e); + } + } + + /** + * The cases that make the importer work hardest, so the test is not proving + * read-only behavior on a file the importer barely looks at: a home in a + * world that no longer exists, a case-only duplicate name, an unnamed home, + * and a player the server has never seen. + */ + private void writeHomes() throws IOException { + String owner = UUID.randomUUID().toString(); + String stranger = UUID.randomUUID().toString(); + + YamlConfiguration yaml = new YamlConfiguration(); + home(yaml, "allNamedHomes." + owner + ".base", "world"); + home(yaml, "allNamedHomes." + owner + ".Base", "world"); + home(yaml, "allNamedHomes." + owner + ".nether", "world_nether"); + home(yaml, "allNamedHomes." + owner + ".plotworld", "creative"); + home(yaml, "unknownHomes." + stranger, "world"); + yaml.save(new File(v1Dir, "homes.yml")); + } + + private void home(YamlConfiguration yaml, String path, String world) { + yaml.set(path + ".world", world); + yaml.set(path + ".x", 1.5); + yaml.set(path + ".y", 64.0); + yaml.set(path + ".z", -2.5); + yaml.set(path + ".pitch", 12.0); + yaml.set(path + ".yaw", 45.0); + } + + private void writeBlacklist() throws IOException { + YamlConfiguration yaml = new YamlConfiguration(); + yaml.set("blacklisted_worlds", List.of("world_the_end")); + yaml.save(new File(v1Dir, "world_blacklist.yml")); + } + + private void writeConfig() throws IOException { + YamlConfiguration yaml = new YamlConfiguration(); + yaml.set("tp-delay", 5); + yaml.set("tp-cancelOnMove", true); + yaml.set("max-homes.default", 3); + yaml.save(new File(v1Dir, "config.yml")); + } +} From 35704ef033e34a114374a4434091a1f92d4999e8 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 19:39:25 -0400 Subject: [PATCH 57/75] feat: parse closing references out of pull request bodies --- .github/workflows/tests.yml | 3 + scripts/issue-status.sh | 19 +++++ scripts/test-issue-status.sh | 140 +++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 scripts/issue-status.sh create mode 100644 scripts/test-issue-status.sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 52a4d79..7eab0bd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -51,6 +51,9 @@ jobs: - name: Run the BukkitDev publish tests run: bash scripts/test-publish-bukkitdev.sh + - name: Run the issue status parsing tests + run: bash scripts/test-issue-status.sh + # Rehearses the release on every pull request. The changelog heading went # missing in a docs change, and nothing noticed until a release ran. - name: Check a release could be applied diff --git a/scripts/issue-status.sh b/scripts/issue-status.sh new file mode 100644 index 0000000..ff36fe6 --- /dev/null +++ b/scripts/issue-status.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Parses issue and pull request references out of text. +# +# Pure text handling, no network, so the workflows that call it stay thin and +# the parsing is unit tested. + +# GitHub's own closing keyword set. A bare #NN must not close anything: pull +# request bodies here routinely mention issues they do not resolve. +CLOSING_KEYWORDS='close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved' + +# Reads text on stdin, writes the issue numbers it closes to stdout, one per +# line, first appearance order, deduplicated. Always exits 0. +closing_refs() { + grep -oiE "\\b(${CLOSING_KEYWORDS})[[:space:]]*:?[[:space:]]+#[0-9]+" \ + | grep -oE '[0-9]+' \ + | grep -E '^[1-9][0-9]*$' \ + | awk '!seen[$0]++' + return 0 +} diff --git a/scripts/test-issue-status.sh b/scripts/test-issue-status.sh new file mode 100644 index 0000000..ac13edd --- /dev/null +++ b/scripts/test-issue-status.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# Defines the issue-status.sh contract. +# +# Sources issue-status.sh so the tests can call its functions in-process. +# +# Run with: bash scripts/test-issue-status.sh +set -u + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ISSUE_STATUS_SH="$SCRIPT_DIR/issue-status.sh" + +# shellcheck source=./issue-status.sh +source "$ISSUE_STATUS_SH" + +PASS=0 +FAIL=0 + +pass() { + PASS=$((PASS + 1)) + printf 'ok - %s\n' "$1" +} + +fail() { + FAIL=$((FAIL + 1)) + printf 'FAIL - %s\n' "$1" + if [ -n "${2:-}" ]; then + printf ' %s\n' "$2" + fi +} + +assert_equals() { + local desc="$1" expected="$2" actual="$3" + if [ "$expected" = "$actual" ]; then + pass "$desc" + else + fail "$desc" "expected [$expected] got [$actual]" + fi +} + +# Runs closing_refs over a body and joins the result with commas, so an +# expectation reads as one string. +refs_of() { + printf '%s' "$1" | closing_refs | paste -sd, - +} + +# -- which references close an issue -- + +test_each_keyword_matches() { + assert_equals "close" "1" "$(refs_of 'close #1')" + assert_equals "closes" "2" "$(refs_of 'closes #2')" + assert_equals "closed" "3" "$(refs_of 'closed #3')" + assert_equals "fix" "4" "$(refs_of 'fix #4')" + assert_equals "fixes" "5" "$(refs_of 'fixes #5')" + assert_equals "fixed" "6" "$(refs_of 'fixed #6')" + assert_equals "resolve" "7" "$(refs_of 'resolve #7')" + assert_equals "resolves" "8" "$(refs_of 'resolves #8')" + assert_equals "resolved" "9" "$(refs_of 'resolved #9')" +} + +test_keywords_are_case_insensitive() { + assert_equals "Closes" "10" "$(refs_of 'Closes #10')" + assert_equals "FIXES" "11" "$(refs_of 'FIXES #11')" +} + +test_a_colon_and_extra_space_are_tolerated() { + assert_equals "colon form" "12" "$(refs_of 'Closes: #12')" + assert_equals "wide space" "13" "$(refs_of 'Closes #13')" +} + +test_trailing_punctuation_is_not_part_of_the_number() { + assert_equals "full stop" "53" "$(refs_of 'Closes #53.')" + assert_equals "comma" "53" "$(refs_of 'Closes #53, and more')" +} + +test_a_bare_mention_does_not_close() { + assert_equals "bare hash" "" "$(refs_of 'See #41 for background')" +} + +test_a_keyword_inside_a_word_does_not_count() { + assert_equals "supercloses" "" "$(refs_of 'supercloses #5')" +} + +test_a_real_pull_request_body_yields_only_the_closed_issue() { + local body + body='Closes #53. First of the five sub-issues split out of #41. + +The alternatives are recorded on #41 and were rejected. + +- #46 (found during the in game check) +- The README change slightly overlaps #56.' + assert_equals "PR 58 body closes only 53" "53" "$(refs_of "$body")" +} + +test_several_references_are_all_returned() { + assert_equals "two keywords" "1,2" "$(refs_of 'Closes #1 and fixes #2')" +} + +test_a_repeated_reference_is_returned_once() { + assert_equals "deduped" "7" "$(refs_of 'Closes #7. Also closes #7.')" +} + +test_a_body_with_no_reference_is_empty_and_clean() { + assert_equals "no refs" "" "$(refs_of 'Just a description.')" + printf '%s' 'Just a description.' | closing_refs > /dev/null + assert_equals "exit status" "0" "$?" +} + +test_an_empty_body_is_empty_and_clean() { + assert_equals "empty" "" "$(refs_of '')" + printf '%s' '' | closing_refs > /dev/null + assert_equals "exit status" "0" "$?" +} + +test_issue_zero_is_rejected() { + assert_equals "hash zero" "" "$(refs_of 'Closes #0')" +} + +test_a_non_numeric_reference_is_rejected() { + assert_equals "hash word" "" "$(refs_of 'Closes #abc')" +} + +test_each_keyword_matches +test_keywords_are_case_insensitive +test_a_colon_and_extra_space_are_tolerated +test_trailing_punctuation_is_not_part_of_the_number +test_a_bare_mention_does_not_close +test_a_keyword_inside_a_word_does_not_count +test_a_real_pull_request_body_yields_only_the_closed_issue +test_several_references_are_all_returned +test_a_repeated_reference_is_returned_once +test_a_body_with_no_reference_is_empty_and_clean +test_an_empty_body_is_empty_and_clean +test_issue_zero_is_rejected +test_a_non_numeric_reference_is_rejected + +printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi +exit 0 From bbf0528166ec3eb8b18b81af373249c60386cc10 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 19:44:47 -0400 Subject: [PATCH 58/75] feat: recover pull request numbers from a commit range --- scripts/issue-status.sh | 11 +++++++ scripts/test-issue-status.sh | 59 ++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/scripts/issue-status.sh b/scripts/issue-status.sh index ff36fe6..dfef119 100644 --- a/scripts/issue-status.sh +++ b/scripts/issue-status.sh @@ -17,3 +17,14 @@ closing_refs() { | awk '!seen[$0]++' return 0 } + +# Reads `git log --format=%s` output on stdin and writes the pull request +# numbers it contains to stdout, one per line, deduplicated. Subjects only: +# a body may mention a pull request the commit did not come from. +pr_numbers_from_log() { + grep -oE '(Merge pull request #[0-9]+|\(#[0-9]+\))' \ + | grep -oE '[0-9]+' \ + | grep -E '^[1-9][0-9]*$' \ + | awk '!seen[$0]++' + return 0 +} diff --git a/scripts/test-issue-status.sh b/scripts/test-issue-status.sh index ac13edd..4328d49 100644 --- a/scripts/test-issue-status.sh +++ b/scripts/test-issue-status.sh @@ -119,6 +119,57 @@ test_a_non_numeric_reference_is_rejected() { assert_equals "hash word" "" "$(refs_of 'Closes #abc')" } +# Runs pr_numbers_from_log over log subjects and joins the result with commas. +prs_of() { + printf '%s' "$1" | pr_numbers_from_log | paste -sd, - +} + +# -- which pull requests reached a commit range -- + +test_a_merge_subject_yields_its_number() { + assert_equals "merge commit" "58" \ + "$(prs_of 'Merge pull request #58 from Blockframe-Studios/issue-53-refuse-alongside-v1')" +} + +test_a_squash_subject_yields_its_number() { + assert_equals "squash commit" "44" \ + "$(prs_of 'Tab completion matches anywhere in a name (#44)')" +} + +test_a_release_commit_yields_nothing() { + assert_equals "release commit" "" "$(prs_of 'chore(release): 1.2.3')" +} + +test_an_ordinary_commit_yields_nothing() { + assert_equals "plain commit" "" "$(prs_of 'fix: send the BukkitDev metadata with --form-string')" +} + +# A subject may cite an issue without the commit having come from that pull +# request. Only the merge and squash forms count. +test_a_mention_in_a_subject_is_not_a_pull_request() { + assert_equals "bare mention" "" "$(prs_of 'fix: address feedback on #41')" +} + +test_an_empty_range_is_empty_and_clean() { + assert_equals "empty range" "" "$(prs_of '')" + printf '%s' '' | pr_numbers_from_log > /dev/null + assert_equals "exit status" "0" "$?" +} + +test_repeated_numbers_collapse() { + local log + log='Merge pull request #31 from Blockframe-Studios/fix/bukkitdev-metadata-semicolon +Merge pull request #31 from Blockframe-Studios/fix/bukkitdev-metadata-semicolon' + assert_equals "deduped" "31" "$(prs_of "$log")" +} + +test_several_merges_keep_first_appearance_order() { + local log + log='Merge pull request #58 from Blockframe-Studios/issue-53-refuse-alongside-v1 +Merge pull request #52 from Blockframe-Studios/issue-49-import-report-colour-codes' + assert_equals "order kept" "58,52" "$(prs_of "$log")" +} + test_each_keyword_matches test_keywords_are_case_insensitive test_a_colon_and_extra_space_are_tolerated @@ -132,6 +183,14 @@ test_a_body_with_no_reference_is_empty_and_clean test_an_empty_body_is_empty_and_clean test_issue_zero_is_rejected test_a_non_numeric_reference_is_rejected +test_a_merge_subject_yields_its_number +test_a_squash_subject_yields_its_number +test_a_release_commit_yields_nothing +test_an_ordinary_commit_yields_nothing +test_a_mention_in_a_subject_is_not_a_pull_request +test_an_empty_range_is_empty_and_clean +test_repeated_numbers_collapse +test_several_merges_keep_first_appearance_order printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" if [ "$FAIL" -gt 0 ]; then From 4713ae8b4cff0dd1fef15409675f58cecbb6ea03 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 19:50:46 -0400 Subject: [PATCH 59/75] feat: close the issues a release contains --- .github/workflows/release.yml | 35 +++++++++++++++++++ scripts/close-released-issues.sh | 60 ++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 scripts/close-released-issues.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef44386..bbf9aeb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,7 @@ concurrency: permissions: contents: write + issues: write jobs: release: @@ -96,6 +97,17 @@ jobs: echo "current=$CURRENT" echo "next=$NEXT" + - name: Dry run - list the issues this release would close + if: steps.plan.outputs.release == 'true' && inputs.dry_run + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.plan.outputs.next }} + DRY_RUN: '1' + run: | + set -uo pipefail + PREV_TAG="$(git describe --tags --abbrev=0 2>/dev/null || true)" \ + bash scripts/close-released-issues.sh + - name: Dry run - resolve BukkitDev game versions if: steps.plan.outputs.release == 'true' && inputs.dry_run env: @@ -116,6 +128,17 @@ jobs: end ' + # Must run before Commit and tag creates v$NEXT, or the range below is + # empty and nothing closes. + - name: Record the previous release tag + id: prev + if: steps.plan.outputs.release == 'true' + run: | + set -uo pipefail + tag="$(git describe --tags --abbrev=0 2>/dev/null || true)" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "Previous release tag: ${tag:-none}" + - name: Commit and tag if: steps.plan.outputs.release == 'true' && !inputs.dry_run env: @@ -153,3 +176,15 @@ jobs: gh release create "v$VERSION" "SetHomesTwo.V$VERSION.jar" \ --title "SetHomesTwo V$VERSION" \ --notes "$NOTES" + + # An issue closes when the commit that fixed it is contained in the commit + # being released. That is what keeps work sitting on dev open when an + # immediate fix ships from master, and closes an immediate fix that never + # passed through dev. + - name: Close the issues this release contains + if: steps.plan.outputs.release == 'true' && !inputs.dry_run + env: + GH_TOKEN: ${{ github.token }} + PREV_TAG: ${{ steps.prev.outputs.tag }} + VERSION: ${{ steps.plan.outputs.next }} + run: bash scripts/close-released-issues.sh diff --git a/scripts/close-released-issues.sh b/scripts/close-released-issues.sh new file mode 100644 index 0000000..8d5aa93 --- /dev/null +++ b/scripts/close-released-issues.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Closes the issues contained in the commit being released. +# +# Reads PREV_TAG (may be empty on a first release) and VERSION from the +# environment. With DRY_RUN=1 it prints what it would close and closes nothing. +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./issue-status.sh +source "$SCRIPT_DIR/issue-status.sh" + +VERSION="${VERSION:?VERSION must be set}" +PREV_TAG="${PREV_TAG:-}" +DRY_RUN="${DRY_RUN:-0}" + +range="HEAD" +if [ -n "$PREV_TAG" ]; then + range="$PREV_TAG..HEAD" +fi +printf 'Range: %s\n' "$range" + +# Subjects only. A commit body may mention a pull request it did not come from. +prs="$(git log --format=%s "$range" | pr_numbers_from_log)" +if [ -z "$prs" ]; then + echo "No pull requests in the range - nothing to close." + exit 0 +fi + +issues="" +while IFS= read -r pr; do + [ -n "$pr" ] || continue + body="$(gh pr view "$pr" --json body --jq '.body')" || continue + refs="$(printf '%s' "$body" | closing_refs)" + [ -n "$refs" ] || continue + issues="$(printf '%s\n%s' "$issues" "$refs")" +done <<< "$prs" + +issues="$(printf '%s' "$issues" | grep -E '^[1-9][0-9]*$' | awk '!seen[$0]++')" +if [ -z "$issues" ]; then + echo "No closing references among those pull requests - nothing to close." + exit 0 +fi + +while IFS= read -r issue; do + [ -n "$issue" ] || continue + + state="$(gh issue view "$issue" --json state --jq '.state')" || continue + if [ "$state" != "OPEN" ]; then + printf '#%s is already %s - skipping.\n' "$issue" "$state" + continue + fi + + if [ "$DRY_RUN" = "1" ]; then + printf 'Would close #%s (Released in v%s)\n' "$issue" "$VERSION" + continue + fi + + gh issue close "$issue" --reason completed --comment "Released in v$VERSION" + printf 'Closed #%s\n' "$issue" +done <<< "$issues" From 5e70e57803502c1b8a2541efafa99e956861d67c Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 19:58:39 -0400 Subject: [PATCH 60/75] fix: surface gh failures in the issue-closing step instead of swallowing them --- .github/workflows/release.yml | 1 + scripts/close-released-issues.sh | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bbf9aeb..b813681 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,7 @@ concurrency: permissions: contents: write issues: write + pull-requests: read jobs: release: diff --git a/scripts/close-released-issues.sh b/scripts/close-released-issues.sh index 8d5aa93..1979a80 100644 --- a/scripts/close-released-issues.sh +++ b/scripts/close-released-issues.sh @@ -27,20 +27,33 @@ if [ -z "$prs" ]; then fi issues="" +pr_count=0 +pr_failures=0 while IFS= read -r pr; do [ -n "$pr" ] || continue - body="$(gh pr view "$pr" --json body --jq '.body')" || continue + pr_count=$((pr_count + 1)) + if ! body="$(gh pr view "$pr" --json body --jq '.body')"; then + printf 'Warning: could not look up pull request #%s - skipping it.\n' "$pr" >&2 + pr_failures=$((pr_failures + 1)) + continue + fi refs="$(printf '%s' "$body" | closing_refs)" [ -n "$refs" ] || continue issues="$(printf '%s\n%s' "$issues" "$refs")" done <<< "$prs" +if [ "$pr_count" -gt 0 ] && [ "$pr_failures" -eq "$pr_count" ]; then + printf 'Error: all %d pull request lookups failed - cannot tell what this release closes.\n' "$pr_count" >&2 + exit 1 +fi + issues="$(printf '%s' "$issues" | grep -E '^[1-9][0-9]*$' | awk '!seen[$0]++')" if [ -z "$issues" ]; then echo "No closing references among those pull requests - nothing to close." exit 0 fi +close_failures=0 while IFS= read -r issue; do [ -n "$issue" ] || continue @@ -55,6 +68,14 @@ while IFS= read -r issue; do continue fi - gh issue close "$issue" --reason completed --comment "Released in v$VERSION" - printf 'Closed #%s\n' "$issue" + if gh issue close "$issue" --reason completed --comment "Released in v$VERSION"; then + printf 'Closed #%s\n' "$issue" + else + printf 'Failed to close #%s - close it by hand.\n' "$issue" >&2 + close_failures=$((close_failures + 1)) + fi done <<< "$issues" + +if [ "$close_failures" -gt 0 ]; then + exit 1 +fi From 22553e2c0cccd98c01d1d58046f32635dfdc596f Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 20:33:20 -0400 Subject: [PATCH 61/75] feat: set an issue's status on the project board --- scripts/set-issue-status.sh | 130 ++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 scripts/set-issue-status.sh diff --git a/scripts/set-issue-status.sh b/scripts/set-issue-status.sh new file mode 100644 index 0000000..6150b80 --- /dev/null +++ b/scripts/set-issue-status.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# Sets an issue's Status on the SetHomesTwo project board. +# +# bash scripts/set-issue-status.sh 54 "In review" +# bash scripts/set-issue-status.sh 54 "In progress" Todo unset +# +# The optional trailing arguments guard the write: the status is set only when +# the item currently holds one of them, where `unset` means no status yet. That +# is what stops a later push to an issue branch pulling the issue back out of +# In review. +# +# Needs GH_TOKEN with the project scope; GITHUB_TOKEN cannot reach an +# organization project. +set -uo pipefail + +ORG="Blockframe-Studios" +REPO="SetHomesTwo" +PROJECT_NUMBER="${PROJECT_NUMBER:?PROJECT_NUMBER must be set}" + +ISSUE="${1:?issue number required}" +TARGET_STATUS="${2:?target status required}" +shift 2 +ALLOWED_CURRENT=("$@") + +PROJECT_QUERY=' + query($org:String!, $number:Int!) { + organization(login:$org) { + projectV2(number:$number) { + id + field(name:"Status") { + ... on ProjectV2SingleSelectField { id options { id name } } + } + } + } + }' + +# gh has jq built in. Standalone jq is not installed on the development machine, +# and every line here has to run locally as well as on a runner. +ids="$(gh api graphql -f query="$PROJECT_QUERY" -f org="$ORG" \ + -F number="$PROJECT_NUMBER" \ + --jq '[.data.organization.projectV2.id, + .data.organization.projectV2.field.id] | @tsv')" || exit 1 +IFS=$'\t' read -r project_id field_id <<<"$ids" + +options="$(gh api graphql -f query="$PROJECT_QUERY" -f org="$ORG" \ + -F number="$PROJECT_NUMBER" \ + --jq '.data.organization.projectV2.field.options[] | [.name, .id] | @tsv')" || exit 1 + +# Matched in awk rather than inside the jq program, so the status name is never +# interpolated into a query. +option_id="$(printf '%s\n' "$options" \ + | awk -F'\t' -v n="$TARGET_STATUS" '$1 == n { print $2; exit }')" + +if [ -z "$option_id" ]; then + printf 'No Status option named [%s] on the project.\n' "$TARGET_STATUS" >&2 + exit 1 +fi + +ISSUE_QUERY=' + query($owner:String!, $repo:String!, $issue:Int!) { + repository(owner:$owner, name:$repo) { + issue(number:$issue) { + id + projectItems(first:20) { + nodes { + id + project { id } + fieldValueByName(name:"Status") { + ... on ProjectV2ItemFieldSingleSelectValue { name } + } + } + } + } + } + }' + +issue_id="$(gh api graphql -f query="$ISSUE_QUERY" -f owner="$ORG" -f repo="$REPO" \ + -F issue="$ISSUE" --jq '.data.repository.issue.id')" || exit 1 +if [ -z "$issue_id" ]; then + printf 'Issue #%s not found - nothing to do.\n' "$ISSUE" + exit 0 +fi + +items="$(gh api graphql -f query="$ISSUE_QUERY" -f owner="$ORG" -f repo="$REPO" \ + -F issue="$ISSUE" \ + --jq '.data.repository.issue.projectItems.nodes[] + | [.project.id, .id, (.fieldValueByName.name // "unset")] | @tsv')" || exit 1 + +item_id="$(printf '%s\n' "$items" \ + | awk -F'\t' -v p="$project_id" '$1 == p { print $2; exit }')" +current_status="$(printf '%s\n' "$items" \ + | awk -F'\t' -v p="$project_id" '$1 == p { print $3; exit }')" + +if [ -z "$item_id" ] || [ "$item_id" = "null" ]; then + current_status="unset" + # Idempotent: returns the existing item when the issue is already on the board. + item_id="$(gh api graphql -f query=' + mutation($project:ID!, $content:ID!) { + addProjectV2ItemById(input:{projectId:$project, contentId:$content}) { + item { id } + } + }' -f project="$project_id" -f content="$issue_id" \ + --jq '.data.addProjectV2ItemById.item.id')" || exit 1 +fi + +if [ "${#ALLOWED_CURRENT[@]}" -gt 0 ]; then + allowed=1 + for candidate in "${ALLOWED_CURRENT[@]}"; do + if [ "$candidate" = "$current_status" ]; then + allowed=0 + break + fi + done + if [ "$allowed" -ne 0 ]; then + printf '#%s is [%s], not one of [%s] - leaving it alone.\n' \ + "$ISSUE" "$current_status" "${ALLOWED_CURRENT[*]}" + exit 0 + fi +fi + +gh api graphql -f query=' + mutation($project:ID!, $item:ID!, $field:ID!, $option:String!) { + updateProjectV2ItemFieldValue(input:{ + projectId:$project, itemId:$item, fieldId:$field, + value:{ singleSelectOptionId:$option } + }) { projectV2Item { id } } + }' -f project="$project_id" -f item="$item_id" -f field="$field_id" \ + -f option="$option_id" > /dev/null || exit 1 + +printf '#%s: %s -> %s\n' "$ISSUE" "$current_status" "$TARGET_STATUS" From 0c3ac169c6a8c849ba6e6216fe0281688456e1d1 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 20:39:55 -0400 Subject: [PATCH 62/75] feat: move issues across the board on push and pull request --- .github/workflows/issue-status.yml | 79 ++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .github/workflows/issue-status.yml diff --git a/.github/workflows/issue-status.yml b/.github/workflows/issue-status.yml new file mode 100644 index 0000000..af04b35 --- /dev/null +++ b/.github/workflows/issue-status.yml @@ -0,0 +1,79 @@ +name: Issue status + +# Moves issues across the project board as their work progresses. GitHub does +# not link an issue to a pull request based on dev, so the closing keywords are +# parsed out of the body here rather than read back from the API. +on: + push: + branches: ['issue-*'] + pull_request: + types: [opened, reopened, ready_for_review, edited, closed] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +env: + PROJECT_NUMBER: '1' + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + +jobs: + in-progress: + if: github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Move the issue to In progress + env: + BRANCH: ${{ github.ref_name }} + run: | + set -uo pipefail + issue="$(printf '%s' "$BRANCH" | sed -nE 's/^issue-([0-9]+)-.*/\1/p')" + if [ -z "$issue" ]; then + echo "Branch $BRANCH does not name an issue - nothing to do." + exit 0 + fi + # Todo or unset only, so a later push cannot pull it out of In review. + bash scripts/set-issue-status.sh "$issue" "In progress" Todo unset + + pull-request: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Move the referenced issues + env: + # Through the environment, never interpolated into the script: a body + # is attacker controllable text. + BODY: ${{ github.event.pull_request.body }} + MERGED: ${{ github.event.pull_request.merged }} + BASE: ${{ github.event.pull_request.base.ref }} + ACTION: ${{ github.event.action }} + run: | + set -uo pipefail + source scripts/issue-status.sh + + if [ "$ACTION" = "closed" ]; then + if [ "$MERGED" != "true" ] || [ "$BASE" != "dev" ]; then + echo "Closed without merging into dev - nothing to do." + exit 0 + fi + status="Ready for release" + else + status="In review" + fi + + refs="$(printf '%s' "$BODY" | closing_refs)" + if [ -z "$refs" ]; then + echo "No closing references in the body - nothing to do." + exit 0 + fi + + while IFS= read -r issue; do + [ -n "$issue" ] || continue + bash scripts/set-issue-status.sh "$issue" "$status" + done <<< "$refs" From c8863ccace7a7a160654a43cb77ed39049b0ba4b Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 20:45:04 -0400 Subject: [PATCH 63/75] fix: guard the pull request status write and skip fork runs Editing an already merged pull request's title or body could pull an issue backwards out of Ready for release, since the pull request job had no allowed-current guard on its non-closed path. Add one. Also skip the job entirely for fork-originated pull requests, since GitHub withholds secrets from those runs and the first API call would fail for a reason the contributor cannot fix. Scope GITHUB_TOKEN permissions down to none, since this workflow only ever uses the PAT in GH_TOKEN. And track failures across the issue loop instead of letting only the last invocation decide the step's exit code. --- .github/workflows/issue-status.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-status.yml b/.github/workflows/issue-status.yml index af04b35..fecef79 100644 --- a/.github/workflows/issue-status.yml +++ b/.github/workflows/issue-status.yml @@ -13,6 +13,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false +permissions: {} + env: PROJECT_NUMBER: '1' GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} @@ -39,7 +41,9 @@ jobs: bash scripts/set-issue-status.sh "$issue" "In progress" Todo unset pull-request: - if: github.event_name == 'pull_request' + # Fork runs get no secrets, so GH_TOKEN would be empty and every external + # contribution would show a red check it has no way to fix. + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest steps: - name: Check out @@ -63,8 +67,12 @@ jobs: exit 0 fi status="Ready for release" + allowed=() else status="In review" + # Guards an edit on an already merged pull request from pulling + # the issue back out of Ready for release or Done. + allowed=(Todo unset "In progress") fi refs="$(printf '%s' "$BODY" | closing_refs)" @@ -73,7 +81,15 @@ jobs: exit 0 fi + failures=0 while IFS= read -r issue; do [ -n "$issue" ] || continue - bash scripts/set-issue-status.sh "$issue" "$status" + if ! bash scripts/set-issue-status.sh "$issue" "$status" "${allowed[@]}"; then + printf 'FAILED: could not set #%s to %s\n' "$issue" "$status" >&2 + failures=$((failures + 1)) + fi done <<< "$refs" + + if [ "$failures" -gt 0 ]; then + exit 1 + fi From 5c98bc50b7ac1bacdbea283d418d7809a492c24c Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 20:57:39 -0400 Subject: [PATCH 64/75] fix: guard issue status automation against three failure modes Down-merges from master to dev can repeat a Closes reference for an issue that already shipped; the merged-into-dev path now allows every status except Done, so a merge still beats an earlier state without dragging a shipped issue backwards. An empty PREV_TAG with tags already present in the repository now fails fast instead of silently scanning all of history and closing every issue any pull request ever referenced; a genuine first release with no tags at all is unaffected. A branch deletion after its pull request merges can fire a push event with no branch left to check out; the push job now skips deleted refs. --- .github/workflows/issue-status.yml | 10 ++++++++-- scripts/close-released-issues.sh | 6 ++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-status.yml b/.github/workflows/issue-status.yml index fecef79..09c2862 100644 --- a/.github/workflows/issue-status.yml +++ b/.github/workflows/issue-status.yml @@ -21,7 +21,9 @@ env: jobs: in-progress: - if: github.event_name == 'push' + # A branch deletion after its pull request merges can also fire a push + # event, with no branch left to check out. + if: github.event_name == 'push' && github.event.deleted == false runs-on: ubuntu-latest steps: - name: Check out @@ -67,7 +69,11 @@ jobs: exit 0 fi status="Ready for release" - allowed=() + # Every state except Done: a merge into dev always beats an + # earlier state, but a down-merge from master can repeat a + # Closes reference for an issue that already shipped, and that + # must not drag it backwards out of Done. + allowed=(Todo unset "In progress" "In review" "Ready for release") else status="In review" # Guards an edit on an already merged pull request from pulling diff --git a/scripts/close-released-issues.sh b/scripts/close-released-issues.sh index 1979a80..19ef826 100644 --- a/scripts/close-released-issues.sh +++ b/scripts/close-released-issues.sh @@ -13,6 +13,12 @@ VERSION="${VERSION:?VERSION must be set}" PREV_TAG="${PREV_TAG:-}" DRY_RUN="${DRY_RUN:-0}" +if [ -z "$PREV_TAG" ] && [ -n "$(git tag --list)" ]; then + echo "Error: PREV_TAG is empty but the repository already has tags - refusing to scan all of history." >&2 + echo "This usually means the checkout is missing tags or full history. Fix the checkout rather than closing every referenced issue." >&2 + exit 1 +fi + range="HEAD" if [ -n "$PREV_TAG" ]; then range="$PREV_TAG..HEAD" From 5db506f300ca389d1c2f6197716ddf48e4c37b6e Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 21:45:03 -0400 Subject: [PATCH 65/75] fix: keep a branch under review from falling back to In progress The push job chose its target from the branch name alone, so merging the base branch into a branch that already had an open pull request reported the issue as in progress. The Todo and unset guard could not help: it stops an issue being pulled out of In review, but an issue that never reached In review is not protected by it. The job now asks whether the branch has an open pull request and aims for In review when it does, advancing from In progress as well as Todo and unset. Neither plan names Ready for release or Done, so a push still cannot disturb a shipped issue. The decision lives in push_plan so it is covered by the suite. --- .github/workflows/issue-status.yml | 19 ++++++++++-- scripts/issue-status.sh | 15 +++++++++ scripts/test-issue-status.sh | 50 ++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issue-status.yml b/.github/workflows/issue-status.yml index 09c2862..96a058b 100644 --- a/.github/workflows/issue-status.yml +++ b/.github/workflows/issue-status.yml @@ -29,18 +29,31 @@ jobs: - name: Check out uses: actions/checkout@v4 - - name: Move the issue to In progress + - name: Move the issue along env: BRANCH: ${{ github.ref_name }} run: | set -uo pipefail + source scripts/issue-status.sh + issue="$(printf '%s' "$BRANCH" | sed -nE 's/^issue-([0-9]+)-.*/\1/p')" if [ -z "$issue" ]; then echo "Branch $BRANCH does not name an issue - nothing to do." exit 0 fi - # Todo or unset only, so a later push cannot pull it out of In review. - bash scripts/set-issue-status.sh "$issue" "In progress" Todo unset + + # Pushing to a branch that is already under review, by merging the + # base branch in for example, must not report it as in progress. + # synchronize is not a trigger type, so this job is the only thing + # that runs on such a push. + if [ "$(gh pr list --head "$BRANCH" --state open --json number --jq 'length')" = "0" ]; then + state="none" + else + state="open-pr" + fi + + mapfile -t plan < <(push_plan "$state") + bash scripts/set-issue-status.sh "$issue" "${plan[0]}" "${plan[@]:1}" pull-request: # Fork runs get no secrets, so GH_TOKEN would be empty and every external diff --git a/scripts/issue-status.sh b/scripts/issue-status.sh index dfef119..6c3be92 100644 --- a/scripts/issue-status.sh +++ b/scripts/issue-status.sh @@ -28,3 +28,18 @@ pr_numbers_from_log() { | awk '!seen[$0]++' return 0 } + +# What a push should do, given `open-pr` when the branch already has an open +# pull request and anything else when it does not. Prints the target status +# first, then the states it may advance from, one per line. +# +# A branch under review must not be reported as work in progress: merging the +# base branch in is a push like any other. Neither plan names Ready for release +# or Done, so a push can never pull a shipped issue backwards. +push_plan() { + if [ "${1:-}" = "open-pr" ]; then + printf 'In review\nTodo\nunset\nIn progress\n' + else + printf 'In progress\nTodo\nunset\n' + fi +} diff --git a/scripts/test-issue-status.sh b/scripts/test-issue-status.sh index 4328d49..51dfb47 100644 --- a/scripts/test-issue-status.sh +++ b/scripts/test-issue-status.sh @@ -163,6 +163,50 @@ Merge pull request #31 from Blockframe-Studios/fix/bukkitdev-metadata-semicolon' assert_equals "deduped" "31" "$(prs_of "$log")" } +# Joins a push plan with commas so an expectation reads as one string. +plan_of() { + push_plan "$1" | paste -sd, - +} + +# -- what a push should aim for -- + +test_a_branch_under_review_aims_for_in_review() { + assert_equals "target under review" "In review" "$(push_plan open-pr | head -1)" +} + +test_a_branch_under_review_may_advance_from_in_progress() { + assert_equals "allowed under review" "In review,Todo,unset,In progress" \ + "$(plan_of open-pr)" +} + +test_a_branch_with_no_pull_request_aims_for_in_progress() { + assert_equals "target with no pull request" "In progress" \ + "$(push_plan none | head -1)" +} + +test_a_branch_with_no_pull_request_cannot_leave_in_progress() { + assert_equals "allowed with no pull request" "In progress,Todo,unset" \ + "$(plan_of none)" +} + +# Neither plan lists Ready for release or Done, so a push can never pull an +# issue back out of either. +test_neither_plan_can_disturb_a_shipped_issue() { + local plan + for arg in open-pr none; do + plan="$(plan_of "$arg")" + # Asserted first, so an empty plan cannot pass this test by matching nothing. + if [ -z "$plan" ]; then + fail "$arg plan is empty" + continue + fi + case "$plan" in + *"Ready for release"*|*Done*) fail "$arg plan must not list a shipped state" ;; + *) pass "$arg plan leaves shipped states alone" ;; + esac + done +} + test_several_merges_keep_first_appearance_order() { local log log='Merge pull request #58 from Blockframe-Studios/issue-53-refuse-alongside-v1 @@ -192,6 +236,12 @@ test_an_empty_range_is_empty_and_clean test_repeated_numbers_collapse test_several_merges_keep_first_appearance_order +test_a_branch_under_review_aims_for_in_review +test_a_branch_under_review_may_advance_from_in_progress +test_a_branch_with_no_pull_request_aims_for_in_progress +test_a_branch_with_no_pull_request_cannot_leave_in_progress +test_neither_plan_can_disturb_a_shipped_issue + printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" if [ "$FAIL" -gt 0 ]; then exit 1 From 6fccfdb1a11a33330b8a1253a669abdac17427af Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 22:09:41 -0400 Subject: [PATCH 66/75] update readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 60ba699..0ff2834 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ That is genuinely the whole setup. Player permissions default to granted, so you Names are optional on `/sethome` and `/home`. Leave the name off and both use a home called `default`. Home names are unique per player and ignore case, so `base` and `Base` are the same home.
-Every command, with long forms and admin commands +All commands + alias | Command | Long form | What it does | | --- | --- | --- | @@ -50,8 +50,10 @@ Names are optional on `/sethome` and `/home`. Leave the name off and both use a | `/give-homes-item` | - | Gives you the item that opens the menu. | On `/sethome`, a second word that names a real item becomes the icon, and everything after it is the description. So `/sethome base stone house` creates `base` with a stone icon and the description "house". If you wanted the whole phrase as the description, put `d` in the icon position: `/sethome base d stone house`. The reply names the icon it chose, so there is never any guessing. +
-**Admin commands** +
+Admin commands | Command | What it does | | --- | --- | From 350e4896c22fef9571e6c605f6413b3230cbb1f1 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Mon, 17 Aug 2026 22:21:06 -0400 Subject: [PATCH 67/75] docs: correct the migration order, and add the rollback path The steps told admins to import first and move the old jar afterwards, which the v1 jar refusal now makes impossible rather than merely unwise. Reordered into six steps with the swap before the import, and folded the old callout into a paragraph that explains why instead of repeating the instruction. Adds the rollback path and its one-way limitation, says a pasted config setting does nothing until the server restarts, and notes that a v1 server without a permissions plugin was never enforcing max-homes in the first place. Also corrects the EssentialsX FAQ, which still said the command clash goes to whichever plugin loads last and told readers to import before removing it. --- README.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0ff2834..2cb3b98 100644 --- a/README.md +++ b/README.md @@ -229,13 +229,18 @@ To pick one up, copy the key you want out of [`default-config.yml`](https://gith Your players keep their homes. The old plugin does not even need to be running, because the importer reads its data files directly. -**Coming from Set Homes v1, move the old jar out of `plugins/` first and keep it.** Both plugins provide `/sethome`, `/home` and `/delhome`, and v1 wins those names whatever the load order, so homes created after the upgrade would go into v1's files while the menu read ours. Rather than let that happen quietly, Set Homes refuses to start while a Set Homes v1 jar is installed, and prints what to do in the console. Your server keeps running v1 exactly as before until you move the jar. Leave the `plugins/SetHomes/` folder itself alone; the importer reads it and never writes to it. +1. **Stop the server.** +2. **Move the old plugin's jar out of `plugins/`** and keep it somewhere safe rather than deleting it. That jar is your way back. Leave its data folder exactly where it is: the importer reads `plugins/SetHomes/` or `plugins/Essentials/userdata/` and never writes to either. +3. **Start the server.** Set Homes creates its own folder and an empty database. No homes are visible yet, and the console tells you how many are waiting. +4. **Run `/import-homes sethomes`** (or `/import-homes essentialsx`). This is a **preview only**. It reports how many homes it would import and warns about any it would skip, and changes nothing. +5. **Happy with the numbers?** Run it again with `confirm` on the end. +6. **Paste any settings the config report listed** into `plugins/SetHomesTwo/config.yml`, then restart the server. Nothing is written there automatically, and there is no in-game reload, so a pasted setting does nothing until the server comes back up. -1. Run `/import-homes essentialsx` (or `/import-homes sethomes`). This is a **preview only**. It reports how many homes it would import and warns about any it would skip, and changes nothing. -2. Happy with the numbers? Run it again with `confirm` on the end. -3. Move the old jar out of `plugins/`. Keep it somewhere safe rather than deleting it, so you can go back if you want to. +**The jar has to move before the import, not after.** Set Homes refuses to start while a Set Homes v1 jar is still in `plugins/`, and prints what to move in the console. Both plugins provide `/sethome`, `/home` and `/delhome`, and v1 wins those names whatever the load order, so homes created after the upgrade would go into v1's files while the menu read ours. Rather than let that happen quietly, Set Homes stays off and your server keeps running v1 exactly as before until you move the jar. -**You will not silently end up with an empty homes list.** Once the old jar is out and Set Homes starts, if `plugins/SetHomes/homes.yml` still holds homes and none have been imported here yet, the console says so at startup, naming the file, how many are waiting and the command to run. Anyone holding `sh2.import-homes` gets the same reminder in chat when they join, because plenty of admins never read the console. Both stop for good the moment any home exists here, so there is nothing to switch off afterwards. To reword the chat line, set `v1ImportPending` in `config.yml`. +**You will not silently end up with an empty homes list.** After step 3, if `plugins/SetHomes/homes.yml` still holds homes and none have been imported here yet, the console says so at startup, naming the file, how many are waiting and the command to run. Anyone holding `sh2.import-homes` gets the same reminder in chat when they join, because plenty of admins never read the console. Both stop for good the moment any home exists here, so there is nothing to switch off afterwards. To reword the chat line, set `v1ImportPending` in `config.yml`. + +**Rolling back.** Put the old jar back in `plugins/`, take the Set Homes jar out, and restart. Both data folders are still there and untouched, so the old plugin picks up exactly where it left off. There is one thing to know before you rely on that: homes your players create under Set Homes after the migration exist only in `plugins/SetHomesTwo/database/homes.db`, and the old plugin cannot read them. They are not destroyed, and coming back to Set Homes restores them, but they are invisible for as long as you stay rolled back. So roll back promptly if you are going to, keep both data folders, and re-import when you return: homes created in the old plugin during the rollback are absent from the Set Homes database, so a later import brings those across too. Existing homes are never overwritten, so re-running the import is always safe. Homes in worlds that no longer exist are skipped with a warning naming the world. @@ -307,6 +312,8 @@ Worth knowing before you copy a permissions file across: | `tp-cooldown` | none | v2 has no cooldown feature | | `tp-cooldown-msg` | none | follows the above | +**If your v1 server had no permissions plugin, `max-homes` was never in force.** v1 logs `Could not connect to a permissions plugin! Config setting "max-homes" will be ignored!` at startup and ignores the caps entirely, so the numbers in your v1 config may never have applied to anyone. Copying them across does not newly restrict your players either: Set Homes in `groups` mode also declines to enforce without LuckPerms, and says so at startup. Install [LuckPerms](https://luckperms.net/download) if you want per-rank limits to actually take effect. +
## FAQ @@ -349,7 +356,9 @@ Install LuckPerms, set `maxHomeEnabled: true` and `maxHomesType: groups`, then r
Can I run it alongside EssentialsX? -Not comfortably. Both register `/sethome`, `/home` and `/delhome`, and whichever loads last wins. Import your homes, then remove EssentialsX. +You can, but the two will split three command names between them. EssentialsX declares `/sethome`, `/home` and `/delhome` as its own commands, while Set Homes declares them as aliases of `/create-home`, `/go-home` and `/delete-home`. Bukkit never lets an alias take a name another plugin already owns, so EssentialsX keeps all three whatever the load order, and homes set with them go into EssentialsX's files while `/homes` reads ours. + +Unlike Set Homes v1, EssentialsX does not stop Set Homes from starting, so nothing warns you about the split. If you want Set Homes handling homes, follow the migration steps above, moving the EssentialsX jar out before you import. If you keep EssentialsX for everything else it does, disable those three commands in its own config instead. Set Homes' own names, `/homes`, `/list-homes`, `/create-home`, `/go-home` and `/delete-home`, always reach Set Homes either way.
From a0915a2ca089ae18e486542bee0006ad87b87e6b Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Tue, 18 Aug 2026 10:00:54 -0400 Subject: [PATCH 68/75] update readme --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2cb3b98..4296b7c 100644 --- a/README.md +++ b/README.md @@ -216,14 +216,9 @@ Per-rank limits need [LuckPerms](https://luckperms.net/download) and `maxHomesTy That table is only the common settings. For the complete list, see [`default-config.yml`](https://github.com/Blockframe-Studios/SetHomesTwo/blob/master/src/main/resources/default-config.yml), the file your `config.yml` is first written from. Every setting the plugin has is in there, commented in place. -
-Upgrading? Your existing config.yml will not gain the new settings - -Set Homes never touches a `config.yml` that already exists, so settings added in a later release do not appear in a file written by an earlier one. Any missing setting quietly falls back to its default, so nothing breaks, but you cannot change a setting you cannot see. +**Your `config.yml` is written once and never touched again.** Installing the plugin, updating the jar and restarting the server all leave the file exactly as you last saved it, so a setting added in a later release will not appear in a file written by an earlier one. Anything missing falls back to its default, so nothing breaks, but you cannot change a setting you cannot see. -To pick one up, copy the key you want out of [`default-config.yml`](https://github.com/Blockframe-Studios/SetHomesTwo/blob/master/src/main/resources/default-config.yml) into your file and restart. To start clean, rename your `config.yml` and restart. A fresh one is written with everything in it, and you can copy your old values across. - -
+To pick a new setting up, copy the key out of [`default-config.yml`](https://github.com/Blockframe-Studios/SetHomesTwo/blob/master/src/main/resources/default-config.yml) into your file and restart. To start clean, rename your `config.yml` and restart. A fresh one is written with everything in it, and you can copy your old values across. ## Coming from EssentialsX or Set Homes v1 From 9610d5cf0808d44874ece33de615fc5b39cf4d65 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Tue, 18 Aug 2026 10:08:44 -0400 Subject: [PATCH 69/75] update readme --- README.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4296b7c..9982373 100644 --- a/README.md +++ b/README.md @@ -231,15 +231,27 @@ Your players keep their homes. The old plugin does not even need to be running, 5. **Happy with the numbers?** Run it again with `confirm` on the end. 6. **Paste any settings the config report listed** into `plugins/SetHomesTwo/config.yml`, then restart the server. Nothing is written there automatically, and there is no in-game reload, so a pasted setting does nothing until the server comes back up. -**The jar has to move before the import, not after.** Set Homes refuses to start while a Set Homes v1 jar is still in `plugins/`, and prints what to move in the console. Both plugins provide `/sethome`, `/home` and `/delhome`, and v1 wins those names whatever the load order, so homes created after the upgrade would go into v1's files while the menu read ours. Rather than let that happen quietly, Set Homes stays off and your server keeps running v1 exactly as before until you move the jar. +**The jar has to move before the import, not after.** Set Homes v2 will not start while a Set Homes v1 jar is still in `plugins/`. Both plugins provide `/sethome`, `/home` and `/delhome`, and v1 usually takes priority and wins those names regardless the load order. -**You will not silently end up with an empty homes list.** After step 3, if `plugins/SetHomes/homes.yml` still holds homes and none have been imported here yet, the console says so at startup, naming the file, how many are waiting and the command to run. Anyone holding `sh2.import-homes` gets the same reminder in chat when they join, because plenty of admins never read the console. Both stop for good the moment any home exists here, so there is nothing to switch off afterwards. To reword the chat line, set `v1ImportPending` in `config.yml`. - -**Rolling back.** Put the old jar back in `plugins/`, take the Set Homes jar out, and restart. Both data folders are still there and untouched, so the old plugin picks up exactly where it left off. There is one thing to know before you rely on that: homes your players create under Set Homes after the migration exist only in `plugins/SetHomesTwo/database/homes.db`, and the old plugin cannot read them. They are not destroyed, and coming back to Set Homes restores them, but they are invisible for as long as you stay rolled back. So roll back promptly if you are going to, keep both data folders, and re-import when you return: homes created in the old plugin during the rollback are absent from the Set Homes database, so a later import brings those across too. +**You are reminded until you import.** While `plugins/SetHomes/homes.yml` still holds homes and none have been imported here, every startup says so in the console, and anyone holding `sh2.import-homes` gets the same reminder in chat on join. Both stop for good once any home exists here. Existing homes are never overwritten, so re-running the import is always safe. Homes in worlds that no longer exist are skipped with a warning naming the world. -Set Homes v1 told `base` and `Base` apart, while home names here ignore case. A player holding both keeps both: the second one is imported under the next free name, so `Base` arrives as `Base2`, and the report and the server log name it. No home is dropped for a name clash. +Set Homes v1 told `base` and `Base` apart, while home names here ignore case. A player holding both keeps both: the second is imported under the next free name, so `Base` arrives as `Base2`, named in the report and the server log. + +**You can go back at any time.** The old plugin's data folder is never written to, so rolling back is mostly putting its jar back. +
+Rolling back to your old plugin + +1. **Stop the server.** +2. **Put the old plugin's jar back in `plugins/`** and take the Set Homes jar out. +3. **Start the server.** Both data folders are untouched, so the old plugin picks up where it left off. + +Homes your players created under Set Homes live only in `plugins/SetHomesTwo/database/homes.db`, which the old plugin cannot read, so they are invisible while you stay rolled back. Nothing is lost, and they come back with the plugin. Re-import when you return, to pick up anything created in the old plugin meanwhile. + +
+ +
What else the Set Homes v1 import brings across From 14d85fd366f28e2ed05ac5109eba0173fa060bce Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Tue, 18 Aug 2026 10:36:26 -0400 Subject: [PATCH 70/75] ci: publish to the Set Homes listing as SetHomes.V.jar Point the release at BukkitDev project 312833, the listing v1 servers already watch, so v2 arrives as the update on the page they know rather than a download they have to go and find. The published artifact and the GitHub Release become SetHomes.V.jar and "Set Homes V". The script comment named the two project ids the wrong way round; that is corrected. Also refuse to publish anything below 2.0.0. v1 last shipped 1.3.1 and the pom is at 1.2.2, so until the major bump is made by hand at promotion a push to master would offer those servers a lower version than they are running. The guard turns that into a failed release step instead of a public downgrade, and sits beside the existing game-version sanity check. plugin.yml's name, the data folder and the Maven artifact are unchanged. --- .changeset/quiet-herons-arrive.md | 5 +++++ .github/workflows/release.yml | 6 +++--- scripts/publish-bukkitdev.sh | 17 +++++++++++++---- scripts/test-publish-bukkitdev.sh | 24 ++++++++++++++++++++++-- 4 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 .changeset/quiet-herons-arrive.md diff --git a/.changeset/quiet-herons-arrive.md b/.changeset/quiet-herons-arrive.md new file mode 100644 index 0000000..bfa2fce --- /dev/null +++ b/.changeset/quiet-herons-arrive.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +Set Homes now publishes to the original Set Homes project page, so servers running the older Set Homes are offered this as an update on the page they already watch. The download is named SetHomes rather than SetHomesTwo. The plugin folder and your existing config.yml are unaffected. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b813681..a9b2327 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -158,7 +158,7 @@ jobs: if: steps.plan.outputs.release == 'true' && !inputs.dry_run env: NEXT: ${{ steps.plan.outputs.next }} - run: cp "target/SetHomesTwo-$NEXT.jar" "SetHomesTwo.V$NEXT.jar" + run: cp "target/SetHomesTwo-$NEXT.jar" "SetHomes.V$NEXT.jar" - name: Publish to BukkitDev if: steps.plan.outputs.release == 'true' && !inputs.dry_run @@ -174,8 +174,8 @@ jobs: VERSION: ${{ steps.plan.outputs.next }} run: | NOTES=$(bash scripts/release.sh notes --readme README.md --version "$VERSION") - gh release create "v$VERSION" "SetHomesTwo.V$VERSION.jar" \ - --title "SetHomesTwo V$VERSION" \ + gh release create "v$VERSION" "SetHomes.V$VERSION.jar" \ + --title "Set Homes V$VERSION" \ --notes "$NOTES" # An issue closes when the commit that fixed it is contained in the commit diff --git a/scripts/publish-bukkitdev.sh b/scripts/publish-bukkitdev.sh index 6362f70..cc4e62c 100755 --- a/scripts/publish-bukkitdev.sh +++ b/scripts/publish-bukkitdev.sh @@ -7,8 +7,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# 913275 is set-homes-two. Not 312833 - that is Set Homes v1. -PROJECT_ID=913275 +# 312833 is the Set Homes listing, the one v1 servers already watch for updates. +PROJECT_ID=312833 API=https://dev.bukkit.org/api if [ -z "${CURSEFORGE_TOKEN:-}" ]; then @@ -16,6 +16,15 @@ if [ -z "${CURSEFORGE_TOKEN:-}" ]; then exit 1 fi +# This listing carries Set Homes v1, which last shipped 1.3.1, so anything below +# 2.0.0 would reach those servers as an update that reads as a downgrade. +case "$VERSION" in + 0.*|1.*) + echo "$VERSION is below the 2.0.0 floor for this listing, refusing to publish" >&2 + exit 1 + ;; +esac + GAME_VERSIONS=$(curl -sS -H "X-Api-Token: $CURSEFORGE_TOKEN" "$API/game/versions" \ | jq -f "$SCRIPT_DIR/game-versions.jq" \ | jq -c 'map(.id)') @@ -40,7 +49,7 @@ CHANGELOG=$(bash "$SCRIPT_DIR/release.sh" notes --readme README.md --version "$V # backslashes and newlines. METADATA=$(jq -n \ --arg changelog "$CHANGELOG" \ - --arg displayName "SetHomesTwo V$VERSION" \ + --arg displayName "Set Homes V$VERSION" \ --argjson gameVersions "$GAME_VERSIONS" \ '{ changelog: $changelog, @@ -55,7 +64,7 @@ METADATA=$(jq -n \ RESPONSE=$(curl -sS -w '\n%{http_code}' -X POST "$API/projects/$PROJECT_ID/upload-file" \ -H "X-Api-Token: $CURSEFORGE_TOKEN" \ --form-string "metadata=$METADATA" \ - -F "file=@SetHomesTwo.V$VERSION.jar") + -F "file=@SetHomes.V$VERSION.jar") BODY=$(echo "$RESPONSE" | head -n -1) CODE=$(echo "$RESPONSE" | tail -n 1) diff --git a/scripts/test-publish-bukkitdev.sh b/scripts/test-publish-bukkitdev.sh index 15d19d5..40ef9d1 100644 --- a/scripts/test-publish-bukkitdev.sh +++ b/scripts/test-publish-bukkitdev.sh @@ -163,17 +163,37 @@ else fi DISPLAY_NAME=$(printf '%s' "$METADATA" | jq -r '.displayName // ""' 2>/dev/null) -if [ "$DISPLAY_NAME" = "SetHomesTwo V9.9.9" ]; then +if [ "$DISPLAY_NAME" = "Set Homes V9.9.9" ]; then pass "names the file after the version" else fail "names the file after the version" "got: $DISPLAY_NAME" fi -if has_arg "file=@SetHomesTwo.V9.9.9.jar"; then +if has_arg "file=@SetHomes.V9.9.9.jar"; then pass "uploads the versioned jar" else fail "uploads the versioned jar" "args were: $(args_summary)" fi +# The listing this publishes to carries Set Homes v1, which shipped 1.3.1, so a +# 1.x upload would be offered to those servers as an update. The guard runs +# before the first API call, so a refused run records no curl at all. +REFUSE_ARGS_DIR="$WORK/args-refused" +REFUSE_OUTPUT=$(cd "$WORK/run" && PATH="$WORK/bin:$PATH" CURSEFORGE_TOKEN=test-token \ + VERSION=1.2.3 CURL_ARGS_DIR="$REFUSE_ARGS_DIR" bash "$PUBLISH_SH" 2>&1) +REFUSE_STATUS=$? + +if [ "$REFUSE_STATUS" -ne 0 ]; then + pass "refuses to publish a version below 2.0.0" +else + fail "refuses to publish a version below 2.0.0" "exited 0: $REFUSE_OUTPUT" +fi + +if [ ! -d "$REFUSE_ARGS_DIR" ]; then + pass "calls nothing when it refuses" +else + fail "calls nothing when it refuses" "recorded: $(ls "$REFUSE_ARGS_DIR")" +fi + printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" [ "$FAIL" -eq 0 ] From b3ed58c17eccfb62f0bc8409587e9291ef07a3bb Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Tue, 18 Aug 2026 11:31:00 -0400 Subject: [PATCH 71/75] chore: use American spelling in user-facing text The migration rehearsal surfaced British spellings in text server owners actually read: the import report, two teleport messages, the config file comments and the README. House style is American English, so they now read capitalization, color and canceled. Also adds a missing word to the block printed when a Set Homes v1 jar is found alongside this one. It read "Nothing ever writes to it, but is needed for migrating homes to v2". The generated changelog entries keep the spelling they were published with, since they are a record of past releases rather than living text. Bukkit's own isCancelled spelling is untouched, and comments describing event cancellation keep it to match the API. --- .changeset/kind-herons-shine.md | 5 +++++ README.md | 8 ++++---- .../java/com/samleighton/sethomestwo/SetHomesTwo.java | 2 +- .../java/com/samleighton/sethomestwo/enums/UserError.java | 4 ++-- .../java/com/samleighton/sethomestwo/gui/HomesGui.java | 2 +- .../sethomestwo/importers/SetHomesV1Importer.java | 6 +++--- src/main/resources/default-config.yml | 8 ++++---- 7 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 .changeset/kind-herons-shine.md diff --git a/.changeset/kind-herons-shine.md b/.changeset/kind-herons-shine.md new file mode 100644 index 0000000..cc6309c --- /dev/null +++ b/.changeset/kind-herons-shine.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +Corrected the spelling in a few of the messages the plugin sends, so they read the same way as the rest of the plugin. The wording of the teleport and import messages is otherwise unchanged. An existing config.yml is not touched, so any message you have already customized stays exactly as you set it. diff --git a/README.md b/README.md index 9982373..01119eb 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ By default players wait three seconds before a teleport fires, and moving cancel ![Instant teleport](docs/img/teleport-instant.gif) -Before it drops anyone anywhere, Set Homes checks the destination is safe to stand in. If a home has been built over, flooded with lava, or left hanging above a drop, the player is moved to the nearest safe spot instead, or the teleport is cancelled and they are told why. Turn it off with `teleportSafety: false`. +Before it drops anyone anywhere, Set Homes checks the destination is safe to stand in. If a home has been built over, flooded with lava, or left hanging above a drop, the player is moved to the nearest safe spot instead, or the teleport is canceled and they are told why. Turn it off with `teleportSafety: false`. ## Permissions @@ -142,7 +142,7 @@ Two bundles group the nodes, so you can move a whole role in one line: | `sh2.update-notify` | OP | Being told on join that a newer release exists | | `sh2.bypass-max-homes` | OP | Creating homes past the configured maximum, whether the limit is server-wide or per group | | `sh2.bypass-blacklist` | OP | Creating a home in a blacklisted world, moving a home into one, and teleporting to a home already in one | -| `sh2.bypass-teleport-delay` | OP | Teleporting with no countdown, and not being cancelled by moving | +| `sh2.bypass-teleport-delay` | OP | Teleporting with no countdown, and not being canceled by moving | These nodes are granted by the bundles, which is why denying a bundle takes its whole set away at once. Granting or denying an individual node works exactly as the table describes. @@ -314,8 +314,8 @@ Worth knowing before you copy a permissions file across: | `tp-delay` | `delay` | direct | | `tp-cancelOnMove` | `cancelOnMove` | direct | | `max-homes.` | `maxHomes.` | also set `maxHomesType: groups` and `maxHomeEnabled: true`. A v1 value of `0` means unlimited; leave that group out of `maxHomes` in v2 rather than setting it to `0`, which would cap it at zero homes instead. | -| `max-homes-msg` | `maxHomesReached` | direct. v1's `§` colour codes paste in unchanged | -| `tp-cancelOnMove-msg` | `movedWhileTeleporting` | direct. v1's `§` colour codes paste in unchanged | +| `max-homes-msg` | `maxHomesReached` | direct. v1's `§` color codes paste in unchanged | +| `tp-cancelOnMove-msg` | `movedWhileTeleporting` | direct. v1's `§` color codes paste in unchanged | | `tp-cooldown` | none | v2 has no cooldown feature | | `tp-cooldown-msg` | none | follows the above | diff --git a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java index b23823f..dc4ae3e 100644 --- a/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java +++ b/src/main/java/com/samleighton/sethomestwo/SetHomesTwo.java @@ -162,7 +162,7 @@ private void refuseToRunAlongsideV1() { log.severe(" until you have migrated to v2."); log.severe(" It is how you roll back if you change your mind."); log.severe(" 3. Leave plugins/SetHomes/ folder where it is. Nothing ever"); - log.severe(" writes to it, but is needed for migrating homes to v2."); + log.severe(" writes to it, but it is needed for migrating homes to v2."); log.severe(" 4. Start the server, then run /import-homes sethomes."); log.severe(""); log.severe("Set Homes v1 is still running, exactly as it was."); diff --git a/src/main/java/com/samleighton/sethomestwo/enums/UserError.java b/src/main/java/com/samleighton/sethomestwo/enums/UserError.java index b07ea0f..3812d6f 100644 --- a/src/main/java/com/samleighton/sethomestwo/enums/UserError.java +++ b/src/main/java/com/samleighton/sethomestwo/enums/UserError.java @@ -12,9 +12,9 @@ public enum UserError { /** Teleport restriction */ TELEPORT_IS_BLACKLISTED("You cannot teleport to this home because the dimension it is in has been blacklisted."), DIMENSION_IS_BLACKLISTED("You cannot set a home in this dimension because it has been blacklisted."), - MOVED_WHILE_TELEPORTING("Your teleport has been cancelled because you have moved."), + MOVED_WHILE_TELEPORTING("Your teleport has been canceled because you have moved."), ALREADY_TELEPORTING("You cannot teleport while already teleporting."), - UNSAFE_HOME("Teleport cancelled: this home is not safe to stand in and no safe spot was found nearby."), + UNSAFE_HOME("Teleport canceled: this home is not safe to stand in and no safe spot was found nearby."), /** Command Input Errors */ DIMENSION_IS_NOT_BLACKLISTED("The %s dimension has not been blacklisted yet therefore you cannot remove it."), diff --git a/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java b/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java index 75441b6..d365ea5 100644 --- a/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java +++ b/src/main/java/com/samleighton/sethomestwo/gui/HomesGui.java @@ -231,7 +231,7 @@ public void onClick(InventoryClickEvent event, GuiSession session) { // Right-click opens management, left-click teleports. Management is only // offered on the viewer's own list; the admin view of another player's - // homes falls through to teleport behaviour on any click. + // homes falls through to teleport behavior on any click. if (event.isRightClick() && isOwnList) { if (!player.hasPermission("sh2.manage-homes")) return; diff --git a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java index 8e6b5d0..22e0e51 100644 --- a/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java +++ b/src/main/java/com/samleighton/sethomestwo/importers/SetHomesV1Importer.java @@ -93,7 +93,7 @@ private void importOne(HomesDao homesDao, NameLedger ledger, ImportReport report if (!storedName.equals(homeName)) { report.renamed++; String note = String.format( - "Home '%s' for player %s differs only in capitalisation from another of that player's homes, which Set Homes v1 allowed. It takes the name '%s' here, so both locations are kept.", + "Home '%s' for player %s differs only in capitalization from another of that player's homes, which Set Homes v1 allowed. It takes the name '%s' here, so both locations are kept.", homeName, playerUUID, storedName); report.warnings.add(note); if (!dryRun) Bukkit.getLogger().warning(note); @@ -272,13 +272,13 @@ private void reportConfig(File pluginsDir, ImportReport report) { } } - // A v1 message may carry section-sign colour codes, which chat would apply + // A v1 message may carry section-sign color codes, which chat would apply // to the rest of the line. Show them as & so the note stays legible. private static String messageNote(String v1Key, String v2Key, String value) { String shown = value.replace(ChatColor.COLOR_CHAR, '&'); String note = String.format("v1 %s -> set %s: '%s' in config.yml", v1Key, v2Key, shown); if (!shown.equals(value)) { - note += " (colour codes shown as &; copy the original from plugins/SetHomes/config.yml to keep them)"; + note += " (color codes shown as &; copy the original from plugins/SetHomes/config.yml to keep them)"; } return note; } diff --git a/src/main/resources/default-config.yml b/src/main/resources/default-config.yml index bbf7546..3445b70 100644 --- a/src/main/resources/default-config.yml +++ b/src/main/resources/default-config.yml @@ -88,7 +88,7 @@ v1ImportPending: "Set Homes v1 has %s home(s) waiting to be imported. Run /impor invalidHomeItem: "The material you entered is not valid, please try a different one." falseHomeItem: "This home item does not belong to you." teleportedWhileTeleporting: "You cannot teleport while already teleporting." -movedWhileTeleporting: "Your teleport has been cancelled because you have moved." +movedWhileTeleporting: "Your teleport has been canceled because you have moved." noHomes: "You have not created any homes yet. Use /create-home to make your first one." teleportToBlacklistedDimension: "You cannot teleport to this home because the dimension it is in has been blacklisted." maxHomesReached: "You have reached the maximum number of homes allowed." @@ -101,7 +101,7 @@ homeDoesNotExist: "The home '%s' does not exist." # Shown when a player name matches nobody online and nobody with saved homes. playerNotFound: "No player by that name is online or has any saved homes." -unsafeHome: "Teleport cancelled: this home is not safe to stand in and no safe spot was found nearby." +unsafeHome: "Teleport canceled: this home is not safe to stand in and no safe spot was found nearby." movedToSafeSpot: "Your home was not safe to stand in, so you were moved to the nearest safe spot." # -- DEBUGGING -- @@ -117,7 +117,7 @@ renamePromptTitle: "New home name" # Hint shown on each home in the homes list, below the description. # Only shown to players who can actually manage the home (sh2.manage-homes) -# and never on another player's homes. Supports '&' colour codes. +# and never on another player's homes. Supports '&' color codes. # Set to "" to hide it. manageHomeHint: "&7Right click to edit home" @@ -125,7 +125,7 @@ manageHomeHint: "&7Right click to edit home" maxHomeNameLength: 32 # Buttons in the management menu. -# Button names support '&' colour codes (e.g. &c for red). +# Button names support '&' color codes (e.g. &c for red). renameButtonItem: "name_tag" renameButtonName: "Rename" moveHomeButtonItem: "ender_pearl" From 4d9cdeb0b90c7e92d01c5c29613341ec997858f7 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Tue, 18 Aug 2026 11:59:00 -0400 Subject: [PATCH 72/75] docs: say that operators hold the bypass permissions by default The upgrade rehearsal showed an operator teleporting with no countdown on a server whose config sets a delay, with nothing in the release notes to explain it. sh2.admin defaults to op and grants all three bypass nodes as children. This is v1 parity rather than a new rule: v1's homes.config_bypass was a child of homes.*, which also defaulted to op, and it skipped the teleport delay in GoHome and the blacklist and home limit in SetHome and UpdateHome. So a server migrating from v1 sees no change. A server already on 1.2.2 does, because 1.2.2 had no bypass nodes at all, and that is the case this sentence covers. --- .changeset/tidy-wolves-travel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tidy-wolves-travel.md b/.changeset/tidy-wolves-travel.md index 541e1b1..4a365de 100644 --- a/.changeset/tidy-wolves-travel.md +++ b/.changeset/tidy-wolves-travel.md @@ -2,4 +2,4 @@ bump: patch --- -Permission defaults can now be changed from config.yml with no permissions plugin installed. Uncomment the permissions block and set any sh2 node to true, false, op or not-op. Two bundles, sh2.player and sh2.admin, move a whole role at once, and three new bypass permissions were added for admins: sh2.bypass-max-homes, sh2.bypass-blacklist and sh2.bypass-teleport-delay. +Permission defaults can now be changed from config.yml with no permissions plugin installed. Uncomment the permissions block and set any sh2 node to true, false, op or not-op. Two bundles, sh2.player and sh2.admin, move a whole role at once, and three new bypass permissions were added for admins: sh2.bypass-max-homes, sh2.bypass-blacklist and sh2.bypass-teleport-delay. Operators hold those three by default, the same way Set Homes v1 granted homes.config_bypass through homes.*, so an operator is not held to the teleport delay, the world blacklist or the home limit. Set any of them to false in the permissions block if you would rather they were. From 00b284e789d19e9b09b53bdb41829f0651b36b97 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Tue, 18 Aug 2026 14:42:37 -0400 Subject: [PATCH 73/75] changeset --- .changeset/brave-badgers-arrive.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/brave-badgers-arrive.md diff --git a/.changeset/brave-badgers-arrive.md b/.changeset/brave-badgers-arrive.md new file mode 100644 index 0000000..07d3acb --- /dev/null +++ b/.changeset/brave-badgers-arrive.md @@ -0,0 +1,5 @@ +--- +bump: major +--- + +Set Homes Two can now take over from Set Homes v1. /import-homes brings your homes and world blacklist across and lists any v1 config settings that have an equivalent here, the v1 commands and aliases work again, and the plugin tells you at startup when homes are still waiting to be imported. From c582e3be68bf39d446e8f1a053d2345d27068293 Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Tue, 18 Aug 2026 15:00:24 -0400 Subject: [PATCH 74/75] update pom to decrease generated jar file size --- pom.xml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5046ffa..a573952 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,23 @@ false + true + + + + net.wesjd:anvilgui + ** + + + org.bstats:* + ** + + + *:* + META-INF/maven/** + + net.wesjd.anvilgui @@ -127,7 +144,7 @@ org.jetbrains annotations RELEASE - compile + provided net.luckperms From e8127b3fff5b054eb85181835d30fba0c695a51e Mon Sep 17 00:00:00 2001 From: milanmalhotra Date: Tue, 18 Aug 2026 15:09:11 -0400 Subject: [PATCH 75/75] changeset --- .changeset/brave-pandas-sing.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/brave-pandas-sing.md diff --git a/.changeset/brave-pandas-sing.md b/.changeset/brave-pandas-sing.md new file mode 100644 index 0000000..20dba33 --- /dev/null +++ b/.changeset/brave-pandas-sing.md @@ -0,0 +1,5 @@ +--- +bump: patch +--- + +decrease generated jar file size

Source | Report a bug | Donate

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 F2>=$*EoJ}! literal 190349 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 ^;_yzuN!!PAbbKh39{gCO`B{tA?y3$d+ZpCBv; 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