diff --git a/bukkit/src/main/java/net/william278/husktowns/hook/PlaceholderAPIHook.java b/bukkit/src/main/java/net/william278/husktowns/hook/PlaceholderAPIHook.java index 38243356..330e1633 100644 --- a/bukkit/src/main/java/net/william278/husktowns/hook/PlaceholderAPIHook.java +++ b/bukkit/src/main/java/net/william278/husktowns/hook/PlaceholderAPIHook.java @@ -64,7 +64,7 @@ private HuskTownsExpansion(@NotNull HuskTowns plugin) { @Override public String onRequest(@Nullable OfflinePlayer offlinePlayer, @NotNull String params) { if (!plugin.isLoaded()) { - return plugin.getLocales().getRawLocale("not_applicable").orElse("N/A"); + return plugin.getLocales().getNotApplicable(); } // Ensure the player is online @@ -152,14 +152,15 @@ public String onRequest(@Nullable OfflinePlayer offlinePlayer, @NotNull String p case "town_money" -> plugin.getUserTown(player) .map(Member::town) .map(Town::getMoney) - .map(String::valueOf) + .map(plugin::formatMoney) .orElse(plugin.getLocales().getRawLocale("placeholder_not_in_town") .orElse("Not in town")); case "town_level_up_cost" -> plugin.getUserTown(player) .map(Member::town) - .map(town -> plugin.getLevels().getLevelUpCost(town.getLevel())) - .map(String::valueOf) + .map(town -> town.getLevel() >= plugin.getLevels().getMaxLevel() + ? plugin.getLocales().getNotApplicable() + : plugin.formatMoney(plugin.getLevels().getLevelUpCost(town.getLevel()))) .orElse(plugin.getLocales().getRawLocale("placeholder_not_in_town") .orElse("Not in town")); @@ -203,7 +204,7 @@ public String onRequest(@Nullable OfflinePlayer offlinePlayer, @NotNull String p case "current_location_plot_members" -> plugin.getClaimAt(player.getPosition()) .map(townClaim -> { final Claim claim = townClaim.claim(); - if (claim.getType() == Claim.Type.PLOT) { + if (claim.getType() != Claim.Type.PLOT) { return plugin.getLocales().getRawLocale("placeholder_not_a_plot") .orElse("Not a plot"); } @@ -218,7 +219,7 @@ public String onRequest(@Nullable OfflinePlayer offlinePlayer, @NotNull String p case "current_location_plot_managers" -> plugin.getClaimAt(player.getPosition()) .map(townClaim -> { final Claim claim = townClaim.claim(); - if (claim.getType() == Claim.Type.PLOT) { + if (claim.getType() != Claim.Type.PLOT) { return plugin.getLocales().getRawLocale("placeholder_not_a_plot") .orElse("Not a plot"); } @@ -239,7 +240,7 @@ public String onRequest(@Nullable OfflinePlayer offlinePlayer, @NotNull String p case "current_location_town_money" -> plugin.getClaimAt(player.getPosition()) .map(TownClaim::town) .map(Town::getMoney) - .map(String::valueOf) + .map(plugin::formatMoney) .orElse(plugin.getLocales().getRawLocale("placeholder_not_claimed") .orElse("Not claimed")); @@ -252,8 +253,9 @@ public String onRequest(@Nullable OfflinePlayer offlinePlayer, @NotNull String p case "current_location_town_level_up_cost" -> plugin.getClaimAt(player.getPosition()) .map(TownClaim::town) - .map(town -> plugin.getLevels().getLevelUpCost(town.getLevel())) - .map(String::valueOf) + .map(town -> town.getLevel() >= plugin.getLevels().getMaxLevel() + ? plugin.getLocales().getNotApplicable() + : plugin.formatMoney(plugin.getLevels().getLevelUpCost(town.getLevel()))) .orElse(plugin.getLocales().getRawLocale("placeholder_not_in_town") .orElse("Not in town")); @@ -300,14 +302,17 @@ private String getTownLeaderboard(@NotNull String identifier) { // Get the leaderboard index and return the sorted list element at the index try { - final int leaderboardIndex = Math.max(1, Integer.parseInt(identifier.substring(lastUnderscore + 1))); + final int leaderboardIndex = Math.max(1, Integer.parseInt( + identifier.substring(lastUnderscore + 1) + )); final List towns = getSortedTownList(identifier.substring(0, lastUnderscore)); if (towns == null) { return null; } - return towns.size() >= leaderboardIndex ? towns.get(leaderboardIndex - 1).getName() - : plugin.getLocales().getRawLocale("not_applicable").orElse("N/A"); + return towns.size() >= leaderboardIndex + ? towns.get(leaderboardIndex - 1).getName() + : plugin.getLocales().getNotApplicable(); } catch (NumberFormatException e) { return null; } diff --git a/bukkit/src/main/java/net/william278/husktowns/listener/BukkitInteractListener.java b/bukkit/src/main/java/net/william278/husktowns/listener/BukkitInteractListener.java index ec0614bf..df859f2e 100644 --- a/bukkit/src/main/java/net/william278/husktowns/listener/BukkitInteractListener.java +++ b/bukkit/src/main/java/net/william278/husktowns/listener/BukkitInteractListener.java @@ -25,7 +25,6 @@ import org.bukkit.FluidCollisionMode; import org.bukkit.Material; import org.bukkit.block.Block; -import org.bukkit.block.data.Openable; import org.bukkit.block.data.type.Sign; import org.bukkit.block.data.type.Switch; import org.bukkit.entity.Player; @@ -65,7 +64,7 @@ default void onPlayerInteract(@NotNull PlayerInteractEvent e) { if (block != null && e.useInteractedBlock() != Event.Result.DENY) { if (getListener().handler().cancelOperation(Operation.of( BukkitUser.adapt(e.getPlayer()), - block.getBlockData() instanceof Openable || block.getState() instanceof InventoryHolder ? Operation.Type.CONTAINER_OPEN + block.getState() instanceof InventoryHolder ? Operation.Type.CONTAINER_OPEN : getPlugin().getSpecialTypes().isFarmBlock(block.getType().getKey().toString()) ? Operation.Type.FARM_BLOCK_INTERACT : block.getBlockData() instanceof Switch ? Operation.Type.REDSTONE_INTERACT : block.getBlockData() instanceof Sign ? Operation.Type.BLOCK_PLACE @@ -190,7 +189,7 @@ default void onPlayerInteractEntity(@NotNull PlayerInteractEntityEvent e) { default void onPlayerArmorStand(@NotNull PlayerArmorStandManipulateEvent e) { if (getListener().handler().cancelOperation(Operation.of( BukkitUser.adapt(e.getPlayer()), - Operation.Type.ENTITY_INTERACT, + Operation.Type.CONTAINER_OPEN, getPosition(e.getRightClicked().getLocation()) ))) { e.setCancelled(true); diff --git a/bukkit/src/main/resources/commodore/town.commodore b/bukkit/src/main/resources/commodore/town.commodore index cdc6e147..a807f8f0 100644 --- a/bukkit/src/main/resources/commodore/town.commodore +++ b/bukkit/src/main/resources/commodore/town.commodore @@ -58,6 +58,7 @@ town { farm; plot { members; + claim; add { player brigadier:string single_word { manager; diff --git a/bukkit/src/test/java/net/william278/husktowns/BukkitPluginTests.java b/bukkit/src/test/java/net/william278/husktowns/BukkitPluginTests.java index b0c26d2f..4e151ff4 100644 --- a/bukkit/src/test/java/net/william278/husktowns/BukkitPluginTests.java +++ b/bukkit/src/test/java/net/william278/husktowns/BukkitPluginTests.java @@ -127,8 +127,8 @@ public class PruningTests { "100d-Prune", 100L, // Records that should not be pruned - "90d-Leave", 90L, "80d-Leave", 80L, + "40d-Leave", 40L, "0d-Leave", 0L ); private static final long PRUNE_AFTER_DAYS = 90L; @@ -330,6 +330,28 @@ private static Stream getTownAndMayorParameters() { } } + @Order(5) + @Nested + @DisplayName("Town Schema Update Tests") + public class TownSchemaUpdateTests { + + @Order(1) + @DisplayName("Test Town Schema Update") + @Test + public void testTownSchemaUpdate() { + final String townJson = String.join("\n", readTestData("town_schema_v0.json")); + final Town readTown = plugin.getTownFromJson(townJson); + Assertions.assertNotNull(readTown); + Assertions.assertAll( + () -> Assertions.assertEquals(Town.CURRENT_SCHEMA, readTown.getSchemaVersion()), + () -> Assertions.assertEquals("Test", readTown.getBio().orElseThrow()), + () -> Assertions.assertEquals("Test", readTown.getGreeting().orElseThrow()), + () -> Assertions.assertEquals("Test", readTown.getFarewell().orElseThrow()) + ); + } + + } + @NotNull private static Player makePlayer() { final Player player = server.addPlayer(); @@ -360,4 +382,5 @@ private static List readTestData(@NotNull String fileName) { } return townNames; } + } diff --git a/bukkit/src/test/resources/town_schema_v0.json b/bukkit/src/test/resources/town_schema_v0.json new file mode 100644 index 00000000..76c7eae4 --- /dev/null +++ b/bukkit/src/test/resources/town_schema_v0.json @@ -0,0 +1,16 @@ +{ + "id": 1, + "name": "Schema0", + "greeting": "Test", + "bio": "Test", + "farewell": "Test", + "members": {}, + "rules": {}, + "claims": 0, + "level": 1, + "money": 0.0, + "bonuses": {}, + "log": {}, + "relations": {}, + "metadata": {} +} \ No newline at end of file diff --git a/common/src/main/java/net/william278/husktowns/audit/Action.java b/common/src/main/java/net/william278/husktowns/audit/Action.java index c3038afe..e1332b96 100644 --- a/common/src/main/java/net/william278/husktowns/audit/Action.java +++ b/common/src/main/java/net/william278/husktowns/audit/Action.java @@ -162,6 +162,7 @@ public enum Type { MAKE_CLAIM_REGULAR, ADD_PLOT_MEMBER, REMOVE_PLOT_MEMBER, + CLAIM_VACANT_PLOT, DEPOSIT_MONEY, WITHDRAW_MONEY, SET_FLAG_RULE, diff --git a/common/src/main/java/net/william278/husktowns/command/TownCommand.java b/common/src/main/java/net/william278/husktowns/command/TownCommand.java index 93f8f399..0f88d87f 100644 --- a/common/src/main/java/net/william278/husktowns/command/TownCommand.java +++ b/common/src/main/java/net/william278/husktowns/command/TownCommand.java @@ -286,9 +286,9 @@ public void execute(@NotNull CommandUser executor, @NotNull String[] args) { .map(town -> locales.getRawLocale("town_list_item", Locales.escapeText(town.getName()), town.getColorRgb(), - Locales.escapeText(locales.wrapText(town.getBio() - .orElse(plugin.getLocales().getRawLocale("not_applicable") - .orElse("N/A")), 40)), + Locales.escapeText(locales.wrapText(town.getBio().orElse( + plugin.getLocales().getNotApplicable() + ), 40)), Integer.toString(town.getLevel()), Integer.toString(town.getClaimCount()), Integer.toString(town.getMaxClaims(plugin)), @@ -866,7 +866,7 @@ public void execute(@NotNull CommandUser executor, @NotNull String[] args) { private static class PlotCommand extends ChildCommand implements TabProvider { protected PlotCommand(@NotNull Command parent, @NotNull HuskTowns plugin) { - super("plot", List.of(), parent, "<(members)|( [manager])>", plugin); + super("plot", List.of(), parent, " [manager])>", plugin); } @Override @@ -898,6 +898,8 @@ public void execute(@NotNull CommandUser executor, @NotNull String[] args) { } plugin.getManager().claims().removePlotMember(user, user.getWorld(), user.getChunk(), target.get()); } + case "claim" -> plugin.getManager().claims() + .claimPlot(user, user.getWorld(), user.getChunk()); case "members", "memberlist", "list" -> plugin.getManager().claims() .listPlotMembers(user, user.getWorld(), user.getChunk()); default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage()) diff --git a/common/src/main/java/net/william278/husktowns/config/Levels.java b/common/src/main/java/net/william278/husktowns/config/Levels.java index 264e2ab5..26197a24 100644 --- a/common/src/main/java/net/william278/husktowns/config/Levels.java +++ b/common/src/main/java/net/william278/husktowns/config/Levels.java @@ -158,8 +158,8 @@ public double getMobSpawnerRateBonus(int level) { /** * Get the cost required to reach a certain level * - * @param level the next level - * @return the cost to upgrade from {@code level - 1} to {@code level} + * @param level the current level of the town + * @return the cost to upgrade from {@code level} to {@code level + 1} */ @NotNull public BigDecimal getLevelUpCost(int level) { diff --git a/common/src/main/java/net/william278/husktowns/config/Locales.java b/common/src/main/java/net/william278/husktowns/config/Locales.java index 12940adb..d9749e65 100644 --- a/common/src/main/java/net/william278/husktowns/config/Locales.java +++ b/common/src/main/java/net/william278/husktowns/config/Locales.java @@ -148,7 +148,7 @@ public static String escapeText(@NotNull String string) { @NotNull public String wrapText(@NotNull String string, int wrapAfter) { if (string.isBlank()) { - return this.getRawLocale("not_applicable").orElse("N/A"); + return getNotApplicable(); } return WordUtils.wrap(string, wrapAfter, "\n", true); } @@ -161,6 +161,11 @@ public String truncateText(@NotNull String string, int truncateAfter) { return string.length() > truncateAfter ? string.substring(0, truncateAfter) + "…" : string; } + @NotNull + public String getNotApplicable() { + return getRawLocale("not_applicable").orElse("N/A"); + } + @NotNull public ListOptions.Builder getBaseList(int itemsPerPage) { return new ListOptions.Builder() diff --git a/common/src/main/java/net/william278/husktowns/config/Roles.java b/common/src/main/java/net/william278/husktowns/config/Roles.java index 5bf55f4c..56cafe25 100644 --- a/common/src/main/java/net/william278/husktowns/config/Roles.java +++ b/common/src/main/java/net/william278/husktowns/config/Roles.java @@ -78,6 +78,7 @@ public class Roles { "1", List.of( Privilege.DEPOSIT.id(), Privilege.CHAT.id(), + Privilege.CLAIM_PLOT.id(), Privilege.SPAWN.id()) )); diff --git a/common/src/main/java/net/william278/husktowns/database/MySqlDatabase.java b/common/src/main/java/net/william278/husktowns/database/MySqlDatabase.java index c57e45d9..2102c217 100644 --- a/common/src/main/java/net/william278/husktowns/database/MySqlDatabase.java +++ b/common/src/main/java/net/william278/husktowns/database/MySqlDatabase.java @@ -221,7 +221,7 @@ public Optional getUser(@NotNull UUID uuid) { User.of(uuid, name), resultSet.getTimestamp("last_login").toLocalDateTime() .atOffset(OffsetDateTime.now().getOffset()), - plugin.getGson().fromJson(preferences, Preferences.class) + plugin.getPreferencesFromJson(preferences) )); } } @@ -248,7 +248,7 @@ public Optional getUser(@NotNull String username) { User.of(uuid, name), resultSet.getTimestamp("last_login").toLocalDateTime() .atOffset(OffsetDateTime.now().getOffset()), - plugin.getGson().fromJson(preferences, Preferences.class) + plugin.getPreferencesFromJson(preferences) )); } } @@ -276,7 +276,7 @@ public List getInactiveUsers(long daysInactive) { User.of(uuid, name), resultSet.getTimestamp("last_login").toLocalDateTime() .atOffset(OffsetDateTime.now().getOffset()), - plugin.getGson().fromJson(preferences, Preferences.class) + plugin.getPreferencesFromJson(preferences) )); } } @@ -345,7 +345,7 @@ public Optional getTown(int townId) { final ResultSet resultSet = statement.executeQuery(); if (resultSet.next()) { final String data = new String(resultSet.getBytes("data"), StandardCharsets.UTF_8); - final Town town = plugin.getGson().fromJson(data, Town.class); + final Town town = plugin.getTownFromJson(data); town.setId(resultSet.getInt("id")); return Optional.of(town); } @@ -366,7 +366,7 @@ public List getAllTowns() throws IllegalStateException { final ResultSet resultSet = statement.executeQuery(); while (resultSet.next()) { final String data = new String(resultSet.getBytes("data"), StandardCharsets.UTF_8); - final Town town = plugin.getGson().fromJson(data, Town.class); + final Town town = plugin.getTownFromJson(data); if (town != null) { town.setId(resultSet.getInt("id")); towns.add(town); @@ -461,7 +461,7 @@ public Map getClaimWorlds(@NotNull String server) throws Ille final World world = World.of(UUID.fromString(resultSet.getString("world_uuid")), resultSet.getString("world_name"), resultSet.getString("world_environment")); - final ClaimWorld claimWorld = plugin.getGson().fromJson(data, ClaimWorld.class); + final ClaimWorld claimWorld = plugin.getClaimWorldFromJson(data); claimWorld.updateId(resultSet.getInt("id")); if (!plugin.getSettings().isUnclaimableWorld(world)) { worlds.put(world, claimWorld); @@ -487,7 +487,7 @@ public Map getAllClaimWorlds() throws IllegalStateExcep final World world = World.of(UUID.fromString(resultSet.getString("world_uuid")), resultSet.getString("world_name"), resultSet.getString("world_environment")); - final ClaimWorld claimWorld = plugin.getGson().fromJson(data, ClaimWorld.class); + final ClaimWorld claimWorld = plugin.getClaimWorldFromJson(data); claimWorld.updateId(resultSet.getInt("id")); worlds.put(new ServerWorld(resultSet.getString("server_name"), world), claimWorld); } diff --git a/common/src/main/java/net/william278/husktowns/database/SqLiteDatabase.java b/common/src/main/java/net/william278/husktowns/database/SqLiteDatabase.java index 9ce67d24..2bdaef41 100644 --- a/common/src/main/java/net/william278/husktowns/database/SqLiteDatabase.java +++ b/common/src/main/java/net/william278/husktowns/database/SqLiteDatabase.java @@ -212,7 +212,7 @@ public Optional getUser(@NotNull UUID uuid) { User.of(uuid, name), resultSet.getTimestamp("last_login").toLocalDateTime() .atOffset(OffsetDateTime.now().getOffset()), - plugin.getGson().fromJson(preferences, Preferences.class) + plugin.getPreferencesFromJson(preferences) )); } } catch (SQLException e) { @@ -237,7 +237,7 @@ public Optional getUser(@NotNull String username) { User.of(uuid, name), resultSet.getTimestamp("last_login").toLocalDateTime() .atOffset(OffsetDateTime.now().getOffset()), - plugin.getGson().fromJson(preferences, Preferences.class) + plugin.getPreferencesFromJson(preferences) )); } } catch (SQLException e) { @@ -263,7 +263,7 @@ public List getInactiveUsers(long daysInactive) { User.of(uuid, name), resultSet.getTimestamp("last_login").toLocalDateTime() .atOffset(OffsetDateTime.now().getOffset()), - plugin.getGson().fromJson(preferences, Preferences.class) + plugin.getPreferencesFromJson(preferences) )); } } catch (SQLException e) { @@ -322,8 +322,9 @@ public Optional getTown(int townId) { statement.setInt(1, townId); final ResultSet resultSet = statement.executeQuery(); if (resultSet.next()) { - final String data = new String(resultSet.getBytes("data"), StandardCharsets.UTF_8); - final Town town = plugin.getGson().fromJson(data, Town.class); + final Town town = plugin.getTownFromJson( + new String(resultSet.getBytes("data"), StandardCharsets.UTF_8) + ); town.setId(resultSet.getInt("id")); return Optional.of(town); } @@ -341,12 +342,11 @@ public List getAllTowns() throws IllegalStateException { FROM `%town_data%`"""))) { final ResultSet resultSet = statement.executeQuery(); while (resultSet.next()) { - final String data = new String(resultSet.getBytes("data"), StandardCharsets.UTF_8); - final Town town = plugin.getGson().fromJson(data, Town.class); - if (town != null) { - town.setId(resultSet.getInt("id")); - towns.add(town); - } + final Town town = plugin.getTownFromJson( + new String(resultSet.getBytes("data"), StandardCharsets.UTF_8) + ); + town.setId(resultSet.getInt("id")); + towns.add(town); } } catch (SQLException | JsonSyntaxException e) { throw new IllegalStateException("Failed to fetch all town data from table", e); @@ -361,13 +361,13 @@ public Town createTown(@NotNull String name, @NotNull User creator) { town.addMember(creator.getUuid(), plugin.getRoles().getMayorRole()); try (PreparedStatement statement = getConnection().prepareStatement(format(""" INSERT INTO `%town_data%` (`name`, `data`) - VALUES (?, ?)"""), Statement.RETURN_GENERATED_KEYS)) { + VALUES (?, ?) + RETURNING `id`;"""))) { statement.setString(1, town.getName()); statement.setBytes(2, plugin.getGson().toJson(town).getBytes(StandardCharsets.UTF_8)); - statement.executeUpdate(); - final ResultSet resultSet = statement.getGeneratedKeys(); + final ResultSet resultSet = statement.executeQuery(); if (resultSet.next()) { - town.setId(resultSet.getInt(1)); + town.setId(resultSet.getInt("id")); } } catch (SQLException | JsonSyntaxException e) { plugin.log(Level.SEVERE, "Failed to create town in table", e); @@ -421,11 +421,12 @@ public Map getClaimWorlds(@NotNull String server) throws Ille statement.setString(1, server); final ResultSet resultSet = statement.executeQuery(); while (resultSet.next()) { - final String data = new String(resultSet.getBytes("claims"), StandardCharsets.UTF_8); final World world = World.of(UUID.fromString(resultSet.getString("world_uuid")), resultSet.getString("world_name"), resultSet.getString("world_environment")); - final ClaimWorld claimWorld = plugin.getGson().fromJson(data, ClaimWorld.class); + final ClaimWorld claimWorld = plugin.getClaimWorldFromJson( + new String(resultSet.getBytes("claims"), StandardCharsets.UTF_8) + ); claimWorld.updateId(resultSet.getInt("id")); if (!plugin.getSettings().isUnclaimableWorld(world)) { worlds.put(world, claimWorld); @@ -445,11 +446,12 @@ public Map getAllClaimWorlds() throws IllegalStateExcep FROM `%claim_data%`"""))) { final ResultSet resultSet = statement.executeQuery(); while (resultSet.next()) { - final String data = new String(resultSet.getBytes("claims"), StandardCharsets.UTF_8); final World world = World.of(UUID.fromString(resultSet.getString("world_uuid")), resultSet.getString("world_name"), resultSet.getString("world_environment")); - final ClaimWorld claimWorld = plugin.getGson().fromJson(data, ClaimWorld.class); + final ClaimWorld claimWorld = plugin.getClaimWorldFromJson( + new String(resultSet.getBytes("claims"), StandardCharsets.UTF_8) + ); claimWorld.updateId(resultSet.getInt("id")); worlds.put(new ServerWorld(resultSet.getString("server_name"), world), claimWorld); } diff --git a/common/src/main/java/net/william278/husktowns/manager/ClaimsManager.java b/common/src/main/java/net/william278/husktowns/manager/ClaimsManager.java index f8a46ff5..2a88a41c 100644 --- a/common/src/main/java/net/william278/husktowns/manager/ClaimsManager.java +++ b/common/src/main/java/net/william278/husktowns/manager/ClaimsManager.java @@ -411,6 +411,35 @@ public void removePlotMember(@NotNull OnlineUser user, @NotNull World world, @No })), () -> plugin.getLocales().getLocale("error_not_in_town").ifPresent(user::sendMessage)); } + public void claimPlot(@NotNull OnlineUser user, @NotNull World world, @NotNull Chunk chunk) { + plugin.getManager().ifMember(user, Privilege.CLAIM_PLOT, (member -> + plugin.getManager().ifClaimOwner(member, user, chunk, world, (claim -> { + if (claim.claim().getType() != Claim.Type.PLOT) { + plugin.getLocales().getLocale("error_claim_not_plot") + .ifPresent(user::sendMessage); + return; + } + final Optional claimWorld = plugin.getClaimWorld(world); + assert claimWorld.isPresent(); + + plugin.runAsync(() -> { + if (!claim.claim().getPlotMembers().isEmpty()) { + plugin.getLocales().getLocale("error_plot_not_vacant") + .ifPresent(user::sendMessage); + return; + } + + claim.claim().setPlotMember(user.getUuid(), true); + plugin.getDatabase().updateClaimWorld(claimWorld.get()); + plugin.getManager().editTown(user, claim.town(), (town -> town.getLog().log(Action.of(user, + Action.Type.CLAIM_VACANT_PLOT, claim.claim().toString())))); + + plugin.getLocales().getLocale("plot_claimed", Integer.toString(chunk.getX()), + Integer.toString(chunk.getZ())).ifPresent(user::sendMessage); + }); + })))); + } + public void listPlotMembers(@NotNull OnlineUser user, @NotNull World world, @NotNull Chunk chunk) { plugin.getUserTown(user).ifPresentOrElse(member -> plugin.getManager() .ifClaimOwner(member, user, chunk, world, (claim -> { @@ -434,8 +463,7 @@ public void listPlotMembers(@NotNull OnlineUser user, @NotNull World world, @Not .collect(Collectors.joining(", ")); plugin.getLocales().getLocale("plot_members", Integer.toString(chunk.getX()), Integer.toString(chunk.getZ()), - members.isEmpty() ? plugin.getLocales().getRawLocale("not_applicable") - .orElse("N/A") : members) + members.isEmpty() ? plugin.getLocales().getNotApplicable() : members) .ifPresent(user::sendMessage); })), () -> plugin.getLocales().getLocale("error_not_in_town").ifPresent(user::sendMessage)); } diff --git a/common/src/main/java/net/william278/husktowns/manager/TownsManager.java b/common/src/main/java/net/william278/husktowns/manager/TownsManager.java index 661de7e8..ded099a9 100644 --- a/common/src/main/java/net/william278/husktowns/manager/TownsManager.java +++ b/common/src/main/java/net/william278/husktowns/manager/TownsManager.java @@ -894,7 +894,7 @@ public void showTownLogs(@NotNull OnlineUser user, int page) { final TreeMap actions = new TreeMap<>(Comparator.reverseOrder()); actions.putAll(member.town().getLog().getActions()); final Locales locales = plugin.getLocales(); - final String NOT_APPLICABLE = plugin.getLocales().getRawLocale("not_applicable").orElse("N/A"); + final String NOT_APPLICABLE = plugin.getLocales().getNotApplicable(); user.sendMessage(PaginatedList.of(actions.entrySet().stream() .map(entry -> locales.getRawLocale("town_audit_log_list_item", entry.getKey().format(DateTimeFormatter.ofPattern("dd MMM")), @@ -965,7 +965,7 @@ public void showPlayerInfo(@NotNull CommandUser executor, @NotNull String userna member.town().getBio() .map(bio -> plugin.getLocales().wrapText(bio, 40)) .map(bio -> plugin.getLocales().truncateText(bio, 120)) - .orElse(plugin.getLocales().getRawLocale("not_applicable").orElse("N/A")) + .orElse(plugin.getLocales().getNotApplicable()) ) .ifPresent(executor::sendMessage); } diff --git a/common/src/main/java/net/william278/husktowns/menu/Overview.java b/common/src/main/java/net/william278/husktowns/menu/Overview.java index af12c6b6..07d1bb6d 100644 --- a/common/src/main/java/net/william278/husktowns/menu/Overview.java +++ b/common/src/main/java/net/william278/husktowns/menu/Overview.java @@ -119,11 +119,9 @@ private Component getStats() { Integer.toString(town.getMembers().size()), Integer.toString(town.getMaxMembers(plugin)), Integer.toString(town.getBonusMembers()), - plugin.getLevels().getMaxLevel() > town.getLevel() - ? plugin.getEconomyHook().map(economy -> economy.formatMoney( - plugin.getLevels().getLevelUpCost(town.getLevel()))) - .orElse(plugin.getLocales().getRawLocale("not_applicable").orElse("N/A")) - : plugin.getLocales().getRawLocale("not_applicable").orElse("N/A")) + town.getLevel() >= plugin.getLevels().getMaxLevel() + ? plugin.getLocales().getNotApplicable() + : plugin.formatMoney(plugin.getLevels().getLevelUpCost(town.getLevel()))) .map(mineDown -> mineDown.toComponent().appendNewline()) .orElse(Component.empty()); } diff --git a/common/src/main/java/net/william278/husktowns/migrator/LegacyMigrator.java b/common/src/main/java/net/william278/husktowns/migrator/LegacyMigrator.java index 0f360357..6bcef489 100644 --- a/common/src/main/java/net/william278/husktowns/migrator/LegacyMigrator.java +++ b/common/src/main/java/net/william278/husktowns/migrator/LegacyMigrator.java @@ -89,9 +89,14 @@ protected List getConvertedTowns() { final Timestamp founded = resultSet.getTimestamp("founded"); towns.add(Town.of(resultSet.getInt("id"), name, - clearLegacyFormatting(resultSet.getString("bio")), - clearLegacyFormatting(resultSet.getString("greeting_message")), - clearLegacyFormatting(resultSet.getString("farewell_message")), + Town.Options.create() + .setBio(clearLegacyFormatting( + resultSet.getString("bio"))) + .setGreeting(clearLegacyFormatting( + resultSet.getString("greeting_message"))) + .setFarewell(clearLegacyFormatting( + resultSet.getString("farewell_message"))) + .setColor(Town.getRandomTextColor(name)), new HashMap<>(), plugin.getRulePresets().getDefaultClaimRules(), 0, @@ -99,7 +104,6 @@ protected List getConvertedTowns() { level, null, Log.migratedLog(founded.toLocalDateTime().atOffset(ZoneOffset.UTC)), - Town.getRandomTextColor(name), new HashMap<>(), null, new HashMap<>(), @@ -120,8 +124,8 @@ protected List getConvertedTowns() { plugin.getRoles().fromWeight(roleWeight) .or(() -> { plugin.log(Level.WARNING, "No role found for weight: " + roleWeight + " - expect errors! " + - "Have you updated your roles.yml to match your existing setup? If not, stop the server, " + - "reset your database and start migration again."); + "Have you updated your roles.yml to match your existing setup? If not, stop the server, " + + "reset your database and start migration again."); return Optional.of(plugin.getRoles().getDefaultRole()); }).orElseThrow()); } @@ -231,7 +235,7 @@ protected Map getConvertedClaimWorlds() { if (world.isEmpty()) { plugin.log(Level.WARNING, "Could not find claim world for " + serverWorld + "! " + - "Are all your servers online and running the latest HuskTowns version?"); + "Are all your servers online and running the latest HuskTowns version?"); continue; } final ClaimWorld claimWorld = world.get(); @@ -245,7 +249,7 @@ protected Map getConvertedClaimWorlds() { .findFirst(); if (town.isEmpty()) { plugin.log(Level.WARNING, "Could not find a town with the name " + townName + "; " + - "it may have been skipped."); + "it may have been skipped."); continue; } diff --git a/common/src/main/java/net/william278/husktowns/network/PluginMessageBroker.java b/common/src/main/java/net/william278/husktowns/network/PluginMessageBroker.java index 28c9bad1..5466b474 100644 --- a/common/src/main/java/net/william278/husktowns/network/PluginMessageBroker.java +++ b/common/src/main/java/net/william278/husktowns/network/PluginMessageBroker.java @@ -72,7 +72,7 @@ public final void onReceive(@NotNull String channel, @NotNull OnlineUser user, b inputStream.readFully(messageBody); try (final DataInputStream messageReader = new DataInputStream(new ByteArrayInputStream(messageBody))) { - super.handle(user, plugin.getGson().fromJson(messageReader.readUTF(), Message.class)); + super.handle(user, plugin.getMessageFromJson(messageReader.readUTF())); } catch (IOException e) { plugin.log(Level.SEVERE, "Failed to fully read plugin message", e); } diff --git a/common/src/main/java/net/william278/husktowns/network/RedisBroker.java b/common/src/main/java/net/william278/husktowns/network/RedisBroker.java index 7aba3eb5..f7c09292 100644 --- a/common/src/main/java/net/william278/husktowns/network/RedisBroker.java +++ b/common/src/main/java/net/william278/husktowns/network/RedisBroker.java @@ -67,7 +67,7 @@ public void onMessage(@NotNull String channel, @NotNull String encodedMessage) { return; } - final Message message = plugin.getGson().fromJson(encodedMessage, Message.class); + final Message message = plugin.getMessageFromJson(encodedMessage); if (message.getTargetType() == Message.TargetType.PLAYER) { plugin.getOnlineUsers().stream() .filter(online -> online.getUsername().equalsIgnoreCase(message.getTarget())) diff --git a/common/src/main/java/net/william278/husktowns/town/Privilege.java b/common/src/main/java/net/william278/husktowns/town/Privilege.java index 99b0dcf6..b4ea499c 100644 --- a/common/src/main/java/net/william278/husktowns/town/Privilege.java +++ b/common/src/main/java/net/william278/husktowns/town/Privilege.java @@ -54,6 +54,10 @@ public enum Privilege { * Ability to convert claimed chunks into plots and vice versa */ SET_PLOT, + /** + * Ability to claim a vacant plot + */ + CLAIM_PLOT, /** * Ability to add members to a plot */ diff --git a/common/src/main/java/net/william278/husktowns/town/Town.java b/common/src/main/java/net/william278/husktowns/town/Town.java index 7b363001..e8dde0b6 100644 --- a/common/src/main/java/net/william278/husktowns/town/Town.java +++ b/common/src/main/java/net/william278/husktowns/town/Town.java @@ -19,10 +19,12 @@ package net.william278.husktowns.town; +import com.google.gson.Gson; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; import net.kyori.adventure.key.InvalidKeyException; import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; import net.william278.husktowns.HuskTowns; import net.william278.husktowns.audit.Log; @@ -31,6 +33,7 @@ import net.william278.husktowns.config.Roles; import net.william278.husktowns.user.User; import net.william278.husktowns.war.War; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -49,21 +52,15 @@ @SuppressWarnings("unused") public class Town { + // Represents the schema version of the town object + public static final int CURRENT_SCHEMA = 1; + // Town ID is stored as the primary key in the database towns table private int id; @Expose private String name; - @Nullable @Expose - private String bio; - @Nullable - @Expose - private String greeting; - @Nullable - @Expose - private String farewell; - @Expose - private String color; + private Options options; @Expose private Map members; @Expose @@ -89,18 +86,18 @@ public class Town { private Map relations; @Expose private Map metadata; + @Expose + @SerializedName("schema_version") + private int schemaVersion; // Internal fat constructor for instantiating a town - private Town(int id, @NotNull String name, @Nullable String bio, @Nullable String greeting, - @Nullable String farewell, @NotNull Map members, @NotNull Map rules, - int claims, @NotNull BigDecimal money, int level, @Nullable Spawn spawn, @NotNull Log log, - @NotNull TextColor color, @NotNull Map bonuses, @Nullable War currentWar, + private Town(int id, @NotNull String name, @NotNull Options options, @NotNull Map members, + @NotNull Map rules, int claims, @NotNull BigDecimal money, int level, + @Nullable Spawn spawn, @NotNull Log log, @NotNull Map bonuses, @Nullable War currentWar, @NotNull Map relations, @NotNull Map metadata) { this.id = id; this.name = name; - this.bio = bio; - this.greeting = greeting; - this.farewell = farewell; + this.options = options; this.members = members; this.rules = rules; this.claims = claims; @@ -108,11 +105,11 @@ private Town(int id, @NotNull String name, @Nullable String bio, @Nullable Strin this.level = level; this.spawn = spawn; this.log = log; - this.color = color.asHexString(); this.bonuses = bonuses; this.currentWar = currentWar; this.relations = relations; this.metadata = metadata; + this.schemaVersion = CURRENT_SCHEMA; } @SuppressWarnings("unused") @@ -124,9 +121,7 @@ private Town() { * * @param id the town ID * @param name the town name - * @param bio the town bio, or {@code null} if none - * @param greeting the town greeting message, or {@code null} if none - * @param farewell the town farewell message, or {@code null} if none + * @param options the town {@link Options} * @param members Map of town member {@link UUID}s to role weights * @param rules Map of {@link Claim.Type}s to {@link Rules flag rule mappings} * @param claims the number of claims the town has made @@ -134,27 +129,25 @@ private Town() { * @param level the town's level always ({@code >= 1}) * @param spawn the town's spawn, or {@code null} if none * @param log the town's audit {@link Log} - * @param color the town's color * @param bonuses the town's {@link Bonus} levels * @param relations the town's {@link Relation}s with other towns * @param metadata the town's metadata tag map * @return a new {@link Town} instance */ - public static Town of(int id, @NotNull String name, @Nullable String bio, @Nullable String greeting, - @Nullable String farewell, @NotNull Map members, + @NotNull + public static Town of(int id, @NotNull String name, @NotNull Options options, @NotNull Map members, @NotNull Map rules, int claims, @NotNull BigDecimal money, int level, - @Nullable Spawn spawn, @NotNull Log log, @NotNull TextColor color, - @NotNull Map bonuses, @Nullable War currentWar, - @NotNull Map relations, @NotNull Map metadata) { + @Nullable Spawn spawn, @NotNull Log log, @NotNull Map bonuses, + @Nullable War currentWar, @NotNull Map relations, + @NotNull Map metadata) { return new Town( - id, name, bio, greeting, farewell, members, rules, claims, money, level, spawn, log, color, - bonuses, currentWar, relations, metadata + id, name, options, members, rules, claims, money, level, spawn, log, bonuses, currentWar, + relations, metadata ); } /** - * @deprecated use {@link Town#of(int, String, String, String, String, Map, Map, int, BigDecimal, int, Spawn, - * Log, TextColor, Map, War, Map, Map)} + * @deprecated use {@link Town#of(int, String, Options, Map, Map, int, BigDecimal, int, Spawn, Log, Map, War, Map, Map)} */ @Deprecated(since = "2.6") @NotNull @@ -164,14 +157,13 @@ public static Town of(int id, @NotNull String name, @Nullable String bio, @Nulla @Nullable Spawn spawn, @NotNull Log log, @NotNull TextColor color, @NotNull Map bonuses, @NotNull Map metadata) { return of( - id, name, bio, greeting, farewell, members, rules, claims, money, level, spawn, log, color, - bonuses, null, new HashMap<>(), metadata + id, name, Options.create().setBio(bio).setGreeting(greeting).setFarewell(farewell).setColor(color), + members, rules, claims, money, level, spawn, log, bonuses, null, new HashMap<>(), metadata ); } /** - * @deprecated use {@link Town#of(int, String, String, String, String, Map, Map, int, BigDecimal, int, Spawn, - * Log, TextColor, Map, War, Map, Map)} + * @deprecated use {@link Town#of(int, String, Options, Map, Map, int, BigDecimal, int, Spawn, Log, Map, War, Map, Map)} */ @Deprecated(since = "2.5.3") @NotNull @@ -181,9 +173,9 @@ public static Town of(int id, @NotNull String name, @Nullable String bio, @Nulla @Nullable Spawn spawn, @NotNull Log log, @NotNull Color color, @NotNull Map bonuses, @NotNull Map metadata) { return of( - id, name, bio, greeting, farewell, members, rules, claims, money, level, spawn, log, - TextColor.color(color.getRed(), color.getGreen(), color.getBlue()), bonuses, null, - new HashMap<>(), metadata + id, name, Options.create().setBio(bio).setGreeting(greeting).setFarewell(farewell) + .setColor(TextColor.color(color.getRed(), color.getGreen(), color.getBlue())), + members, rules, claims, money, level, spawn, log, bonuses, null, new HashMap<>(), metadata ); } @@ -196,12 +188,12 @@ public static Town of(int id, @NotNull String name, @Nullable String bio, @Nulla * @return A town, with {@code id} set to 0 of the name {@code name}, with the creator as the only member and mayor */ @NotNull + @ApiStatus.Internal public static Town create(@NotNull String name, @NotNull User creator, @NotNull HuskTowns plugin) { return of( - 0, name, null, null, null, new HashMap<>(), + 0, name, Options.create().setColor(Town.getRandomTextColor(name)), new HashMap<>(), plugin.getRulePresets().getDefaultClaimRules(), 0, BigDecimal.ZERO, 1, - null, Log.newTownLog(creator), Town.getRandomTextColor(name), - new HashMap<>(), null, new HashMap<>(), new HashMap<>() + null, Log.newTownLog(creator), new HashMap<>(), null, new HashMap<>(), new HashMap<>() ); } @@ -212,14 +204,13 @@ public static Town create(@NotNull String name, @NotNull User creator, @NotNull * @return The administrator town */ @NotNull + @ApiStatus.Internal public static Town admin(@NotNull HuskTowns plugin) { return new Town( - 0, plugin.getSettings().getAdminTownName(), null, - plugin.getLocales().getRawLocale("entering_admin_claim").orElse(null), - plugin.getLocales().getRawLocale("leaving_admin_claim").orElse(null), + 0, plugin.getSettings().getAdminTownName(), Options.admin(plugin), Map.of(), Map.of(Claim.Type.CLAIM, plugin.getRulePresets().getAdminClaimRules()), 0, - BigDecimal.ZERO, 0, null, Log.empty(), plugin.getSettings().getAdminTownColor(), - Map.of(), null, Map.of(), Map.of(plugin.getKey("admin_town").toString(), "true") + BigDecimal.ZERO, 0, null, Log.empty(), Map.of(), null, Map.of(), + Map.of(plugin.getKey("admin_town").toString(), "true") ); } @@ -293,7 +284,7 @@ public void setName(@NotNull String name) { * @return the town bio wrapped in an {@link Optional}; empty if there is no bio */ public Optional getBio() { - return Optional.ofNullable(bio); + return this.options.getBio(); } /** @@ -303,7 +294,7 @@ public Optional getBio() { * @apiNote This should be passed through {@link net.william278.husktowns.util.Validator#isValidTownMetadata(String)} first */ public void setBio(@NotNull String bio) { - this.bio = bio; + this.options.setBio(bio); } /** @@ -314,7 +305,7 @@ public void setBio(@NotNull String bio) { * @return the town bio wrapped in an {@link Optional}; empty if there is no bio */ public Optional getGreeting() { - return Optional.ofNullable(greeting); + return this.options.getGreeting(); } /** @@ -324,7 +315,7 @@ public Optional getGreeting() { * @apiNote This should be passed through {@link net.william278.husktowns.util.Validator#isValidTownMetadata(String)} first */ public void setGreeting(@NotNull String greeting) { - this.greeting = greeting; + this.options.setGreeting(greeting); } /** @@ -335,7 +326,7 @@ public void setGreeting(@NotNull String greeting) { * @return the town bio wrapped in an {@link Optional}; empty if there is no bio */ public Optional getFarewell() { - return Optional.ofNullable(farewell); + return this.options.getFarewell(); } /** @@ -345,7 +336,57 @@ public Optional getFarewell() { * @apiNote This should be passed through {@link net.william278.husktowns.util.Validator#isValidTownMetadata(String)} first */ public void setFarewell(@NotNull String farewell) { - this.farewell = farewell; + this.options.setFarewell(farewell); + } + + /** + * Get the setColor of the town as a {@link Color} + * + * @return the {@link Color} of the town + * @deprecated use {@link #getDisplayColor()} to get an adventure {@link TextColor} instead + */ + @NotNull + @Deprecated(since = "2.5.3") + public Color getColor() { + return Color.decode(options.getColor()); + } + + @NotNull + public TextColor getDisplayColor() { + return Objects.requireNonNull( + TextColor.fromHexString(options.getColor()), + String.format("Invalid setColor hex string (\"%s\") for town %s", options.getColor(), getName()) + ); + } + + /** + * Get the {@link TextColor} of the town as a hex string, including the leading {@code #} + * + * @return the {@link TextColor} of the town as a hex string (e.g. {@code #FF0000}) + */ + @NotNull + public String getColorRgb() { + return options.getColor(); + } + + /** + * Set the {@link TextColor} of the town + * + * @param color the new {@link TextColor} of the town + */ + public void setTextColor(@NotNull TextColor color) { + this.options.setColor(color); + } + + /** + * Set the {@link Color} of the town + * + * @param color the new {@link Color} of the town + * @deprecated use {@link #setTextColor(TextColor)} to set an adventure {@link TextColor} instead + */ + @Deprecated(since = "2.5.3") + public void setColor(@NotNull Color color) { + this.options.setColor(String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue())); } /** @@ -531,56 +572,6 @@ public OffsetDateTime getFoundedTime() { return log.getFoundedTime(); } - /** - * Get the color of the town as a {@link Color} - * - * @return the {@link Color} of the town - * @deprecated use {@link #getDisplayColor()} to get an adventure {@link TextColor} instead - */ - @NotNull - @Deprecated(since = "2.5.3") - public Color getColor() { - return Color.decode(color); - } - - @NotNull - public TextColor getDisplayColor() { - return Objects.requireNonNull( - TextColor.fromHexString(color), - String.format("Invalid color hex string (\"%s\") for town %s", color, getName()) - ); - } - - /** - * Get the {@link TextColor} of the town as a hex string, including the leading {@code #} - * - * @return the {@link TextColor} of the town as a hex string (e.g. {@code #FF0000}) - */ - @NotNull - public String getColorRgb() { - return color; - } - - /** - * Set the {@link TextColor} of the town - * - * @param color the new {@link TextColor} of the town - */ - public void setTextColor(@NotNull TextColor color) { - this.color = color.asHexString(); - } - - /** - * Set the {@link Color} of the town - * - * @param color the new {@link Color} of the town - * @deprecated use {@link #setTextColor(TextColor)} to set an adventure {@link TextColor} instead - */ - @Deprecated(since = "2.5.3") - public void setColor(@NotNull Color color) { - this.color = String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()); - } - /** * Get the value set for a given town {@link Bonus} * @@ -810,6 +801,63 @@ public Map getMetadataTags() { } } + /** + * Get the schema version of this town object + * + * @return the schema version of this town object + */ + @ApiStatus.Internal + public int getSchemaVersion() { + return schemaVersion; + } + + /** + * Set the schema version of this town object + * + * @param schemaVersion the schema version of this town object + */ + @ApiStatus.Internal + public void setSchemaVersion(int schemaVersion) { + this.schemaVersion = schemaVersion; + } + + /** + * Carries out town schema object upgrades + * + * @param json the JSON-ified town + * @param gson the Gson instance + * @return the upgraded town + */ + @ApiStatus.Internal + @NotNull + public Town upgradeSchema(String json, Gson gson) { + if (this.schemaVersion >= CURRENT_SCHEMA) { + return this; + } + + // Perform schema upgrades + if (this.schemaVersion == 0) { + final Map map = gson.fromJson(json, Map.class); + this.options = Options.create(); + + if (map.containsKey("bio")) { + setBio((String) map.get("bio")); + } + if (map.containsKey("greeting")) { + setGreeting((String) map.get("greeting")); + } + if (map.containsKey("farewell")) { + setFarewell((String) map.get("farewell")); + } + if (map.containsKey("color")) { + setTextColor(Objects.requireNonNull(TextColor.fromHexString((String) map.get("color")))); + } + + setSchemaVersion(1); + } + return this; + } + /** * Compares this town to another object * @@ -826,6 +874,89 @@ public boolean equals(Object obj) { return this.id == town.id; } + /** + * Represents town options + */ + public static class Options { + @Expose + @Nullable + private String bio; + @Expose + @Nullable + private String greeting; + @Expose + @Nullable + private String farewell; + @Expose + private String color = NamedTextColor.GRAY.asHexString(); + + private Options() { + } + + @NotNull + public static Options create() { + return new Options(); + } + + @NotNull + public static Options admin(@NotNull HuskTowns plugin) { + return new Options() + .setColor(plugin.getSettings().getAdminTownColor()) + .setGreeting(plugin.getLocales().getRawLocale("entering_admin_claim").orElse(null)) + .setFarewell(plugin.getLocales().getRawLocale("leaving_admin_claim").orElse(null)); + } + + @NotNull + public Options setBio(@Nullable String bio) { + this.bio = bio; + return this; + } + + @NotNull + public Options setGreeting(@Nullable String greeting) { + this.greeting = greeting; + return this; + } + + @NotNull + public Options setFarewell(@Nullable String farewell) { + this.farewell = farewell; + return this; + } + + @NotNull + public Options setColor(@NotNull TextColor color) { + this.color = color.asHexString(); + return this; + } + + @NotNull + public Options setColor(@NotNull String color) { + this.color = color; + return this; + } + + @Nullable + public Optional getBio() { + return Optional.ofNullable(bio); + } + + @Nullable + public Optional getGreeting() { + return Optional.ofNullable(greeting); + } + + @Nullable + public Optional getFarewell() { + return Optional.ofNullable(farewell); + } + + @NotNull + public String getColor() { + return color; + } + } + /** * Identifies different categories of town bonuses */ diff --git a/common/src/main/java/net/william278/husktowns/util/GsonProvider.java b/common/src/main/java/net/william278/husktowns/util/GsonProvider.java index 6d29462f..690015ae 100644 --- a/common/src/main/java/net/william278/husktowns/util/GsonProvider.java +++ b/common/src/main/java/net/william278/husktowns/util/GsonProvider.java @@ -22,6 +22,11 @@ import com.fatboyindustrial.gsonjavatime.Converters; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import net.william278.husktowns.claim.ClaimWorld; +import net.william278.husktowns.network.Message; +import net.william278.husktowns.town.Town; +import net.william278.husktowns.user.Preferences; import org.jetbrains.annotations.NotNull; public interface GsonProvider { @@ -36,4 +41,24 @@ default Gson getGson() { return getGsonBuilder().create(); } + @NotNull + default Town getTownFromJson(@NotNull String json) throws JsonSyntaxException { + return getGson().fromJson(json, Town.class).upgradeSchema(json, getGson()); + } + + @NotNull + default ClaimWorld getClaimWorldFromJson(@NotNull String json) throws JsonSyntaxException { + return getGson().fromJson(json, ClaimWorld.class); + } + + @NotNull + default Message getMessageFromJson(@NotNull String json) throws JsonSyntaxException { + return getGson().fromJson(json, Message.class); + } + + @NotNull + default Preferences getPreferencesFromJson(@NotNull String json) throws JsonSyntaxException { + return getGson().fromJson(json, Preferences.class); + } + } diff --git a/common/src/main/resources/locales/en-gb.yml b/common/src/main/resources/locales/en-gb.yml index 283098fb..37944d68 100644 --- a/common/src/main/resources/locales/en-gb.yml +++ b/common/src/main/resources/locales/en-gb.yml @@ -19,6 +19,7 @@ operation_cancelled_privileges: '[You do not have access to do that in this clai operation_cancelled: '[You do not have permission to do that here](#ff7e5e)' plot_member_added: '[%1%](#00fb9a) [has been added to the plot at (x: %2%, z: %3%)](#00fb9a)' plot_member_removed: '[%1%](#00fb9a) [has been removed from the plot at (x: %2%, z: %3%)](#00fb9a)' +plot_claimed: '[Successfully claimed the vacant town plot at (x: %1%, z: %2%)](#00fb9a)' claim_made_plot: '[Successfully set the chunk at (x: %1%, z: %2%) as a town plot](#00fb9a)' claim_made_farm: '[Successfully set the chunk at (x: %1%, z: %2%) as a town farm](#00fb9a)' claim_made_regular: '[Successfully set the chunk at (x: %1%, z: %2%) as a regular claim](#00fb9a)' @@ -207,6 +208,7 @@ error_not_in_town: '[Error:](#ff3300) [You are not in a town! Create a town with error_other_not_in_town: '[Error:](#ff3300) [%1% is not a member of your town!](#ff7e5e)' error_insufficient_privileges: '[Error:](#ff3300) [You do not have sufficient privileges to do that](#ff7e5e)' error_plot_manager_privilege: '[Error:](#ff3300) [You do not have sufficient privileges to assign a player as manager of this plot](#ff7e5e)' +error_plot_not_vacant: '[Error:](#ff3300) [This plot is not vacant](#ff7e5e)' error_no_permission: '[Error:](#ff3300) [You do not have permission to use this command](#ff7e5e)' error_command_in_game_only: '[Error:](#ff3300) [This command can only be used in-game](#ff7e5e)' error_user_not_found: '[Error:](#ff3300) [That user could not be found](#ff7e5e)' diff --git a/common/src/main/resources/locales/es-es.yml b/common/src/main/resources/locales/es-es.yml index c24192e2..71f3c828 100644 --- a/common/src/main/resources/locales/es-es.yml +++ b/common/src/main/resources/locales/es-es.yml @@ -19,6 +19,7 @@ operation_cancelled_privileges: '[No tienes acceso a esto en este claim](#ff7e5e operation_cancelled: '[No tienes permiso para hacer eso aquí.](#ff7e5e)' plot_member_added: '[%1%](#00fb9a) [ha sido añadido a la parcela situada en las coordenadas (x: %2%, z: %3%)](#00fb9a)' plot_member_removed: '[%1%](#00fb9a) [ha sido eliminado de la parcela situada en las coordenadas (x: %2%, z: %3%)](#00fb9a)' +plot_claimed: '[Successfully claimed the vacant town plot at (x: %1%, z: %2%)](#00fb9a)' claim_made_plot: '[Has establecido el terreno situado en las coordenadas (x: %1%, z: %2%) como parcela de la ciudad](#00fb9a)' claim_made_farm: '[Has establecido el terreno situado en las coordenadas (x: %1%, z: %2%) como granja de la ciudad](#00fb9a)' claim_made_regular: '[Has establecido el terreno situado en las coordenadas (x: %1%, z: %2%) terreno regular](#00fb9a)' @@ -207,6 +208,7 @@ error_not_in_town: '[Error:](#ff3300) [No perteneces a una ciudad! Crea una ciud error_other_not_in_town: '[Error:](#ff3300) [%1% no es miembro de esta ciudad!](#ff7e5e)' error_insufficient_privileges: '[Error:](#ff3300) [No tiene suficientes privilegios para hacer eso.](#ff7e5e)' error_plot_manager_privilege: '[Error:](#ff3300) [No tienes privilegios suficientes para asignar a un jugador como administrador de esta parcela](#ff7e5e)' +error_plot_not_vacant: '[Error:](#ff3300) [This plot is not vacant](#ff7e5e)' error_no_permission: '[Error:](#ff3300) [No tienes permiso para usar este comando](#ff7e5e)' error_command_in_game_only: '[Error:](#ff3300) [This command can only be used in-game](#ff7e5e)' error_user_not_found: '[Error:](#ff3300) [Ese usuario no pudo ser encontrado](#ff7e5e)' diff --git a/common/src/main/resources/locales/fr-fr.yml b/common/src/main/resources/locales/fr-fr.yml index 49e78bde..5d89d02a 100644 --- a/common/src/main/resources/locales/fr-fr.yml +++ b/common/src/main/resources/locales/fr-fr.yml @@ -19,6 +19,7 @@ operation_cancelled_privileges: '[e suis désolé, mais vous n''êtes pas autori operation_cancelled: '[Vous n''avez pas la permission de faire cela.](#ff7e5e)' plot_member_added: '[%1%](#00fb9a) [has been added to the plot at (x: %2%, z: %3%)](#00fb9a)' plot_member_removed: '[%1%](#00fb9a) [a été retiré du terrain à (x: %2%, z: %3%)](#00fb9a)' +plot_claimed: '[Successfully claimed the vacant town plot at (x: %1%, z: %2%)](#00fb9a)' claim_made_plot: '[Le chunk à (x: %1%, z: %2%) a été défini avec succès comme terrain de la ville](#00fb9a)' claim_made_farm: '[Le chunk à (x: %1%, z: %2%) a été défini avec succès comme ferme de la ville](#00fb9a)' claim_made_regular: '[Le chunk à (x: %1%, z: %2%) a été défini avec succès comme revendication régulière](#00fb9a)' @@ -207,6 +208,7 @@ error_not_in_town: '[Erreur:](#ff3300) [Vous n''êtes pas dans une ville ! Crée error_other_not_in_town: '[Erreur:](#ff3300) [%1% n''est pas membre de votre ville !](#ff7e5e)' error_insufficient_privileges: '[Erreur:](#ff3300) [Vous n''avez pas les privilèges suffisants pour le faire](#ff7e5e)' error_plot_manager_privilege: '[Erreur:](#ff3300) [Vous n''avez pas les privilèges suffisants pour désigner un joueur comme gestionnaire de ce terrain](#ff7e5e)' +error_plot_not_vacant: '[Error:](#ff3300) [This plot is not vacant](#ff7e5e)' error_no_permission: '[Erreur:](#ff3300) [Vous n''êtes pas autorisé à utiliser cette commande](#ff7e5e)' error_command_in_game_only: '[Erreur:](#ff3300) [Cette commande ne peut être utilisée que dans le jeu](#ff7e5e)' error_user_not_found: '[Erreur:](#ff3300) [Cet utilisateur est introuvable](#ff7e5e)' diff --git a/common/src/main/resources/locales/ru-ru.yml b/common/src/main/resources/locales/ru-ru.yml index 5539bb97..75b87e59 100644 --- a/common/src/main/resources/locales/ru-ru.yml +++ b/common/src/main/resources/locales/ru-ru.yml @@ -19,6 +19,7 @@ operation_cancelled_privileges: '[У вас нет доступа к этому operation_cancelled: '[У вас нет разрешения на это здесь](#ff7e5e)' plot_member_added: '[%1%](#00fb9a) [был добавлен в земельный участок в (x: %2%, z: %3%)](#00fb9a)' plot_member_removed: '[%1%](#00fb9a) [был удален из земельного участка в (x: %2%, z: %3%)](#00fb9a)' +plot_claimed: '[Successfully claimed the vacant town plot at (x: %1%, z: %2%)](#00fb9a)' claim_made_plot: '[Успешно установлен земельный участок города в (x: %1%, z: %2%)](#00fb9a)' claim_made_farm: '[Успешно установлена городская ферма в (x: %1%, z: %2%)](#00fb9a)' claim_made_regular: '[Успешно установлен обычный земельный участок в (x: %1%, z: %2%)](#00fb9a)' @@ -207,6 +208,7 @@ error_not_in_town: '[Ошибка:](#ff3300) [Вы не находитесь в error_other_not_in_town: '[Ошибка:](#ff3300) [%1% не является членом вашего города!](#ff7e5e)' error_insufficient_privileges: '[Ошибка:](#ff3300) [У вас недостаточно прав для этого](#ff7e5e)' error_plot_manager_privilege: '[Ошибка:](#ff3300) [У вас недостаточно прав, чтобы назначить игрока менеджером этого участка](#ff7e5e)' +error_plot_not_vacant: '[Error:](#ff3300) [This plot is not vacant](#ff7e5e)' error_no_permission: '[Ошибка:](#ff3300) [У вас нет разрешения на использование этой команды](#ff7e5e)' error_command_in_game_only: '[Ошибка:](#ff3300) [Эту команду можно использовать только в игре](#ff7e5e)' error_user_not_found: '[Ошибка:](#ff3300) [Этот пользователь не найден](#ff7e5e)' diff --git a/common/src/main/resources/locales/tr-tr.yml b/common/src/main/resources/locales/tr-tr.yml index b7f08978..51b9f0fd 100644 --- a/common/src/main/resources/locales/tr-tr.yml +++ b/common/src/main/resources/locales/tr-tr.yml @@ -19,6 +19,7 @@ operation_cancelled_privileges: '[Bu topraklarda bunu yapma izniniz yok](#ff7e5e operation_cancelled: '[Bu topraklarda bunu yapma izniniz yok](#ff7e5e)' plot_member_added: '[%1%](#00fb9a) [oyuncusu (x: %2%, z: %3%) konumundaki topraklara eklendi](#00fb9a)' plot_member_removed: '[%1%](#00fb9a) [oyuncusu (x: %2%, z: %3%) konumundaki topraklardan çıkarıldı](#00fb9a)' +plot_claimed: '[Successfully claimed the vacant town plot at (x: %1%, z: %2%)](#00fb9a)' claim_made_plot: '[(x: %1%, z: %2%) Konumundaki topraklar kasaba toprağı olarak sahiplenildi](#00fb9a)' claim_made_farm: '[(x: %1%, z: %2%) Konumundaki topraklar kasaba çiftliği olarak sahiplenildi](#00fb9a)' claim_made_regular: '[(x: %1%, z: %2%) Konumundaki topraklar sahiplenildi](#00fb9a)' @@ -207,6 +208,7 @@ error_not_in_town: '[Hata:](#ff3300) [Bir kasaban yok! Kasaba oluşturmak için: error_other_not_in_town: '[Hata:](#ff3300) [%1% kasabanın bir üyesi değil!](#ff7e5e)' error_insufficient_privileges: '[Hata:](#ff3300) [Bu yapmak için gerekli izinlere sahip değilsin](#ff7e5e)' error_plot_manager_privilege: '[Hata:](#ff3300) [Bir oyuncuyu bu arazinin düzenleyicisi olarak atamak için gerekli izinlere sahip değilsin](#ff7e5e)' +error_plot_not_vacant: '[Error:](#ff3300) [This plot is not vacant](#ff7e5e)' error_no_permission: '[Hata:](#ff3300) [Bu komutu kullanmak için gerekli izinlere sahip değilsin](#ff7e5e)' error_command_in_game_only: '[Hata:](#ff3300) [Bu komut yalnızca oyun içinden çalıştırılır](#ff7e5e)' error_user_not_found: '[Hata:](#ff3300) [Bu isimde bir oyuncu bulunamadı](#ff7e5e)' diff --git a/common/src/main/resources/locales/zh-cn.yml b/common/src/main/resources/locales/zh-cn.yml index 2e7ea001..65999867 100644 --- a/common/src/main/resources/locales/zh-cn.yml +++ b/common/src/main/resources/locales/zh-cn.yml @@ -19,6 +19,7 @@ operation_cancelled_privileges: '[你在这个领地上面没有权限这样做] operation_cancelled: '[你在这里没有权限这样做](#ff7e5e)' plot_member_added: '[%1%](#00fb9a) [已被添加到地皮 (x: %2%, z: %3%)](#00fb9a)' plot_member_removed: '[%1%](#00fb9a) [已被移除出地皮 (x: %2%, z: %3%)](#00fb9a)' +plot_claimed: '[Successfully claimed the vacant town plot at (x: %1%, z: %2%)](#00fb9a)' claim_made_plot: '[成功设置区块 (x: %1%, z: %2%) 作为城镇地皮](#00fb9a)' claim_made_farm: '[成功设置区块 (x: %1%, z: %2%) 作为城镇农场](#00fb9a)' claim_made_regular: '[成功设置区块 (x: %1%, z: %2%) 做为普通领地](#00fb9a)' @@ -207,6 +208,7 @@ error_not_in_town: '[错误:](#ff3300) [你还没有加入一个城镇! 这样 error_other_not_in_town: '[错误:](#ff3300) [%1% 不是你城镇的一员!](#ff7e5e)' error_insufficient_privileges: '[错误:](#ff3300) [你的权限不允许你这样做](#ff7e5e)' error_plot_manager_privilege: '[错误:](#ff3300) [你的权限不允许你设置一个玩家为该地皮的管理者](#ff7e5e)' +error_plot_not_vacant: '[Error:](#ff3300) [This plot is not vacant](#ff7e5e)' error_no_permission: '[错误:](#ff3300) [你没有权限使用这个指令](#ff7e5e)' error_command_in_game_only: '[错误:](#ff3300) [这个指令只能在游戏内使用](#ff7e5e)' error_user_not_found: '[错误:](#ff3300) [找不到该用户](#ff7e5e)' diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index c2781c6b..76715694 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -129,7 +129,7 @@ general: ### 4.4 Town plots To make or manage a town plot claim, you must be standing in it. -To make a town plot from a regular claim, use `/town plot`. You can then add someone to the plot with `/town plot add `. Note the `` does not actually have to be a town member. Players added to a plot have full access to build within it. +To make a town plot from a regular claim, use `/town plot` and members will then be able to use `/plot claim` to claim the plot while it is vacant. Alternatively, you can assign someone to the plot with `/town plot add `. Note the `` does not actually have to be a town member. Players added to a plot have full access to build within it. You can designate someone as a "manager" of a town plot, which will let them add others to the plot as well using the previously mentioned command. You can do this with `/town plot add manager`. diff --git a/gradle.properties b/gradle.properties index a1834f2f..7419846c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ plugin_version=2.6 plugin_archive=husktowns plugin_description=Simple and elegant proxy-compatible Towny-style protection -jedis_version=5.0.2 +jedis_version=5.1.0 mysql_driver_version=8.2.0 -mariadb_driver_version=3.2.0 -sqlite_driver_version=3.42.0.0 +mariadb_driver_version=3.3.0 +sqlite_driver_version=3.44.0.0 diff --git a/paper/build.gradle b/paper/build.gradle index 78aa53ae..b0f1c8cf 100644 --- a/paper/build.gradle +++ b/paper/build.gradle @@ -40,6 +40,6 @@ shadowJar { tasks { runServer { - minecraftVersion('1.20.1') + minecraftVersion('1.20.2') } } \ No newline at end of file