From 02bc08dae1bde6c446ab8b0ef3138d6b1657f767 Mon Sep 17 00:00:00 2001 From: gafru Date: Fri, 11 Sep 2026 16:03:39 +0300 Subject: [PATCH 1/8] feat: initialize JoyBank plugin and add first command --- gradle.properties | 6 +++--- .../{EmptyPlugin.java => JoyBankPlugin.java} | 7 ++++--- .../joutak/template/command/JoyCommand.java | 20 +++++++++++++++++++ src/main/resources/plugin.yml | 10 +++++++--- 4 files changed, 34 insertions(+), 9 deletions(-) rename src/main/java/ru/joutak/template/{EmptyPlugin.java => JoyBankPlugin.java} (67%) create mode 100644 src/main/java/ru/joutak/template/command/JoyCommand.java diff --git a/gradle.properties b/gradle.properties index cf51ce0..cd24e1e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,8 @@ # Plugin properties -repo=https://github.com/JouTak/TemplatePlugin-Java +repo=https://github.com/gafrus06/TemplatePlugin-Java group=ru.joutak -name=Template-Java -version=1.0.0 +name=JoyBank +version=0.1.0 # Gradle properties org.gradle.parallel=true \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/EmptyPlugin.java b/src/main/java/ru/joutak/template/JoyBankPlugin.java similarity index 67% rename from src/main/java/ru/joutak/template/EmptyPlugin.java rename to src/main/java/ru/joutak/template/JoyBankPlugin.java index d93430e..da5f8e6 100644 --- a/src/main/java/ru/joutak/template/EmptyPlugin.java +++ b/src/main/java/ru/joutak/template/JoyBankPlugin.java @@ -2,15 +2,16 @@ import lombok.Getter; import org.bukkit.plugin.java.JavaPlugin; +import ru.joutak.template.command.JoyCommand; -public final class EmptyPlugin extends JavaPlugin { +public final class JoyBankPlugin extends JavaPlugin { @Getter - private static EmptyPlugin instance; + private static JoyBankPlugin instance; @Override public void onEnable() { instance = this; - + getCommand("joy").setExecutor(new JoyCommand()); getLogger().info( String.format("Плагин %s версии %s включен!", getPluginMeta().getName(), getPluginMeta().getVersion()) ); diff --git a/src/main/java/ru/joutak/template/command/JoyCommand.java b/src/main/java/ru/joutak/template/command/JoyCommand.java new file mode 100644 index 0000000..b01fc0a --- /dev/null +++ b/src/main/java/ru/joutak/template/command/JoyCommand.java @@ -0,0 +1,20 @@ +package ru.joutak.template.command; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +public class JoyCommand implements CommandExecutor { + + @Override + public boolean onCommand( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args + ){ + sender.sendMessage("JoyBank работает!"); + return true; + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 4428874..7650cfe 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,12 +1,16 @@ name: ${NAME} version: ${VERSION} api-version: ${MINECRAFT_VERSION} -main: ru.joutak.template.EmptyPlugin +main: ru.joutak.template.JoyBankPlugin description: 'A template plugin' -authors: ['EnderDissa', 'Stunnerer', 's4dnex'] +authors: ['russlan_1esse'] contributors: ['technosimp'] website: ${WEBSITE} depend: [] libraries: - - 'org.projectlombok:lombok:${LOMBOK_VERSION}' \ No newline at end of file + - 'org.projectlombok:lombok:${LOMBOK_VERSION}' +commands: + joy: + description: Основная команда JoyBank + usage: /joy \ No newline at end of file From 2b1c8a176328cd4763d16e91d81a99e34793b281 Mon Sep 17 00:00:00 2001 From: gafru Date: Fri, 11 Sep 2026 16:34:34 +0300 Subject: [PATCH 2/8] feat: add Joy coins and admin give command --- .../ru/joutak/template/JoyBankPlugin.java | 5 +- .../ru/joutak/template/coin/CoinService.java | 56 +++++++++++++++++++ .../joutak/template/command/JoyCommand.java | 53 +++++++++++++++++- src/main/resources/plugin.yml | 7 ++- 4 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 src/main/java/ru/joutak/template/coin/CoinService.java diff --git a/src/main/java/ru/joutak/template/JoyBankPlugin.java b/src/main/java/ru/joutak/template/JoyBankPlugin.java index da5f8e6..a3719d9 100644 --- a/src/main/java/ru/joutak/template/JoyBankPlugin.java +++ b/src/main/java/ru/joutak/template/JoyBankPlugin.java @@ -2,16 +2,19 @@ import lombok.Getter; import org.bukkit.plugin.java.JavaPlugin; +import ru.joutak.template.coin.CoinService; import ru.joutak.template.command.JoyCommand; public final class JoyBankPlugin extends JavaPlugin { @Getter private static JoyBankPlugin instance; + private CoinService coinService; @Override public void onEnable() { instance = this; - getCommand("joy").setExecutor(new JoyCommand()); + coinService = new CoinService(this); + getCommand("joy").setExecutor(new JoyCommand(coinService)); getLogger().info( String.format("Плагин %s версии %s включен!", getPluginMeta().getName(), getPluginMeta().getVersion()) ); diff --git a/src/main/java/ru/joutak/template/coin/CoinService.java b/src/main/java/ru/joutak/template/coin/CoinService.java new file mode 100644 index 0000000..f5a630a --- /dev/null +++ b/src/main/java/ru/joutak/template/coin/CoinService.java @@ -0,0 +1,56 @@ +package ru.joutak.template.coin; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.java.JavaPlugin; + +public class CoinService { + + private final NamespacedKey coinKey; + + public CoinService(JavaPlugin plugin) { + this.coinKey = new NamespacedKey(plugin, "joy_coin"); + } + + public ItemStack createCoin(int amount) { + ItemStack coin = new ItemStack(Material.GOLD_NUGGET, amount); + + ItemMeta meta = coin.getItemMeta(); + + meta.displayName( + Component.text("Джой", NamedTextColor.GOLD) + ); + + meta.getPersistentDataContainer().set( + coinKey, + PersistentDataType.BYTE, + (byte) 1 + ); + + coin.setItemMeta(meta); + + return coin; + } + + public boolean isCoin(ItemStack item) { + if (item == null) { + return false; + } + + if (item.getType() != Material.GOLD_NUGGET) { + return false; + } + + ItemMeta meta = item.getItemMeta(); + + return meta.getPersistentDataContainer().has( + coinKey, + PersistentDataType.BYTE + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/command/JoyCommand.java b/src/main/java/ru/joutak/template/command/JoyCommand.java index b01fc0a..bb52287 100644 --- a/src/main/java/ru/joutak/template/command/JoyCommand.java +++ b/src/main/java/ru/joutak/template/command/JoyCommand.java @@ -3,10 +3,19 @@ import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; +import ru.joutak.template.coin.CoinService; +import org.bukkit.entity.Player; public class JoyCommand implements CommandExecutor { + private final CoinService coinService; + + public JoyCommand(CoinService coinService) { + this.coinService = coinService; + } + @Override public boolean onCommand( @NotNull CommandSender sender, @@ -14,7 +23,49 @@ public boolean onCommand( @NotNull String label, @NotNull String[] args ){ - sender.sendMessage("JoyBank работает!"); + if(!(sender instanceof Player player)){ + sender.sendMessage("Эту команду может использовать только игрок"); + return true; + } + if(args.length == 0){ + player.sendMessage("Команда: /joy give"); + return true; + } + if (args[0].equalsIgnoreCase("give")) { + + if (!player.hasPermission("joybank.admin.give")) { + player.sendMessage("У вас нет прав на эту команду."); + return true; + } + + if (args.length < 2) { + player.sendMessage("Использование: /joy give <количество>"); + return true; + } + + int amount; + + try { + amount = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + player.sendMessage("Количество должно быть числом."); + return true; + } + + if (amount <= 0) { + player.sendMessage("Количество должно быть больше 0."); + return true; + } + + ItemStack coin = coinService.createCoin(amount); + + player.getInventory().addItem(coin); + + player.sendMessage("Вы получили " + amount + " Джой."); + + return true; + } + player.sendMessage("Неизвестная команда"); return true; } } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 7650cfe..2a04626 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -13,4 +13,9 @@ libraries: commands: joy: description: Основная команда JoyBank - usage: /joy \ No newline at end of file + usage: /joy + +permissions: + joybank.admin.give: + description: Позволяет выдавать монеты Джой + default: op \ No newline at end of file From 1f1112192b52df9e38f266adcc87734fc33c2bdd Mon Sep 17 00:00:00 2001 From: gafru Date: Fri, 11 Sep 2026 16:55:10 +0300 Subject: [PATCH 3/8] feat: add persistent player balances --- .../ru/joutak/template/JoyBankPlugin.java | 12 +++- .../ru/joutak/template/bank/BankService.java | 52 ++++++++++++++++ .../joutak/template/command/JoyCommand.java | 56 ++++++++++++++++- .../template/storage/AccountRepository.java | 9 +++ .../storage/YamlAccountRepository.java | 61 +++++++++++++++++++ 5 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 src/main/java/ru/joutak/template/bank/BankService.java create mode 100644 src/main/java/ru/joutak/template/storage/AccountRepository.java create mode 100644 src/main/java/ru/joutak/template/storage/YamlAccountRepository.java diff --git a/src/main/java/ru/joutak/template/JoyBankPlugin.java b/src/main/java/ru/joutak/template/JoyBankPlugin.java index a3719d9..dac70f2 100644 --- a/src/main/java/ru/joutak/template/JoyBankPlugin.java +++ b/src/main/java/ru/joutak/template/JoyBankPlugin.java @@ -4,17 +4,27 @@ import org.bukkit.plugin.java.JavaPlugin; import ru.joutak.template.coin.CoinService; import ru.joutak.template.command.JoyCommand; +import ru.joutak.template.bank.BankService; +import ru.joutak.template.storage.AccountRepository; +import ru.joutak.template.storage.YamlAccountRepository; public final class JoyBankPlugin extends JavaPlugin { @Getter private static JoyBankPlugin instance; private CoinService coinService; + private BankService bankService; @Override public void onEnable() { instance = this; coinService = new CoinService(this); - getCommand("joy").setExecutor(new JoyCommand(coinService)); + AccountRepository accountRepository = + new YamlAccountRepository(this); + bankService = new BankService(accountRepository); + getCommand("joy") + .setExecutor( + new JoyCommand(coinService, bankService) + ); getLogger().info( String.format("Плагин %s версии %s включен!", getPluginMeta().getName(), getPluginMeta().getVersion()) ); diff --git a/src/main/java/ru/joutak/template/bank/BankService.java b/src/main/java/ru/joutak/template/bank/BankService.java new file mode 100644 index 0000000..23fb3f7 --- /dev/null +++ b/src/main/java/ru/joutak/template/bank/BankService.java @@ -0,0 +1,52 @@ +package ru.joutak.template.bank; + +import ru.joutak.template.storage.AccountRepository; + +import java.util.UUID; + +public class BankService { + + private final AccountRepository accountRepository; + + public BankService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + public long getBalance(UUID playerId) { + return accountRepository.getBalance(playerId); + } + + public void deposit(UUID playerId, long amount) { + if (amount <= 0) { + throw new IllegalArgumentException( + "Сумма пополнения должна быть больше нуля" + ); + } + + long currentBalance = getBalance(playerId); + + accountRepository.setBalance( + playerId, + currentBalance + amount + ); + } + + public boolean withdraw(UUID playerId, long amount) { + if (amount <= 0) { + return false; + } + + long currentBalance = getBalance(playerId); + + if (currentBalance < amount) { + return false; + } + + accountRepository.setBalance( + playerId, + currentBalance - amount + ); + + return true; + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/command/JoyCommand.java b/src/main/java/ru/joutak/template/command/JoyCommand.java index bb52287..121d3da 100644 --- a/src/main/java/ru/joutak/template/command/JoyCommand.java +++ b/src/main/java/ru/joutak/template/command/JoyCommand.java @@ -5,15 +5,18 @@ import org.bukkit.command.CommandSender; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; +import ru.joutak.template.bank.BankService; import ru.joutak.template.coin.CoinService; import org.bukkit.entity.Player; public class JoyCommand implements CommandExecutor { private final CoinService coinService; + private final BankService bankService; - public JoyCommand(CoinService coinService) { + public JoyCommand(CoinService coinService, BankService bankService) { this.coinService = coinService; + this.bankService = bankService; } @Override @@ -28,7 +31,56 @@ public boolean onCommand( return true; } if(args.length == 0){ - player.sendMessage("Команда: /joy give"); + player.sendMessage("Команда: /joy "); + return true; + } + if (args[0].equalsIgnoreCase("balance")) { + + long balance = bankService.getBalance( + player.getUniqueId() + ); + + player.sendMessage( + "Ваш баланс: " + balance + " Джой." + ); + + return true; + } + if (args[0].equalsIgnoreCase("deposit")) { + + if (args.length < 2) { + player.sendMessage("Использование: /joy deposit <сумма>"); + return true; + } + + long amount; + + try { + amount = Long.parseLong(args[1]); + } catch (NumberFormatException e) { + player.sendMessage("Сумма должна быть числом."); + return true; + } + + if (amount <= 0) { + player.sendMessage("Сумма должна быть больше нуля."); + return true; + } + + bankService.deposit( + player.getUniqueId(), + amount + ); + + long balance = bankService.getBalance( + player.getUniqueId() + ); + + player.sendMessage( + "Баланс пополнен на " + amount + + ". Текущий баланс: " + balance + " Джой." + ); + return true; } if (args[0].equalsIgnoreCase("give")) { diff --git a/src/main/java/ru/joutak/template/storage/AccountRepository.java b/src/main/java/ru/joutak/template/storage/AccountRepository.java new file mode 100644 index 0000000..d99a955 --- /dev/null +++ b/src/main/java/ru/joutak/template/storage/AccountRepository.java @@ -0,0 +1,9 @@ +package ru.joutak.template.storage; + +import java.util.UUID; + +public interface AccountRepository { + long getBalance(UUID playerId); + + void setBalance(UUID playerId, long balance); +} diff --git a/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java b/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java new file mode 100644 index 0000000..19594e7 --- /dev/null +++ b/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java @@ -0,0 +1,61 @@ +package ru.joutak.template.storage; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +public class YamlAccountRepository implements AccountRepository { + + private final JavaPlugin plugin; + private final File file; + private final YamlConfiguration configuration; + + public YamlAccountRepository(JavaPlugin plugin) { + this.plugin = plugin; + + if (!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdirs(); + } + + this.file = new File( + plugin.getDataFolder(), + "accounts.yml" + ); + + this.configuration = YamlConfiguration.loadConfiguration(file); + } + + @Override + public long getBalance(UUID playerId) { + String path = "players." + playerId + ".balance"; + + return configuration.getLong(path, 0L); + } + + @Override + public void setBalance(UUID playerId, long balance) { + String path = "players." + playerId + ".balance"; + + configuration.set(path, balance); + + save(); + } + + private void save() { + try { + configuration.save(file); + } catch (IOException e) { + plugin.getLogger().severe( + "Не удалось сохранить accounts.yml: " + e.getMessage() + ); + + throw new IllegalStateException( + "Не удалось сохранить баланс игрока", + e + ); + } + } +} \ No newline at end of file From 6974c4564ec109d834f03fc9e30f54dd0937fe39 Mon Sep 17 00:00:00 2001 From: gafru Date: Fri, 11 Sep 2026 17:05:42 +0300 Subject: [PATCH 4/8] feat: add persistent transaction history --- .../ru/joutak/template/bank/BankService.java | 23 ++++++ .../ru/joutak/template/bank/Transaction.java | 10 +++ .../joutak/template/bank/TransactionType.java | 6 ++ .../joutak/template/command/JoyCommand.java | 27 +++++++ .../template/storage/AccountRepository.java | 9 +++ .../storage/YamlAccountRepository.java | 71 ++++++++++++++++++- 6 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ru/joutak/template/bank/Transaction.java create mode 100644 src/main/java/ru/joutak/template/bank/TransactionType.java diff --git a/src/main/java/ru/joutak/template/bank/BankService.java b/src/main/java/ru/joutak/template/bank/BankService.java index 23fb3f7..8b33a3d 100644 --- a/src/main/java/ru/joutak/template/bank/BankService.java +++ b/src/main/java/ru/joutak/template/bank/BankService.java @@ -2,6 +2,8 @@ import ru.joutak.template.storage.AccountRepository; +import java.time.Instant; +import java.util.List; import java.util.UUID; public class BankService { @@ -29,6 +31,18 @@ public void deposit(UUID playerId, long amount) { playerId, currentBalance + amount ); + + accountRepository.addTransaction( + playerId, + new Transaction( + TransactionType.DEPOSIT, + amount, + Instant.now() + ) + ); + } + public List getTransactions(UUID playerId) { + return accountRepository.getTransactions(playerId); } public boolean withdraw(UUID playerId, long amount) { @@ -47,6 +61,15 @@ public boolean withdraw(UUID playerId, long amount) { currentBalance - amount ); + accountRepository.addTransaction( + playerId, + new Transaction( + TransactionType.WITHDRAW, + amount, + Instant.now() + ) + ); + return true; } } \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/bank/Transaction.java b/src/main/java/ru/joutak/template/bank/Transaction.java new file mode 100644 index 0000000..8dc33e7 --- /dev/null +++ b/src/main/java/ru/joutak/template/bank/Transaction.java @@ -0,0 +1,10 @@ +package ru.joutak.template.bank; + +import java.time.Instant; + +public record Transaction( + TransactionType type, + long amount, + Instant timestamp +) { +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/bank/TransactionType.java b/src/main/java/ru/joutak/template/bank/TransactionType.java new file mode 100644 index 0000000..5803e2b --- /dev/null +++ b/src/main/java/ru/joutak/template/bank/TransactionType.java @@ -0,0 +1,6 @@ +package ru.joutak.template.bank; + +public enum TransactionType { + DEPOSIT, + WITHDRAW +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/command/JoyCommand.java b/src/main/java/ru/joutak/template/command/JoyCommand.java index 121d3da..03e1d58 100644 --- a/src/main/java/ru/joutak/template/command/JoyCommand.java +++ b/src/main/java/ru/joutak/template/command/JoyCommand.java @@ -8,6 +8,9 @@ import ru.joutak.template.bank.BankService; import ru.joutak.template.coin.CoinService; import org.bukkit.entity.Player; +import ru.joutak.template.bank.Transaction; + +import java.util.List; public class JoyCommand implements CommandExecutor { @@ -34,6 +37,30 @@ public boolean onCommand( player.sendMessage("Команда: /joy "); return true; } + if (args[0].equalsIgnoreCase("history")) { + + List transactions = + bankService.getTransactions(player.getUniqueId()); + + if (transactions.isEmpty()) { + player.sendMessage("История операций пуста."); + return true; + } + + player.sendMessage("История операций:"); + + for (Transaction transaction : transactions) { + player.sendMessage( + transaction.type() + + " | " + + transaction.amount() + + " Джой | " + + transaction.timestamp() + ); + } + + return true; + } if (args[0].equalsIgnoreCase("balance")) { long balance = bankService.getBalance( diff --git a/src/main/java/ru/joutak/template/storage/AccountRepository.java b/src/main/java/ru/joutak/template/storage/AccountRepository.java index d99a955..f959237 100644 --- a/src/main/java/ru/joutak/template/storage/AccountRepository.java +++ b/src/main/java/ru/joutak/template/storage/AccountRepository.java @@ -1,9 +1,18 @@ package ru.joutak.template.storage; +import ru.joutak.template.bank.Transaction; + +import java.util.List; import java.util.UUID; public interface AccountRepository { long getBalance(UUID playerId); void setBalance(UUID playerId, long balance); + void addTransaction( + UUID playerId, + Transaction transaction + ); + + List getTransactions(UUID playerId); } diff --git a/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java b/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java index 19594e7..d6eb0a7 100644 --- a/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java +++ b/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java @@ -2,10 +2,14 @@ import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.java.JavaPlugin; +import ru.joutak.template.bank.Transaction; +import ru.joutak.template.bank.TransactionType; import java.io.File; import java.io.IOException; -import java.util.UUID; +import java.time.Instant; +import java.util.*; + public class YamlAccountRepository implements AccountRepository { @@ -44,6 +48,71 @@ public void setBalance(UUID playerId, long balance) { save(); } + @Override + public void addTransaction( + UUID playerId, + Transaction transaction + ) { + String path = "players." + playerId + ".transactions"; + + List> transactions = + new ArrayList<>(configuration.getMapList(path)); + + Map data = new HashMap<>(); + + data.put("type", transaction.type().name()); + data.put("amount", transaction.amount()); + data.put("timestamp", transaction.timestamp().toString()); + + transactions.add(data); + + configuration.set(path, transactions); + + save(); + } + + @Override + public List getTransactions(UUID playerId) { + String path = "players." + playerId + ".transactions"; + + List> storedTransactions = + configuration.getMapList(path); + + List result = new ArrayList<>(); + + for (Map data : storedTransactions) { + try { + TransactionType type = TransactionType.valueOf( + String.valueOf(data.get("type")) + ); + + long amount = Long.parseLong( + String.valueOf(data.get("amount")) + ); + + Instant timestamp = Instant.parse( + String.valueOf(data.get("timestamp")) + ); + + result.add( + new Transaction( + type, + amount, + timestamp + ) + ); + + } catch (Exception e) { + plugin.getLogger().warning( + "Не удалось прочитать операцию игрока " + + playerId + ); + } + } + + return result; + } + private void save() { try { configuration.save(file); From ca41b2e1e36f649337283da4a9e6a5bccc78fe2f Mon Sep 17 00:00:00 2001 From: gafru Date: Fri, 11 Sep 2026 17:21:55 +0300 Subject: [PATCH 5/8] feat: implement cash deposit and withdrawal logic --- .../ru/joutak/template/coin/CoinService.java | 107 +++++++++++++ .../joutak/template/command/JoyCommand.java | 141 ++++++++++++++++++ 2 files changed, 248 insertions(+) diff --git a/src/main/java/ru/joutak/template/coin/CoinService.java b/src/main/java/ru/joutak/template/coin/CoinService.java index f5a630a..82ae0fc 100644 --- a/src/main/java/ru/joutak/template/coin/CoinService.java +++ b/src/main/java/ru/joutak/template/coin/CoinService.java @@ -8,6 +8,9 @@ import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.inventory.PlayerInventory; + +import java.util.Map; public class CoinService { @@ -53,4 +56,108 @@ public boolean isCoin(ItemStack item) { PersistentDataType.BYTE ); } + + public int countCoins(PlayerInventory inventory) { + int total = 0; + + for (ItemStack item : inventory.getContents()) { + if (isCoin(item)) { + total += item.getAmount(); + } + } + + return total; + } + public boolean removeCoins(PlayerInventory inventory, int amount) { + if (amount <= 0) { + return false; + } + + if (countCoins(inventory) < amount) { + return false; + } + + int remaining = amount; + + for (int slot = 0; slot < inventory.getSize(); slot++) { + ItemStack item = inventory.getItem(slot); + + if (!isCoin(item)) { + continue; + } + + int stackAmount = item.getAmount(); + + if (stackAmount <= remaining) { + remaining -= stackAmount; + inventory.setItem(slot, null); + } else { + item.setAmount(stackAmount - remaining); + remaining = 0; + } + + if (remaining == 0) { + break; + } + } + + return true; + } + public int getFreeCoinCapacity(PlayerInventory inventory) { + ItemStack template = createCoin(1); + int capacity = 0; + + for (ItemStack item : inventory.getStorageContents()) { + if (item == null || item.getType().isAir()) { + capacity += template.getMaxStackSize(); + continue; + } + + if (item.isSimilar(template)) { + capacity += item.getMaxStackSize() - item.getAmount(); + } + } + return capacity; + } + + public boolean canFitCoins( + PlayerInventory inventory, + int amount + ) { + if (amount <= 0) { + return false; + } + + return getFreeCoinCapacity(inventory) >= amount; + } + public boolean addCoins( + PlayerInventory inventory, + int amount + ) { + if (!canFitCoins(inventory, amount)) { + return false; + } + + int remaining = amount; + + while (remaining > 0) { + int stackAmount = Math.min( + remaining, + 64 + ); + + ItemStack stack = createCoin(stackAmount); + + Map leftovers = + inventory.addItem(stack); + + if (!leftovers.isEmpty()) { + return false; + } + + remaining -= stackAmount; + } + + return true; + } } \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/command/JoyCommand.java b/src/main/java/ru/joutak/template/command/JoyCommand.java index 03e1d58..747e7f5 100644 --- a/src/main/java/ru/joutak/template/command/JoyCommand.java +++ b/src/main/java/ru/joutak/template/command/JoyCommand.java @@ -73,6 +73,147 @@ public boolean onCommand( return true; } + if (args[0].equalsIgnoreCase("depositcash")) { + + if (args.length < 2) { + player.sendMessage( + "Использование: /joy depositcash <сумма>" + ); + return true; + } + + int amount; + + try { + amount = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + player.sendMessage("Сумма должна быть числом."); + return true; + } + + if (amount <= 0) { + player.sendMessage("Сумма должна быть больше нуля."); + return true; + } + + int availableCoins = + coinService.countCoins(player.getInventory()); + + if (availableCoins < amount) { + player.sendMessage( + "Недостаточно наличных монет. У вас: " + + availableCoins + ); + return true; + } + + boolean removed = coinService.removeCoins( + player.getInventory(), + amount + ); + + if (!removed) { + player.sendMessage( + "Не удалось забрать монеты." + ); + return true; + } + + bankService.deposit( + player.getUniqueId(), + amount + ); + + player.sendMessage( + "Внесено на счёт: " + amount + " Джой." + ); + + return true; + } + if (args[0].equalsIgnoreCase("withdrawcash")) { + + if (args.length < 2) { + player.sendMessage( + "Использование: /joy withdrawcash <сумма>" + ); + return true; + } + + int amount; + + try { + amount = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + player.sendMessage("Сумма должна быть числом."); + return true; + } + + if (amount <= 0) { + player.sendMessage( + "Сумма должна быть больше нуля." + ); + return true; + } + + long balance = bankService.getBalance( + player.getUniqueId() + ); + + if (balance < amount) { + player.sendMessage( + "Недостаточно денег на счёте. Баланс: " + + balance + + " Джой." + ); + return true; + } + + if (!coinService.canFitCoins( + player.getInventory(), + amount + )) { + player.sendMessage( + "Недостаточно места в инвентаре." + ); + return true; + } + + boolean added = coinService.addCoins( + player.getInventory(), + amount + ); + + if (!added) { + player.sendMessage( + "Не удалось выдать монеты." + ); + return true; + } + + boolean withdrawn = bankService.withdraw( + player.getUniqueId(), + amount + ); + + if (!withdrawn) { + coinService.removeCoins( + player.getInventory(), + amount + ); + + player.sendMessage( + "Не удалось снять деньги со счёта." + ); + + return true; + } + + player.sendMessage( + "Снято со счёта: " + amount + " Джой." + ); + + return true; + } if (args[0].equalsIgnoreCase("deposit")) { if (args.length < 2) { From 3f521f630e9fae5a81fb1e211962b858c89a3104 Mon Sep 17 00:00:00 2001 From: gafru Date: Fri, 11 Sep 2026 19:03:03 +0300 Subject: [PATCH 6/8] feat: implement persistent ATM banking and custom Joy coins --- .../assets/minecraft/items/gold_nugget.json | 20 ++ .../minecraft/models/item/joy_coin.json | 6 + .../minecraft/textures/item/joy_coin.png | Bin 0 -> 4593 bytes resourcepack/JoyBankPack/pack.mcmeta | 6 + .../ru/joutak/template/JoyBankPlugin.java | 53 ++++ src/main/java/ru/joutak/template/atm/Atm.java | 15 + .../ru/joutak/template/atm/AtmService.java | 102 +++++++ .../atm/listener/AtmCreationListener.java | 92 ++++++ .../atm/listener/AtmInteractionListener.java | 56 ++++ .../atm/listener/AtmMenuListener.java | 286 ++++++++++++++++++ .../atm/listener/AtmProtectionListener.java | 165 ++++++++++ .../ru/joutak/template/bank/BankService.java | 50 +-- .../ru/joutak/template/coin/CoinService.java | 21 +- .../joutak/template/command/JoyCommand.java | 178 ----------- .../java/ru/joutak/template/gui/AtmMenu.java | 101 +++++++ .../ru/joutak/template/gui/AtmMenuHolder.java | 34 +++ .../template/storage/AccountRepository.java | 7 +- .../template/storage/AtmRepository.java | 18 ++ .../storage/YamlAccountRepository.java | 136 +++++++-- .../template/storage/YamlAtmRepository.java | 175 +++++++++++ src/main/resources/plugin.yml | 6 + 21 files changed, 1298 insertions(+), 229 deletions(-) create mode 100644 resourcepack/JoyBankPack/assets/minecraft/items/gold_nugget.json create mode 100644 resourcepack/JoyBankPack/assets/minecraft/models/item/joy_coin.json create mode 100644 resourcepack/JoyBankPack/assets/minecraft/textures/item/joy_coin.png create mode 100644 resourcepack/JoyBankPack/pack.mcmeta create mode 100644 src/main/java/ru/joutak/template/atm/Atm.java create mode 100644 src/main/java/ru/joutak/template/atm/AtmService.java create mode 100644 src/main/java/ru/joutak/template/atm/listener/AtmCreationListener.java create mode 100644 src/main/java/ru/joutak/template/atm/listener/AtmInteractionListener.java create mode 100644 src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java create mode 100644 src/main/java/ru/joutak/template/atm/listener/AtmProtectionListener.java create mode 100644 src/main/java/ru/joutak/template/gui/AtmMenu.java create mode 100644 src/main/java/ru/joutak/template/gui/AtmMenuHolder.java create mode 100644 src/main/java/ru/joutak/template/storage/AtmRepository.java create mode 100644 src/main/java/ru/joutak/template/storage/YamlAtmRepository.java diff --git a/resourcepack/JoyBankPack/assets/minecraft/items/gold_nugget.json b/resourcepack/JoyBankPack/assets/minecraft/items/gold_nugget.json new file mode 100644 index 0000000..fd4770f --- /dev/null +++ b/resourcepack/JoyBankPack/assets/minecraft/items/gold_nugget.json @@ -0,0 +1,20 @@ +{ + "model": { + "type": "minecraft:range_dispatch", + "property": "minecraft:custom_model_data", + "index": 0, + "entries": [ + { + "threshold": 1001.0, + "model": { + "type": "minecraft:model", + "model": "minecraft:item/joy_coin" + } + } + ], + "fallback": { + "type": "minecraft:model", + "model": "minecraft:item/gold_nugget" + } + } +} \ No newline at end of file diff --git a/resourcepack/JoyBankPack/assets/minecraft/models/item/joy_coin.json b/resourcepack/JoyBankPack/assets/minecraft/models/item/joy_coin.json new file mode 100644 index 0000000..3196b5a --- /dev/null +++ b/resourcepack/JoyBankPack/assets/minecraft/models/item/joy_coin.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:item/joy_coin" + } +} \ No newline at end of file diff --git a/resourcepack/JoyBankPack/assets/minecraft/textures/item/joy_coin.png b/resourcepack/JoyBankPack/assets/minecraft/textures/item/joy_coin.png new file mode 100644 index 0000000000000000000000000000000000000000..6173eedb319ab30922eec7390917b2061f13df9c GIT binary patch literal 4593 zcmdT|YgAKL77i$a7{o$&6a=c(n1F!k@KCh86jCq&LSj*dN4(Jyuz*4YQGyhN>IF~@6 ze|zt9?r}dKs*bjaHV%i=@%m_sKMtox1V3%9MPNo!Gz-787A-7?Hjq>bzU)1d-bblhx;en2iH!}6K##MmEm99Q|xJ?XQZB6fB%~qj_=fH z`;C#qW86E&GoIZJXOcIEeVqmgERI4E>hc~A39b6nw$g_}Mv0_b>(ikKN_x0I4Hhki ztxpsnfthX;>#Qn%3>{~dh?m-}-{7f_b@T+}PkU$G_3hY{ne+m(lFhz>O*vso?%jj1DJ~%!f|Ea$VpDPw(5AK^!vIV* zFn~7oMTXc53WUG(0y7pZAk@e4=vlsVdiTT;*@}ctyI|$R=_E14b5UlDS0>a2+-St6tZS-GqeG3<@u!0Vm10lS;Xo=J*s?7hFql;6!)QzQNP z4~FxcIS(5`7ni+NJKQ}Ue|5w~$>zP@7#&d3I%uBw5IE(_9&vB4Em=ZB)2xb+r2{t7 z=3Uz|b2Bws-5YYO;PLWtHmT z7#%QFfdBfBT7e%VYbYK_u4h-wEFR41d7D!r*)x*bJ(0)mZfV&X_auvp(Jr+OZs-|$ z!L>xwzRPD5ni9`r961eg=-2b&cMHbPF&hxCK93HF_gFHY`xU)uT=(jT?hy{JB=oxT z^zM$lyRIWbiJq(NaEleU_V`_XWnJ;bg-MfN>{j&d{l{B5dFJfW*+F$!bWp*dWhRyv zE+jO$2Sw4ARFP15%8pt+Kj7{ARLbsz9a*aJf4w61|oAgxQZhLQ6Fvc(cbI0tz zKwm41MjrV3tt%U-d~kJKl6TX&&ErTgZX#>`mQwz^C}Q0Qm@o7hU# zRQ7D1keSap$9b2Z*2_;R;GHs(vg+0mGNB^#qxCskleN<)$mr^&b0o){Ow%Tc)I($Q z3|7*j1Q6WYMyD9yhnOIScQ(Ofc(0?BweBllfQEdOgp!Io3CyYnXHW$T9}AJfvN$H~ zpPOy?f5_%&C3NSlb1LU`{I~M>u2zdEfxO2+pPtyx17`6@#ksa`EYWACZki>?Eogm& zHlY;4_9tL<{LVsxos=N02*lt}$q|CpwE;p<$Tb8Y$Oyq8vr*HaQ#>doA7e0(Y_V)V zlbnJgg-C!E`UCU;s;#bWOd6y1iUEe*qYO~n8dvA%lTlKC3oK%XS@d~e`0DvJ5SRo- z1O_;Nv=Fm)@h_S636=J(G9+^QP2H}C@qHmlk0Egxnd(fJFk5 zl$A?5bRKfQVeP=cBFNe(S*$G9GcegCF>OlR7e!Nu{jm9kwiA#ka5+^L08nuIOhuMm zPO@hIs)oCC1mtI!Ctma~pTuux}8;8}$dwqr%0el*hSVQfJ$?yjpQ%?jK zYwr_#hyY}2&e(EUM$nJ4_zi z_+@$uYXZq6S-X|E;8E?qwD20TMepIu-K;P?{uWn}|h=nvO{P?>r zkLrk<-C{YfR;>An5tRQ2Mqu#$8&Rt>j?Zs*nR}*xBsiZC12}AHwqX7Pn-9~VKyM!k z1NELEFvHJH?qm_h(#Alwa3s1s^0?~X&;e-dYsh4rOMLCF#`}S#$F`|*zHfU zd7SQ3fE>bO1=oC)!-h2I6-5gK^w8(6FA2YCL!2$qL;+5^Is=uniZvG^1@i9MzeT?| z^3JKd(eDHPKghp~gjwXy2ro5hc;W5mMm*r-_bcbjwJ&#nc_sHlEX893Xr;07oWO_K z>P1L|8pdnxFf5822DICzzioOGAi^c&bhS)b&C+(@8EZ6W9Va?>Q$ga(J35gF*)SkA zi_RKQTxN&mK`|Uljk4*T1V2Z#%?rycr+6?Dms7<5qOAk*Rx3`zREgsBu(W^D2}Z+F zAW2%beCf=`k`J7m{gba)c&Z)X6y+{dSsD*U<)+!DOnb(~dLfqJ;5O$lan<9d6Zd0cEt7^M?oPoxWaMe>_l=bRjgp_fUNALq-&|CIad2As_rw5#|GI=p7jp7UMR10)r8n=X<8V12hO9(>1(ALA zRNx#np&&;$Hx(+Zu^$lT+i#e!0VmBidIqS%hJKY?j)0z?68BV#t__%Ml2T~V7s2_M zR+?}k$z<*$Pf+)b4c9yrxcBo^i zHva~@v$Zm4jG!S^7uLbfe8+t`30^s~kFrMxUoxXeR*uF98#e}D^ z{)~p$;kf|yFq?Aa0X36MNZ@-S(30CVvA7C1L`LKpO!9_GeR7Eu@(P1ZtN5q=61JV0~sBNd=!FfCkPopaBSE0}WU+ h`VU`mEL6?f$>Pe+-fv;>1qbKl;j^XKot=WieGe`uBOw3) literal 0 HcmV?d00001 diff --git a/resourcepack/JoyBankPack/pack.mcmeta b/resourcepack/JoyBankPack/pack.mcmeta new file mode 100644 index 0000000..693434b --- /dev/null +++ b/resourcepack/JoyBankPack/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "pack_format": 55, + "description": "JoyBank Resource Pack" + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/JoyBankPlugin.java b/src/main/java/ru/joutak/template/JoyBankPlugin.java index dac70f2..4bdf178 100644 --- a/src/main/java/ru/joutak/template/JoyBankPlugin.java +++ b/src/main/java/ru/joutak/template/JoyBankPlugin.java @@ -2,17 +2,28 @@ import lombok.Getter; import org.bukkit.plugin.java.JavaPlugin; +import ru.joutak.template.atm.AtmService; import ru.joutak.template.coin.CoinService; import ru.joutak.template.command.JoyCommand; import ru.joutak.template.bank.BankService; import ru.joutak.template.storage.AccountRepository; import ru.joutak.template.storage.YamlAccountRepository; +import ru.joutak.template.atm.AtmService; +import ru.joutak.template.storage.AtmRepository; +import ru.joutak.template.storage.YamlAtmRepository; +import ru.joutak.template.atm.listener.AtmCreationListener; +import ru.joutak.template.gui.AtmMenu; +import ru.joutak.template.atm.listener.AtmProtectionListener; +import ru.joutak.template.gui.AtmMenuListener; +import ru.joutak.template.atm.listener.AtmInteractionListener; public final class JoyBankPlugin extends JavaPlugin { @Getter private static JoyBankPlugin instance; private CoinService coinService; private BankService bankService; + private AtmService atmService; + private AtmMenu atmMenu; @Override public void onEnable() { @@ -21,6 +32,15 @@ public void onEnable() { AccountRepository accountRepository = new YamlAccountRepository(this); bankService = new BankService(accountRepository); + + AtmRepository atmRepository = + new YamlAtmRepository(this); + + atmService = + new AtmService(atmRepository); + + atmMenu = new AtmMenu(bankService); + getCommand("joy") .setExecutor( new JoyCommand(coinService, bankService) @@ -28,6 +48,39 @@ public void onEnable() { getLogger().info( String.format("Плагин %s версии %s включен!", getPluginMeta().getName(), getPluginMeta().getVersion()) ); + + getServer() + .getPluginManager() + .registerEvents( + new AtmCreationListener(atmService), + this + ); + getServer() + .getPluginManager() + .registerEvents( + new AtmInteractionListener( + atmService, + atmMenu + ), + this + ); + getServer() + .getPluginManager() + .registerEvents( + new AtmMenuListener( + coinService, + bankService, + atmService, + atmMenu + ), + this + ); + getServer() + .getPluginManager() + .registerEvents( + new AtmProtectionListener(atmService), + this + ); } @Override diff --git a/src/main/java/ru/joutak/template/atm/Atm.java b/src/main/java/ru/joutak/template/atm/Atm.java new file mode 100644 index 0000000..fd3fcb6 --- /dev/null +++ b/src/main/java/ru/joutak/template/atm/Atm.java @@ -0,0 +1,15 @@ +package ru.joutak.template.atm; + +import java.util.UUID; + +public record Atm( + UUID id, + UUID worldId, + int barrelX, + int barrelY, + int barrelZ, + int signX, + int signY, + int signZ +) { +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/atm/AtmService.java b/src/main/java/ru/joutak/template/atm/AtmService.java new file mode 100644 index 0000000..9ce0dcf --- /dev/null +++ b/src/main/java/ru/joutak/template/atm/AtmService.java @@ -0,0 +1,102 @@ +package ru.joutak.template.atm; + +import org.bukkit.Location; +import org.bukkit.World; +import ru.joutak.template.storage.AtmRepository; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public class AtmService { + + private final AtmRepository atmRepository; + private final Map atms = new HashMap<>(); + + public AtmService(AtmRepository atmRepository) { + this.atmRepository = atmRepository; + + for (Atm atm : atmRepository.findAll()) { + atms.put(atm.id(), atm); + } + } + + public Atm register( + Location barrelLocation, + Location signLocation + ) { + Atm atm = new Atm( + UUID.randomUUID(), + barrelLocation.getWorld().getUID(), + + barrelLocation.getBlockX(), + barrelLocation.getBlockY(), + barrelLocation.getBlockZ(), + + signLocation.getBlockX(), + signLocation.getBlockY(), + signLocation.getBlockZ() + ); + + atms.put(atm.id(), atm); + atmRepository.save(atm); + + return atm; + } + + public Optional findByBlock(Location location) { + World world = location.getWorld(); + + if (world == null) { + return Optional.empty(); + } + + for (Atm atm : atms.values()) { + + if (!atm.worldId().equals(world.getUID())) { + continue; + } + + int x = location.getBlockX(); + int y = location.getBlockY(); + int z = location.getBlockZ(); + + boolean isBarrel = + x == atm.barrelX() + && y == atm.barrelY() + && z == atm.barrelZ(); + + boolean isSign = + x == atm.signX() + && y == atm.signY() + && z == atm.signZ(); + + boolean isBase = + x == atm.barrelX() + && y == atm.barrelY() - 1 + && z == atm.barrelZ(); + + if (isBarrel || isSign || isBase) { + return Optional.of(atm); + } + } + + return Optional.empty(); + } + + public Collection getAll() { + return atms.values(); + } + + public void unregister(UUID atmId) { + atms.remove(atmId); + atmRepository.delete(atmId); + } + public Optional findById(UUID atmId) { + return Optional.ofNullable( + atms.get(atmId) + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/atm/listener/AtmCreationListener.java b/src/main/java/ru/joutak/template/atm/listener/AtmCreationListener.java new file mode 100644 index 0000000..38ab367 --- /dev/null +++ b/src/main/java/ru/joutak/template/atm/listener/AtmCreationListener.java @@ -0,0 +1,92 @@ +package ru.joutak.template.atm.listener; + +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.WallSign; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.SignChangeEvent; +import ru.joutak.template.atm.AtmService; + +public class AtmCreationListener implements Listener { + + private final AtmService atmService; + + public AtmCreationListener(AtmService atmService) { + this.atmService = atmService; + } + + @EventHandler + public void onSignChange(SignChangeEvent event) { + + String firstLine = event.getLine(0); + + if (firstLine == null + || !firstLine.equalsIgnoreCase("[joybank]")) { + return; + } + + if (!event.getPlayer() + .hasPermission("joybank.atm.create")) { + + event.getPlayer().sendMessage( + "У вас нет прав на создание банкомата." + ); + + return; + } + + Block signBlock = event.getBlock(); + + BlockData blockData = signBlock.getBlockData(); + + if (!(blockData instanceof WallSign wallSign)) { + event.getPlayer().sendMessage( + "Табличка должна быть прикреплена к боку бочки." + ); + return; + } + + Block barrel = signBlock.getRelative( + wallSign.getFacing().getOppositeFace() + ); + + if (barrel.getType() != Material.BARREL) { + event.getPlayer().sendMessage( + "Табличка должна быть прикреплена к бочке." + ); + return; + } + + Block base = barrel.getRelative(0, -1, 0); + + if (base.getType() != Material.IRON_BLOCK) { + event.getPlayer().sendMessage( + "Под бочкой должен находиться железный блок." + ); + return; + } + + if (atmService.findByBlock(barrel.getLocation()).isPresent()) { + event.getPlayer().sendMessage( + "Этот банкомат уже зарегистрирован." + ); + return; + } + + atmService.register( + barrel.getLocation(), + signBlock.getLocation() + ); + + event.setLine(0, "JOY BANK"); + event.setLine(1, "Банкомат"); + event.setLine(2, ""); + event.setLine(3, ""); + + event.getPlayer().sendMessage( + "Банкомат успешно зарегистрирован." + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/atm/listener/AtmInteractionListener.java b/src/main/java/ru/joutak/template/atm/listener/AtmInteractionListener.java new file mode 100644 index 0000000..5504c5e --- /dev/null +++ b/src/main/java/ru/joutak/template/atm/listener/AtmInteractionListener.java @@ -0,0 +1,56 @@ +package ru.joutak.template.atm.listener; + +import org.bukkit.block.Block; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import ru.joutak.template.atm.Atm; +import ru.joutak.template.atm.AtmService; +import ru.joutak.template.gui.AtmMenu; + +import java.util.Optional; + +public class AtmInteractionListener implements Listener { + + private final AtmService atmService; + private final AtmMenu atmMenu; + + public AtmInteractionListener( + AtmService atmService, + AtmMenu atmMenu + ) { + this.atmService = atmService; + this.atmMenu = atmMenu; + } + + @EventHandler + public void onInteract(PlayerInteractEvent event) { + + if (event.getAction() != Action.RIGHT_CLICK_BLOCK) { + return; + } + + Block block = event.getClickedBlock(); + + if (block == null) { + return; + } + + Optional atm = + atmService.findByBlock( + block.getLocation() + ); + + if (atm.isEmpty()) { + return; + } + + event.setCancelled(true); + + atmMenu.open( + event.getPlayer(), + atm.get() + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java b/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java new file mode 100644 index 0000000..81b7766 --- /dev/null +++ b/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java @@ -0,0 +1,286 @@ +package ru.joutak.template.gui; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import ru.joutak.template.atm.Atm; +import ru.joutak.template.atm.AtmService; +import ru.joutak.template.bank.BankService; +import ru.joutak.template.coin.CoinService; + +import java.util.Optional; + +public class AtmMenuListener implements Listener { + + private final CoinService coinService; + private final BankService bankService; + private final AtmService atmService; + private final AtmMenu atmMenu; + + public AtmMenuListener( + CoinService coinService, + BankService bankService, + AtmService atmService, + AtmMenu atmMenu + ) { + this.coinService = coinService; + this.bankService = bankService; + this.atmService = atmService; + this.atmMenu = atmMenu; + } + + @EventHandler + public void onClick(InventoryClickEvent event) { + + if (!(event.getView() + .getTopInventory() + .getHolder(false) + instanceof AtmMenuHolder holder)) { + return; + } + event.setCancelled(true); + + if (!(event.getWhoClicked() instanceof Player player)) { + return; + } + if (event.getRawSlot() + >= event.getView().getTopInventory().getSize()) { + return; + } + + Optional optionalAtm = + atmService.findById(holder.getAtmId()); + + if (optionalAtm.isEmpty()) { + player.closeInventory(); + player.sendMessage("Этот банкомат больше не существует."); + return; + } + + Atm atm = optionalAtm.get(); + + switch (event.getRawSlot()) { + + case 9 -> deposit(player, atm, 1); + case 10 -> deposit(player, atm, 16); + case 11 -> deposit(player, atm, 64); + case 12 -> depositAll(player, atm); + + case 14 -> withdraw(player, atm, 1); + case 15 -> withdraw(player, atm, 16); + case 16 -> withdraw(player, atm, 64); + case 17 -> withdrawAll(player, atm); + } + } + + @EventHandler + public void onDrag(InventoryDragEvent event) { + + if (event.getView() + .getTopInventory() + .getHolder(false) + instanceof AtmMenuHolder) { + + event.setCancelled(true); + } + } + + private void deposit( + Player player, + Atm atm, + int amount + ) { + + int available = + coinService.countCoins( + player.getInventory() + ); + + if (available < amount) { + player.sendMessage( + "Недостаточно наличных. У вас: " + + available + " Джой." + ); + return; + } + + boolean removed = + coinService.removeCoins( + player.getInventory(), + amount + ); + + if (!removed) { + player.sendMessage( + "Не удалось забрать монеты." + ); + return; + } + + try { + + bankService.deposit( + player.getUniqueId(), + amount + ); + + } catch (RuntimeException e) { + + // Возвращаем физические деньги + coinService.addCoins( + player.getInventory(), + amount + ); + + player.sendMessage( + "Ошибка сохранения. Монеты возвращены." + ); + + return; + } + + player.sendMessage( + "Внесено: " + amount + " Джой." + ); + + atmMenu.open(player, atm); + } + private void depositAll( + Player player, + Atm atm + ) { + + int amount = + coinService.countCoins( + player.getInventory() + ); + + if (amount == 0) { + player.sendMessage( + "У вас нет наличных Джой." + ); + return; + } + + deposit( + player, + atm, + amount + ); + } + private void withdraw( + Player player, + Atm atm, + int amount + ) { + + long balance = + bankService.getBalance( + player.getUniqueId() + ); + + if (balance < amount) { + player.sendMessage( + "Недостаточно денег на счёте. Баланс: " + + balance + " Джой." + ); + return; + } + + if (!coinService.canFitCoins( + player.getInventory(), + amount + )) { + player.sendMessage( + "Недостаточно места в инвентаре." + ); + return; + } + + boolean added = + coinService.addCoins( + player.getInventory(), + amount + ); + + if (!added) { + player.sendMessage( + "Не удалось выдать монеты." + ); + return; + } + + try { + + boolean withdrawn = + bankService.withdraw( + player.getUniqueId(), + amount + ); + + if (!withdrawn) { + + coinService.removeCoins( + player.getInventory(), + amount + ); + + player.sendMessage( + "Не удалось снять деньги." + ); + + return; + } + + } catch (RuntimeException e) { + coinService.removeCoins( + player.getInventory(), + amount + ); + + player.sendMessage( + "Ошибка сохранения. Операция отменена." + ); + + return; + } + + player.sendMessage( + "Снято: " + amount + " Джой." + ); + + atmMenu.open(player, atm); + } + private void withdrawAll( + Player player, + Atm atm + ) { + + long balance = + bankService.getBalance( + player.getUniqueId() + ); + + if (balance <= 0) { + player.sendMessage( + "На счёте нет денег." + ); + return; + } + + if (balance > Integer.MAX_VALUE) { + player.sendMessage( + "Баланс слишком большой для одной операции." + ); + return; + } + + withdraw( + player, + atm, + (int) balance + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/atm/listener/AtmProtectionListener.java b/src/main/java/ru/joutak/template/atm/listener/AtmProtectionListener.java new file mode 100644 index 0000000..676a350 --- /dev/null +++ b/src/main/java/ru/joutak/template/atm/listener/AtmProtectionListener.java @@ -0,0 +1,165 @@ +package ru.joutak.template.atm.listener; + +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import ru.joutak.template.atm.Atm; +import ru.joutak.template.atm.AtmService; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockBurnEvent; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.block.BlockPistonExtendEvent; +import org.bukkit.event.block.BlockPistonRetractEvent; +import org.bukkit.event.entity.EntityChangeBlockEvent; +import org.bukkit.event.entity.EntityExplodeEvent; + +import java.util.Optional; + +public class AtmProtectionListener implements Listener { + + private final AtmService atmService; + + public AtmProtectionListener(AtmService atmService) { + this.atmService = atmService; + } + + @EventHandler + public void onBlockBreak(BlockBreakEvent event) { + + Block block = event.getBlock(); + + Optional optionalAtm = + atmService.findByBlock(block.getLocation()); + if (optionalAtm.isEmpty()) { + return; + } + + Player player = event.getPlayer(); + + if (!player.hasPermission("joybank.atm.break")) { + + event.setCancelled(true); + + player.sendMessage( + "Вы не можете разрушить зарегистрированный банкомат." + ); + + return; + } + + Atm atm = optionalAtm.get(); + + atmService.unregister(atm.id()); + + player.sendMessage( + "Банкомат удалён." + ); + } + @EventHandler + public void onEntityExplode(EntityExplodeEvent event) { + event.blockList().removeIf(this::isAtmBlock); + } + + @EventHandler + public void onBlockExplode(BlockExplodeEvent event) { + event.blockList().removeIf(this::isAtmBlock); + } + private boolean isAtmBlock(Block block) { + return atmService + .findByBlock(block.getLocation()) + .isPresent(); + } + + @EventHandler + public void onPistonExtend(BlockPistonExtendEvent event) { + + boolean movesAtm = event.getBlocks() + .stream() + .anyMatch(this::isAtmBlock); + + if (movesAtm) { + event.setCancelled(true); + } + } + + @EventHandler + public void onPistonRetract(BlockPistonRetractEvent event) { + + boolean movesAtm = event.getBlocks() + .stream() + .anyMatch(this::isAtmBlock); + + if (movesAtm) { + event.setCancelled(true); + } + } + + @EventHandler + public void onBurn(BlockBurnEvent event) { + if (isAtmBlock(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler + public void onEntityChangeBlock(EntityChangeBlockEvent event) { + if (isAtmBlock(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler( + priority = EventPriority.HIGHEST, + ignoreCancelled = true + ) + public void onBlockBreakProtection(BlockBreakEvent event) { + + if (!isAtmBlock(event.getBlock())) { + return; + } + + if (!event.getPlayer() + .hasPermission("joybank.atm.break")) { + + event.setCancelled(true); + + event.getPlayer().sendMessage( + "Вы не можете разрушить зарегистрированный банкомат." + ); + } + } + + @EventHandler( + priority = EventPriority.MONITOR, + ignoreCancelled = true + ) + public void onBlockBreakCleanup(BlockBreakEvent event) { + + Optional atm = + atmService.findByBlock( + event.getBlock().getLocation() + ); + + if (atm.isEmpty()) { + return; + } + + if (!event.getPlayer() + .hasPermission("joybank.atm.break")) { + return; + } + + atmService.unregister( + atm.get().id() + ); + + event.getPlayer().sendMessage( + "Банкомат удалён." + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/bank/BankService.java b/src/main/java/ru/joutak/template/bank/BankService.java index 8b33a3d..5004a34 100644 --- a/src/main/java/ru/joutak/template/bank/BankService.java +++ b/src/main/java/ru/joutak/template/bank/BankService.java @@ -18,7 +18,12 @@ public long getBalance(UUID playerId) { return accountRepository.getBalance(playerId); } + public List getTransactions(UUID playerId) { + return accountRepository.getTransactions(playerId); + } + public void deposit(UUID playerId, long amount) { + if (amount <= 0) { throw new IllegalArgumentException( "Сумма пополнения должна быть больше нуля" @@ -27,25 +32,27 @@ public void deposit(UUID playerId, long amount) { long currentBalance = getBalance(playerId); - accountRepository.setBalance( - playerId, - currentBalance + amount + long newBalance = Math.addExact( + currentBalance, + amount + ); + + Transaction transaction = new Transaction( + TransactionType.DEPOSIT, + amount, + Instant.now() ); - accountRepository.addTransaction( + accountRepository.applyTransaction( playerId, - new Transaction( - TransactionType.DEPOSIT, - amount, - Instant.now() - ) + newBalance, + transaction ); } - public List getTransactions(UUID playerId) { - return accountRepository.getTransactions(playerId); - } + public boolean withdraw(UUID playerId, long amount) { + if (amount <= 0) { return false; } @@ -56,18 +63,19 @@ public boolean withdraw(UUID playerId, long amount) { return false; } - accountRepository.setBalance( - playerId, - currentBalance - amount + long newBalance = + currentBalance - amount; + + Transaction transaction = new Transaction( + TransactionType.WITHDRAW, + amount, + Instant.now() ); - accountRepository.addTransaction( + accountRepository.applyTransaction( playerId, - new Transaction( - TransactionType.WITHDRAW, - amount, - Instant.now() - ) + newBalance, + transaction ); return true; diff --git a/src/main/java/ru/joutak/template/coin/CoinService.java b/src/main/java/ru/joutak/template/coin/CoinService.java index 82ae0fc..9b084fd 100644 --- a/src/main/java/ru/joutak/template/coin/CoinService.java +++ b/src/main/java/ru/joutak/template/coin/CoinService.java @@ -9,24 +9,32 @@ import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.inventory.PlayerInventory; +import org.bukkit.inventory.meta.components.CustomModelDataComponent; + +import java.util.List; import java.util.Map; public class CoinService { private final NamespacedKey coinKey; + private static final float JOY_MODEL_DATA = 1001.0f; public CoinService(JavaPlugin plugin) { this.coinKey = new NamespacedKey(plugin, "joy_coin"); } public ItemStack createCoin(int amount) { - ItemStack coin = new ItemStack(Material.GOLD_NUGGET, amount); + ItemStack coin = + new ItemStack(Material.GOLD_NUGGET, amount); ItemMeta meta = coin.getItemMeta(); meta.displayName( - Component.text("Джой", NamedTextColor.GOLD) + Component.text( + "Джой", + NamedTextColor.GOLD + ) ); meta.getPersistentDataContainer().set( @@ -35,6 +43,15 @@ public ItemStack createCoin(int amount) { (byte) 1 ); + CustomModelDataComponent modelData = + meta.getCustomModelDataComponent(); + + modelData.setFloats( + List.of(JOY_MODEL_DATA) + ); + + meta.setCustomModelDataComponent(modelData); + coin.setItemMeta(meta); return coin; diff --git a/src/main/java/ru/joutak/template/command/JoyCommand.java b/src/main/java/ru/joutak/template/command/JoyCommand.java index 747e7f5..fa0346f 100644 --- a/src/main/java/ru/joutak/template/command/JoyCommand.java +++ b/src/main/java/ru/joutak/template/command/JoyCommand.java @@ -73,184 +73,6 @@ public boolean onCommand( return true; } - if (args[0].equalsIgnoreCase("depositcash")) { - - if (args.length < 2) { - player.sendMessage( - "Использование: /joy depositcash <сумма>" - ); - return true; - } - - int amount; - - try { - amount = Integer.parseInt(args[1]); - } catch (NumberFormatException e) { - player.sendMessage("Сумма должна быть числом."); - return true; - } - - if (amount <= 0) { - player.sendMessage("Сумма должна быть больше нуля."); - return true; - } - - int availableCoins = - coinService.countCoins(player.getInventory()); - - if (availableCoins < amount) { - player.sendMessage( - "Недостаточно наличных монет. У вас: " - + availableCoins - ); - return true; - } - - boolean removed = coinService.removeCoins( - player.getInventory(), - amount - ); - - if (!removed) { - player.sendMessage( - "Не удалось забрать монеты." - ); - return true; - } - - bankService.deposit( - player.getUniqueId(), - amount - ); - - player.sendMessage( - "Внесено на счёт: " + amount + " Джой." - ); - - return true; - } - if (args[0].equalsIgnoreCase("withdrawcash")) { - - if (args.length < 2) { - player.sendMessage( - "Использование: /joy withdrawcash <сумма>" - ); - return true; - } - - int amount; - - try { - amount = Integer.parseInt(args[1]); - } catch (NumberFormatException e) { - player.sendMessage("Сумма должна быть числом."); - return true; - } - - if (amount <= 0) { - player.sendMessage( - "Сумма должна быть больше нуля." - ); - return true; - } - - long balance = bankService.getBalance( - player.getUniqueId() - ); - - if (balance < amount) { - player.sendMessage( - "Недостаточно денег на счёте. Баланс: " - + balance - + " Джой." - ); - return true; - } - - if (!coinService.canFitCoins( - player.getInventory(), - amount - )) { - player.sendMessage( - "Недостаточно места в инвентаре." - ); - return true; - } - - boolean added = coinService.addCoins( - player.getInventory(), - amount - ); - - if (!added) { - player.sendMessage( - "Не удалось выдать монеты." - ); - return true; - } - - boolean withdrawn = bankService.withdraw( - player.getUniqueId(), - amount - ); - - if (!withdrawn) { - coinService.removeCoins( - player.getInventory(), - amount - ); - - player.sendMessage( - "Не удалось снять деньги со счёта." - ); - - return true; - } - - player.sendMessage( - "Снято со счёта: " + amount + " Джой." - ); - - return true; - } - if (args[0].equalsIgnoreCase("deposit")) { - - if (args.length < 2) { - player.sendMessage("Использование: /joy deposit <сумма>"); - return true; - } - - long amount; - - try { - amount = Long.parseLong(args[1]); - } catch (NumberFormatException e) { - player.sendMessage("Сумма должна быть числом."); - return true; - } - - if (amount <= 0) { - player.sendMessage("Сумма должна быть больше нуля."); - return true; - } - - bankService.deposit( - player.getUniqueId(), - amount - ); - - long balance = bankService.getBalance( - player.getUniqueId() - ); - - player.sendMessage( - "Баланс пополнен на " + amount + - ". Текущий баланс: " + balance + " Джой." - ); - - return true; - } if (args[0].equalsIgnoreCase("give")) { if (!player.hasPermission("joybank.admin.give")) { diff --git a/src/main/java/ru/joutak/template/gui/AtmMenu.java b/src/main/java/ru/joutak/template/gui/AtmMenu.java new file mode 100644 index 0000000..a7398f7 --- /dev/null +++ b/src/main/java/ru/joutak/template/gui/AtmMenu.java @@ -0,0 +1,101 @@ +package ru.joutak.template.gui; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import ru.joutak.template.atm.Atm; +import ru.joutak.template.bank.BankService; + +public class AtmMenu { + + private final BankService bankService; + + public AtmMenu(BankService bankService) { + this.bankService = bankService; + } + + public void open(Player player, Atm atm) { + + AtmMenuHolder holder = + new AtmMenuHolder(atm.id()); + + long balance = bankService.getBalance( + player.getUniqueId() + ); + + holder.getInventory().setItem( + 4, + createButton( + Material.GOLD_INGOT, + "Баланс: " + balance + " Джой", + NamedTextColor.GOLD + ) + ); + + // Внести + holder.getInventory().setItem( + 9, + createButton(Material.LIME_WOOL, "Внести 1", NamedTextColor.GREEN) + ); + + holder.getInventory().setItem( + 10, + createButton(Material.LIME_WOOL, "Внести 16", NamedTextColor.GREEN) + ); + + holder.getInventory().setItem( + 11, + createButton(Material.LIME_WOOL, "Внести 64", NamedTextColor.GREEN) + ); + + holder.getInventory().setItem( + 12, + createButton(Material.EMERALD_BLOCK, "Внести всё", NamedTextColor.GREEN) + ); + + // Снять + holder.getInventory().setItem( + 14, + createButton(Material.RED_WOOL, "Снять 1", NamedTextColor.RED) + ); + + holder.getInventory().setItem( + 15, + createButton(Material.RED_WOOL, "Снять 16", NamedTextColor.RED) + ); + + holder.getInventory().setItem( + 16, + createButton(Material.RED_WOOL, "Снять 64", NamedTextColor.RED) + ); + + holder.getInventory().setItem( + 17, + createButton(Material.REDSTONE_BLOCK, "Снять всё", NamedTextColor.RED) + ); + + player.openInventory(holder.getInventory()); + } + + private ItemStack createButton( + Material material, + String name, + NamedTextColor color + ) { + + ItemStack item = new ItemStack(material); + + ItemMeta meta = item.getItemMeta(); + + meta.displayName( + Component.text(name, color) + ); + + item.setItemMeta(meta); + + return item; + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/gui/AtmMenuHolder.java b/src/main/java/ru/joutak/template/gui/AtmMenuHolder.java new file mode 100644 index 0000000..66fc3f8 --- /dev/null +++ b/src/main/java/ru/joutak/template/gui/AtmMenuHolder.java @@ -0,0 +1,34 @@ +package ru.joutak.template.gui; + +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class AtmMenuHolder implements InventoryHolder { + + private final UUID atmId; + private final Inventory inventory; + + public AtmMenuHolder(UUID atmId) { + this.atmId = atmId; + + this.inventory = Bukkit.createInventory( + this, + 27, + Component.text("JoyBank") + ); + } + + public UUID getAtmId() { + return atmId; + } + + @Override + public @NotNull Inventory getInventory() { + return inventory; + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/storage/AccountRepository.java b/src/main/java/ru/joutak/template/storage/AccountRepository.java index f959237..f1d5197 100644 --- a/src/main/java/ru/joutak/template/storage/AccountRepository.java +++ b/src/main/java/ru/joutak/template/storage/AccountRepository.java @@ -6,13 +6,14 @@ import java.util.UUID; public interface AccountRepository { + long getBalance(UUID playerId); - void setBalance(UUID playerId, long balance); - void addTransaction( + void applyTransaction( UUID playerId, + long newBalance, Transaction transaction ); List getTransactions(UUID playerId); -} +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/storage/AtmRepository.java b/src/main/java/ru/joutak/template/storage/AtmRepository.java new file mode 100644 index 0000000..0e7a85e --- /dev/null +++ b/src/main/java/ru/joutak/template/storage/AtmRepository.java @@ -0,0 +1,18 @@ +package ru.joutak.template.storage; + +import ru.joutak.template.atm.Atm; + +import java.util.Collection; +import java.util.Optional; +import java.util.UUID; + +public interface AtmRepository { + + void save(Atm atm); + + void delete(UUID atmId); + + Optional findById(UUID atmId); + + Collection findAll(); +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java b/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java index d6eb0a7..e74a7c9 100644 --- a/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java +++ b/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java @@ -4,6 +4,11 @@ import org.bukkit.plugin.java.JavaPlugin; import ru.joutak.template.bank.Transaction; import ru.joutak.template.bank.TransactionType; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.io.File; import java.io.IOException; @@ -40,35 +45,80 @@ public long getBalance(UUID playerId) { } @Override - public void setBalance(UUID playerId, long balance) { - String path = "players." + playerId + ".balance"; - - configuration.set(path, balance); - - save(); - } - - @Override - public void addTransaction( + public void applyTransaction( UUID playerId, + long newBalance, Transaction transaction ) { - String path = "players." + playerId + ".transactions"; - List> transactions = - new ArrayList<>(configuration.getMapList(path)); + if (newBalance < 0) { + throw new IllegalArgumentException( + "Баланс не может быть отрицательным" + ); + } + + String playerPath = + "players." + playerId; + String balancePath = + playerPath + ".balance"; + String transactionsPath = + playerPath + ".transactions"; + Object oldBalance = + configuration.get(balancePath); + + List> oldTransactions = + new ArrayList<>( + configuration.getMapList( + transactionsPath + ) + ); + List> newTransactions = + new ArrayList<>(oldTransactions); + + Map data = + new HashMap<>(); + data.put( + "type", + transaction.type().name() + ); + data.put( + "amount", + transaction.amount() + ); + data.put( + "timestamp", + transaction.timestamp().toString() + ); + newTransactions.add(data); + + try { - Map data = new HashMap<>(); + configuration.set( + balancePath, + newBalance + ); + + configuration.set( + transactionsPath, + newTransactions + ); - data.put("type", transaction.type().name()); - data.put("amount", transaction.amount()); - data.put("timestamp", transaction.timestamp().toString()); + save(); - transactions.add(data); + } catch (RuntimeException e) { + + configuration.set( + balancePath, + oldBalance + ); - configuration.set(path, transactions); + configuration.set( + transactionsPath, + oldTransactions + ); - save(); + throw e; + } } @Override @@ -114,15 +164,51 @@ public List getTransactions(UUID playerId) { } private void save() { + + Path target = file.toPath(); + + Path temp = target.resolveSibling( + file.getName() + ".tmp" + ); + try { - configuration.save(file); - } catch (IOException e) { - plugin.getLogger().severe( - "Не удалось сохранить accounts.yml: " + e.getMessage() + + String yaml = + configuration.saveToString(); + + Files.writeString( + temp, + yaml, + StandardCharsets.UTF_8 ); + try { + + Files.move( + temp, + target, + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING + ); + + } catch (AtomicMoveNotSupportedException e) { + + Files.move( + temp, + target, + StandardCopyOption.REPLACE_EXISTING + ); + } + + } catch (IOException e) { + + try { + Files.deleteIfExists(temp); + } catch (IOException ignored) { + } + throw new IllegalStateException( - "Не удалось сохранить баланс игрока", + "Не удалось сохранить accounts.yml", e ); } diff --git a/src/main/java/ru/joutak/template/storage/YamlAtmRepository.java b/src/main/java/ru/joutak/template/storage/YamlAtmRepository.java new file mode 100644 index 0000000..b59aee6 --- /dev/null +++ b/src/main/java/ru/joutak/template/storage/YamlAtmRepository.java @@ -0,0 +1,175 @@ +package ru.joutak.template.storage; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; +import ru.joutak.template.atm.Atm; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; +import java.util.UUID; + +public class YamlAtmRepository implements AtmRepository { + + private final JavaPlugin plugin; + private final File file; + private final YamlConfiguration configuration; + + public YamlAtmRepository(JavaPlugin plugin) { + this.plugin = plugin; + + if (!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdirs(); + } + + this.file = new File( + plugin.getDataFolder(), + "atms.yml" + ); + + this.configuration = + YamlConfiguration.loadConfiguration(file); + } + + @Override + public void save(Atm atm) { + String path = "atms." + atm.id(); + + configuration.set( + path + ".world", + atm.worldId().toString() + ); + + configuration.set( + path + ".barrel.x", + atm.barrelX() + ); + configuration.set( + path + ".barrel.y", + atm.barrelY() + ); + configuration.set( + path + ".barrel.z", + atm.barrelZ() + ); + + configuration.set( + path + ".sign.x", + atm.signX() + ); + configuration.set( + path + ".sign.y", + atm.signY() + ); + configuration.set( + path + ".sign.z", + atm.signZ() + ); + + saveFile(); + } + + @Override + public void delete(UUID atmId) { + configuration.set( + "atms." + atmId, + null + ); + + saveFile(); + } + + @Override + public Optional findById(UUID atmId) { + String path = "atms." + atmId; + + if (!configuration.contains(path)) { + return Optional.empty(); + } + + return Optional.of(readAtm(path, atmId)); + } + + @Override + public Collection findAll() { + Collection result = new ArrayList<>(); + + ConfigurationSection section = + configuration.getConfigurationSection("atms"); + + if (section == null) { + return result; + } + + for (String idString : section.getKeys(false)) { + + try { + UUID id = UUID.fromString(idString); + + result.add( + readAtm( + "atms." + idString, + id + ) + ); + + } catch (IllegalArgumentException e) { + plugin.getLogger().warning( + "Некорректный ATM id: " + idString + ); + } + } + + return result; + } + + private Atm readAtm( + String path, + UUID id + ) { + UUID worldId = UUID.fromString( + configuration.getString( + path + ".world" + ) + ); + + return new Atm( + id, + worldId, + + configuration.getInt( + path + ".barrel.x" + ), + configuration.getInt( + path + ".barrel.y" + ), + configuration.getInt( + path + ".barrel.z" + ), + + configuration.getInt( + path + ".sign.x" + ), + configuration.getInt( + path + ".sign.y" + ), + configuration.getInt( + path + ".sign.z" + ) + ); + } + + private void saveFile() { + try { + configuration.save(file); + } catch (IOException e) { + throw new IllegalStateException( + "Не удалось сохранить atms.yml", + e + ); + } + } +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 2a04626..9de1ab0 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -18,4 +18,10 @@ commands: permissions: joybank.admin.give: description: Позволяет выдавать монеты Джой + default: op + joybank.atm.create: + description: Позволяет создавать банкоматы + default: op + joybank.atm.break: + description: Позволяет удалять зарегистрированные банкоматы default: op \ No newline at end of file From 89626c721e32d03d3d64c14ca04f651847402aae Mon Sep 17 00:00:00 2001 From: gafru Date: Sun, 13 Sep 2026 20:41:24 +0300 Subject: [PATCH 7/8] fix: address pr review commets --- .../ru/joutak/template/JoyBankPlugin.java | 4 +-- .../ru/joutak/template/atm/AtmService.java | 6 ++-- .../atm/listener/AtmMenuListener.java | 4 ++- .../atm/listener/AtmProtectionListener.java | 31 ------------------- .../joutak/template/command/JoyCommand.java | 11 +++++-- src/main/resources/plugin.yml | 2 +- 6 files changed, 17 insertions(+), 41 deletions(-) diff --git a/src/main/java/ru/joutak/template/JoyBankPlugin.java b/src/main/java/ru/joutak/template/JoyBankPlugin.java index 4bdf178..d44a5ef 100644 --- a/src/main/java/ru/joutak/template/JoyBankPlugin.java +++ b/src/main/java/ru/joutak/template/JoyBankPlugin.java @@ -3,18 +3,18 @@ import lombok.Getter; import org.bukkit.plugin.java.JavaPlugin; import ru.joutak.template.atm.AtmService; +import ru.joutak.template.atm.listener.AtmMenuListener; import ru.joutak.template.coin.CoinService; import ru.joutak.template.command.JoyCommand; import ru.joutak.template.bank.BankService; import ru.joutak.template.storage.AccountRepository; import ru.joutak.template.storage.YamlAccountRepository; -import ru.joutak.template.atm.AtmService; import ru.joutak.template.storage.AtmRepository; import ru.joutak.template.storage.YamlAtmRepository; import ru.joutak.template.atm.listener.AtmCreationListener; import ru.joutak.template.gui.AtmMenu; import ru.joutak.template.atm.listener.AtmProtectionListener; -import ru.joutak.template.gui.AtmMenuListener; + import ru.joutak.template.atm.listener.AtmInteractionListener; public final class JoyBankPlugin extends JavaPlugin { diff --git a/src/main/java/ru/joutak/template/atm/AtmService.java b/src/main/java/ru/joutak/template/atm/AtmService.java index 9ce0dcf..df037ec 100644 --- a/src/main/java/ru/joutak/template/atm/AtmService.java +++ b/src/main/java/ru/joutak/template/atm/AtmService.java @@ -39,9 +39,8 @@ public Atm register( signLocation.getBlockY(), signLocation.getBlockZ() ); - - atms.put(atm.id(), atm); atmRepository.save(atm); + atms.put(atm.id(), atm); return atm; } @@ -91,8 +90,9 @@ public Collection getAll() { } public void unregister(UUID atmId) { - atms.remove(atmId); + atmRepository.delete(atmId); + atms.remove(atmId); } public Optional findById(UUID atmId) { return Optional.ofNullable( diff --git a/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java b/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java index 81b7766..c367981 100644 --- a/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java +++ b/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java @@ -1,4 +1,4 @@ -package ru.joutak.template.gui; +package ru.joutak.template.atm.listener; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -9,6 +9,8 @@ import ru.joutak.template.atm.AtmService; import ru.joutak.template.bank.BankService; import ru.joutak.template.coin.CoinService; +import ru.joutak.template.gui.AtmMenu; +import ru.joutak.template.gui.AtmMenuHolder; import java.util.Optional; diff --git a/src/main/java/ru/joutak/template/atm/listener/AtmProtectionListener.java b/src/main/java/ru/joutak/template/atm/listener/AtmProtectionListener.java index 676a350..ac364dd 100644 --- a/src/main/java/ru/joutak/template/atm/listener/AtmProtectionListener.java +++ b/src/main/java/ru/joutak/template/atm/listener/AtmProtectionListener.java @@ -28,38 +28,7 @@ public AtmProtectionListener(AtmService atmService) { this.atmService = atmService; } - @EventHandler - public void onBlockBreak(BlockBreakEvent event) { - - Block block = event.getBlock(); - - Optional optionalAtm = - atmService.findByBlock(block.getLocation()); - if (optionalAtm.isEmpty()) { - return; - } - - Player player = event.getPlayer(); - - if (!player.hasPermission("joybank.atm.break")) { - - event.setCancelled(true); - - player.sendMessage( - "Вы не можете разрушить зарегистрированный банкомат." - ); - - return; - } - Atm atm = optionalAtm.get(); - - atmService.unregister(atm.id()); - - player.sendMessage( - "Банкомат удалён." - ); - } @EventHandler public void onEntityExplode(EntityExplodeEvent event) { event.blockList().removeIf(this::isAtmBlock); diff --git a/src/main/java/ru/joutak/template/command/JoyCommand.java b/src/main/java/ru/joutak/template/command/JoyCommand.java index fa0346f..eb0880f 100644 --- a/src/main/java/ru/joutak/template/command/JoyCommand.java +++ b/src/main/java/ru/joutak/template/command/JoyCommand.java @@ -3,7 +3,6 @@ import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; -import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import ru.joutak.template.bank.BankService; import ru.joutak.template.coin.CoinService; @@ -99,9 +98,15 @@ public boolean onCommand( return true; } - ItemStack coin = coinService.createCoin(amount); + boolean added = coinService.addCoins( + player.getInventory(), + amount + ); - player.getInventory().addItem(coin); + if (!added) { + player.sendMessage("В инвентаре недостаточно места."); + return true; + } player.sendMessage("Вы получили " + amount + " Джой."); diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 9de1ab0..ec3bbe3 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -4,7 +4,7 @@ api-version: ${MINECRAFT_VERSION} main: ru.joutak.template.JoyBankPlugin description: 'A template plugin' authors: ['russlan_1esse'] -contributors: ['technosimp'] +contributors: ['EnderDissa', 'Stunnerer', 's4dnex', 'technosimp'] website: ${WEBSITE} depend: [] From 1f8177e8ce926bc938062979366f170da773af7d Mon Sep 17 00:00:00 2001 From: gafru Date: Sat, 19 Sep 2026 16:29:48 +0300 Subject: [PATCH 8/8] feat: add auditable transaction model --- .../atm/listener/AtmMenuListener.java | 50 +- .../joutak/template/bank/AccountChange.java | 21 + .../ru/joutak/template/bank/BankService.java | 219 +++++++-- .../joutak/template/bank/InitiatorType.java | 7 + .../ru/joutak/template/bank/Transaction.java | 147 +++++- .../template/bank/TransactionInitiator.java | 7 + .../template/bank/TransactionSource.java | 7 + .../joutak/template/bank/TransactionType.java | 7 +- .../template/storage/AccountRepository.java | 10 +- .../storage/YamlAccountRepository.java | 429 ++++++++++++++---- 10 files changed, 735 insertions(+), 169 deletions(-) create mode 100644 src/main/java/ru/joutak/template/bank/AccountChange.java create mode 100644 src/main/java/ru/joutak/template/bank/InitiatorType.java create mode 100644 src/main/java/ru/joutak/template/bank/TransactionInitiator.java create mode 100644 src/main/java/ru/joutak/template/bank/TransactionSource.java diff --git a/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java b/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java index c367981..21a6990 100644 --- a/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java +++ b/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java @@ -11,6 +11,9 @@ import ru.joutak.template.coin.CoinService; import ru.joutak.template.gui.AtmMenu; import ru.joutak.template.gui.AtmMenuHolder; +import ru.joutak.template.bank.InitiatorType; +import ru.joutak.template.bank.TransactionInitiator; +import ru.joutak.template.bank.TransactionSource; import java.util.Optional; @@ -94,7 +97,6 @@ private void deposit( Atm atm, int amount ) { - int available = coinService.countCoins( player.getInventory() @@ -122,15 +124,19 @@ private void deposit( } try { - - bankService.deposit( + bankService.cashDeposit( player.getUniqueId(), - amount + amount, + TransactionSource.ATM, + new TransactionInitiator( + InitiatorType.PLAYER, + player.getUniqueId().toString() + ), + "Внесение наличных через банкомат" ); } catch (RuntimeException e) { - // Возвращаем физические деньги coinService.addCoins( player.getInventory(), amount @@ -139,7 +145,6 @@ private void deposit( player.sendMessage( "Ошибка сохранения. Монеты возвращены." ); - return; } @@ -177,7 +182,6 @@ private void withdraw( Atm atm, int amount ) { - long balance = bankService.getBalance( player.getUniqueId() @@ -215,28 +219,19 @@ private void withdraw( } try { - - boolean withdrawn = - bankService.withdraw( - player.getUniqueId(), - amount - ); - - if (!withdrawn) { - - coinService.removeCoins( - player.getInventory(), - amount - ); - - player.sendMessage( - "Не удалось снять деньги." - ); - - return; - } + bankService.cashWithdraw( + player.getUniqueId(), + amount, + TransactionSource.ATM, + new TransactionInitiator( + InitiatorType.PLAYER, + player.getUniqueId().toString() + ), + "Снятие наличных через банкомат" + ); } catch (RuntimeException e) { + coinService.removeCoins( player.getInventory(), amount @@ -245,7 +240,6 @@ private void withdraw( player.sendMessage( "Ошибка сохранения. Операция отменена." ); - return; } diff --git a/src/main/java/ru/joutak/template/bank/AccountChange.java b/src/main/java/ru/joutak/template/bank/AccountChange.java new file mode 100644 index 0000000..10a79f5 --- /dev/null +++ b/src/main/java/ru/joutak/template/bank/AccountChange.java @@ -0,0 +1,21 @@ +package ru.joutak.template.bank; + +import java.util.Objects; +import java.util.UUID; + +public record AccountChange( + UUID accountId, + long balanceBefore, + long balanceAfter +) { + + public AccountChange { + Objects.requireNonNull(accountId); + + if (balanceBefore < 0 || balanceAfter < 0) { + throw new IllegalArgumentException( + "Баланс не может быть отрицательным" + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/bank/BankService.java b/src/main/java/ru/joutak/template/bank/BankService.java index 5004a34..8f98e79 100644 --- a/src/main/java/ru/joutak/template/bank/BankService.java +++ b/src/main/java/ru/joutak/template/bank/BankService.java @@ -14,70 +14,203 @@ public BankService(AccountRepository accountRepository) { this.accountRepository = accountRepository; } - public long getBalance(UUID playerId) { - return accountRepository.getBalance(playerId); + public long getBalance(UUID accountId) { + return accountRepository.getBalance(accountId); } - public List getTransactions(UUID playerId) { - return accountRepository.getTransactions(playerId); + public List getTransactions(UUID accountId) { + return accountRepository.getTransactions(accountId); } - public void deposit(UUID playerId, long amount) { + public Transaction emit( + UUID accountId, + long amount, + TransactionSource source, + TransactionInitiator initiator, + String reason + ) { + validateAmount(amount); + long before = getBalance(accountId); + long after = Math.addExact(before, amount); + Transaction transaction = new Transaction( + UUID.randomUUID(), + TransactionType.EMISSION, + amount, + Instant.now(), + List.of( + new AccountChange( + accountId, + before, + after + ) + ), + source, + initiator, + reason, + null + ); + accountRepository.applyTransaction(transaction); + return transaction; + } - if (amount <= 0) { - throw new IllegalArgumentException( - "Сумма пополнения должна быть больше нуля" + public Transaction retire( + UUID accountId, + long amount, + TransactionSource source, + TransactionInitiator initiator, + String reason + ) { + validateAmount(amount); + long before = getBalance(accountId); + + if (before < amount) { + throw new IllegalStateException( + "Недостаточно средств" ); } - - long currentBalance = getBalance(playerId); - - long newBalance = Math.addExact( - currentBalance, - amount + long after = before - amount; + Transaction transaction = new Transaction( + UUID.randomUUID(), + TransactionType.RETIREMENT, + amount, + Instant.now(), + List.of( + new AccountChange( + accountId, + before, + after + ) + ), + source, + initiator, + reason, + null ); + accountRepository.applyTransaction(transaction); + return transaction; + } + public Transaction cashDeposit( + UUID accountId, + long amount, + TransactionSource source, + TransactionInitiator initiator, + String reason + ) { + validateAmount(amount); + long before = getBalance(accountId); + long after = Math.addExact(before, amount); Transaction transaction = new Transaction( - TransactionType.DEPOSIT, + UUID.randomUUID(), + TransactionType.CASH_DEPOSIT, amount, - Instant.now() + Instant.now(), + List.of( + new AccountChange( + accountId, + before, + after + ) + ), + source, + initiator, + reason, + null ); + accountRepository.applyTransaction(transaction); + return transaction; + } - accountRepository.applyTransaction( - playerId, - newBalance, - transaction + public Transaction cashWithdraw( + UUID accountId, + long amount, + TransactionSource source, + TransactionInitiator initiator, + String reason + ) { + validateAmount(amount); + long before = getBalance(accountId); + if (before < amount) { + throw new IllegalStateException( + "Недостаточно средств" + ); + } + long after = before - amount; + Transaction transaction = new Transaction( + UUID.randomUUID(), + TransactionType.CASH_WITHDRAWAL, + amount, + Instant.now(), + List.of( + new AccountChange( + accountId, + before, + after + ) + ), + source, + initiator, + reason, + null ); + accountRepository.applyTransaction(transaction); + return transaction; } - - public boolean withdraw(UUID playerId, long amount) { - - if (amount <= 0) { - return false; + public Transaction transfer( + UUID fromAccountId, + UUID toAccountId, + long amount, + TransactionSource source, + TransactionInitiator initiator, + String reason + ) { + validateAmount(amount); + if (fromAccountId.equals(toAccountId)) { + throw new IllegalArgumentException( + "Нельзя переводить самому себе" + ); } - - long currentBalance = getBalance(playerId); - - if (currentBalance < amount) { - return false; + long fromBefore = getBalance(fromAccountId); + if (fromBefore < amount) { + throw new IllegalStateException( + "Недостаточно средств" + ); } - - long newBalance = - currentBalance - amount; - + long toBefore = getBalance(toAccountId); + long fromAfter = fromBefore - amount; + long toAfter = Math.addExact(toBefore, amount); Transaction transaction = new Transaction( - TransactionType.WITHDRAW, + UUID.randomUUID(), + TransactionType.TRANSFER, amount, - Instant.now() - ); - - accountRepository.applyTransaction( - playerId, - newBalance, - transaction + Instant.now(), + List.of( + new AccountChange( + fromAccountId, + fromBefore, + fromAfter + ), + new AccountChange( + toAccountId, + toBefore, + toAfter + ) + ), + source, + initiator, + reason, + null ); + accountRepository.applyTransaction(transaction); + return transaction; + } - return true; + private void validateAmount(long amount) { + if (amount <= 0) { + throw new IllegalArgumentException( + "Сумма должна быть больше нуля" + ); + } } } \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/bank/InitiatorType.java b/src/main/java/ru/joutak/template/bank/InitiatorType.java new file mode 100644 index 0000000..4714503 --- /dev/null +++ b/src/main/java/ru/joutak/template/bank/InitiatorType.java @@ -0,0 +1,7 @@ +package ru.joutak.template.bank; + +public enum InitiatorType { + PLAYER, + CONSOLE, + SYSTEM +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/bank/Transaction.java b/src/main/java/ru/joutak/template/bank/Transaction.java index 8dc33e7..0ff7fba 100644 --- a/src/main/java/ru/joutak/template/bank/Transaction.java +++ b/src/main/java/ru/joutak/template/bank/Transaction.java @@ -1,10 +1,155 @@ package ru.joutak.template.bank; import java.time.Instant; +import java.util.List; +import java.util.Objects; +import java.util.UUID; public record Transaction( + UUID id, TransactionType type, long amount, - Instant timestamp + Instant timestamp, + List accountChanges, + TransactionSource source, + TransactionInitiator initiator, + String reason, + UUID relatedTransactionId ) { + + public Transaction { + Objects.requireNonNull(id); + Objects.requireNonNull(type); + Objects.requireNonNull(timestamp); + Objects.requireNonNull(accountChanges); + Objects.requireNonNull(source); + Objects.requireNonNull(initiator); + + if (amount <= 0) { + throw new IllegalArgumentException( + "Сумма операции должна быть больше нуля" + ); + } + + if (reason == null || reason.isBlank()) { + throw new IllegalArgumentException( + "Причина операции обязательна" + ); + } + + accountChanges = List.copyOf(accountChanges); + + validateChanges( + type, + amount, + accountChanges + ); + } + + private static void validateChanges( + TransactionType type, + long amount, + List changes + ) { + switch (type) { + + case EMISSION -> { + requireSingleChange(changes); + + if (delta(changes.getFirst()) != amount) { + throw new IllegalArgumentException( + "Эмиссия должна увеличить баланс на сумму операции" + ); + } + } + + case RETIREMENT -> { + requireSingleChange(changes); + + if (delta(changes.getFirst()) != -amount) { + throw new IllegalArgumentException( + "Изъятие должно уменьшить баланс на сумму операции" + ); + } + } + + case CASH_DEPOSIT -> { + requireSingleChange(changes); + + if (delta(changes.getFirst()) != amount) { + throw new IllegalArgumentException( + "Внесение наличных должно увеличить баланс" + ); + } + } + + case CASH_WITHDRAWAL -> { + requireSingleChange(changes); + + if (delta(changes.getFirst()) != -amount) { + throw new IllegalArgumentException( + "Снятие наличных должно уменьшить баланс" + ); + } + } + + case TRANSFER -> validateTransfer( + amount, + changes + ); + } + } + + private static void validateTransfer( + long amount, + List changes + ) { + if (changes.size() != 2) { + throw new IllegalArgumentException( + "Перевод должен затрагивать два счёта" + ); + } + + AccountChange first = changes.get(0); + AccountChange second = changes.get(1); + + if (first.accountId().equals(second.accountId())) { + throw new IllegalArgumentException( + "Перевод должен затрагивать разные счета" + ); + } + + long firstDelta = delta(first); + long secondDelta = delta(second); + + boolean valid = + firstDelta == -amount + && secondDelta == amount + || + firstDelta == amount + && secondDelta == -amount; + + if (!valid) { + throw new IllegalArgumentException( + "Перевод не должен изменять общую денежную массу" + ); + } + } + + private static void requireSingleChange( + List changes + ) { + if (changes.size() != 1) { + throw new IllegalArgumentException( + "Операция должна затрагивать один счёт" + ); + } + } + + private static long delta(AccountChange change) { + return Math.subtractExact( + change.balanceAfter(), + change.balanceBefore() + ); + } } \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/bank/TransactionInitiator.java b/src/main/java/ru/joutak/template/bank/TransactionInitiator.java new file mode 100644 index 0000000..d975daa --- /dev/null +++ b/src/main/java/ru/joutak/template/bank/TransactionInitiator.java @@ -0,0 +1,7 @@ +package ru.joutak.template.bank; + +public record TransactionInitiator( + InitiatorType type, + String id +) { +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/bank/TransactionSource.java b/src/main/java/ru/joutak/template/bank/TransactionSource.java new file mode 100644 index 0000000..0e8cb7d --- /dev/null +++ b/src/main/java/ru/joutak/template/bank/TransactionSource.java @@ -0,0 +1,7 @@ +package ru.joutak.template.bank; + +public enum TransactionSource { + ATM, + COMMAND, + INTERNAL_SYSTEM +} diff --git a/src/main/java/ru/joutak/template/bank/TransactionType.java b/src/main/java/ru/joutak/template/bank/TransactionType.java index 5803e2b..10dc5ff 100644 --- a/src/main/java/ru/joutak/template/bank/TransactionType.java +++ b/src/main/java/ru/joutak/template/bank/TransactionType.java @@ -1,6 +1,9 @@ package ru.joutak.template.bank; public enum TransactionType { - DEPOSIT, - WITHDRAW + EMISSION, + RETIREMENT, + TRANSFER, + CASH_DEPOSIT, + CASH_WITHDRAWAL } \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/storage/AccountRepository.java b/src/main/java/ru/joutak/template/storage/AccountRepository.java index f1d5197..32189a7 100644 --- a/src/main/java/ru/joutak/template/storage/AccountRepository.java +++ b/src/main/java/ru/joutak/template/storage/AccountRepository.java @@ -7,13 +7,9 @@ public interface AccountRepository { - long getBalance(UUID playerId); + long getBalance(UUID accountId); - void applyTransaction( - UUID playerId, - long newBalance, - Transaction transaction - ); + void applyTransaction(Transaction transaction); - List getTransactions(UUID playerId); + List getTransactions(UUID accountId); } \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java b/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java index e74a7c9..9188cbf 100644 --- a/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java +++ b/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java @@ -1,21 +1,20 @@ package ru.joutak.template.storage; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.java.JavaPlugin; -import ru.joutak.template.bank.Transaction; -import ru.joutak.template.bank.TransactionType; +import ru.joutak.template.bank.*; + +import java.io.File; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; - -import java.io.File; -import java.io.IOException; import java.time.Instant; import java.util.*; - public class YamlAccountRepository implements AccountRepository { private final JavaPlugin plugin; @@ -34,142 +33,394 @@ public YamlAccountRepository(JavaPlugin plugin) { "accounts.yml" ); - this.configuration = YamlConfiguration.loadConfiguration(file); + this.configuration = + YamlConfiguration.loadConfiguration(file); } @Override - public long getBalance(UUID playerId) { - String path = "players." + playerId + ".balance"; + public long getBalance(UUID accountId) { + return configuration.getLong( + balancePath(accountId), + 0L + ); + } + + @Override + public void applyTransaction(Transaction transaction) { + + if (configuration.contains( + transactionPath(transaction.id()) + )) { + throw new IllegalStateException( + "Транзакция уже существует: " + + transaction.id() + ); + } + + Map oldBalances = + new HashMap<>(); + + Set affectedAccounts = + new HashSet<>(); + + for (AccountChange change + : transaction.accountChanges()) { + + if (!affectedAccounts.add( + change.accountId() + )) { + throw new IllegalArgumentException( + "Счёт указан в транзакции несколько раз" + ); + } + + if (change.balanceAfter() < 0) { + throw new IllegalArgumentException( + "Баланс не может быть отрицательным" + ); + } + + String path = + balancePath(change.accountId()); + + long currentBalance = + configuration.getLong(path, 0L); + + if (currentBalance + != change.balanceBefore()) { + throw new IllegalStateException( + "Баланс счёта изменился" + ); + } + + oldBalances.put( + change.accountId(), + configuration.get(path) + ); + } - return configuration.getLong(path, 0L); + String transactionPath = + transactionPath(transaction.id()); + + try { + + for (AccountChange change + : transaction.accountChanges()) { + + configuration.set( + balancePath(change.accountId()), + change.balanceAfter() + ); + } + + writeTransaction( + transactionPath, + transaction + ); + + save(); + + } catch (RuntimeException e) { + + for (Map.Entry entry + : oldBalances.entrySet()) { + + configuration.set( + balancePath(entry.getKey()), + entry.getValue() + ); + } + + configuration.set( + transactionPath, + null + ); + + throw e; + } } @Override - public void applyTransaction( - UUID playerId, - long newBalance, - Transaction transaction + public List getTransactions( + UUID accountId ) { - if (newBalance < 0) { - throw new IllegalArgumentException( - "Баланс не может быть отрицательным" - ); + ConfigurationSection section = + configuration.getConfigurationSection( + "transactions" + ); + + if (section == null) { + return List.of(); } - String playerPath = - "players." + playerId; - String balancePath = - playerPath + ".balance"; - String transactionsPath = - playerPath + ".transactions"; - Object oldBalance = - configuration.get(balancePath); - - List> oldTransactions = - new ArrayList<>( - configuration.getMapList( - transactionsPath - ) + List result = + new ArrayList<>(); + + for (String transactionId + : section.getKeys(false)) { + + try { + + Transaction transaction = + readTransaction(transactionId); + + boolean affectsAccount = + transaction.accountChanges() + .stream() + .anyMatch(change -> + change.accountId() + .equals(accountId) + ); + + if (affectsAccount) { + result.add(transaction); + } + + } catch (Exception e) { + + plugin.getLogger().warning( + "Не удалось прочитать транзакцию " + + transactionId ); - List> newTransactions = - new ArrayList<>(oldTransactions); + } + } - Map data = - new HashMap<>(); - data.put( - "type", + result.sort( + Comparator.comparing( + Transaction::timestamp + ) + ); + + return result; + } + + private void writeTransaction( + String path, + Transaction transaction + ) { + + configuration.set( + path + ".type", transaction.type().name() ); - data.put( - "amount", + + configuration.set( + path + ".amount", transaction.amount() ); - data.put( - "timestamp", + + configuration.set( + path + ".timestamp", transaction.timestamp().toString() ); - newTransactions.add(data); - try { + configuration.set( + path + ".source", + transaction.source().name() + ); - configuration.set( - balancePath, - newBalance - ); + configuration.set( + path + ".initiator.type", + transaction.initiator() + .type() + .name() + ); + + configuration.set( + path + ".initiator.id", + transaction.initiator().id() + ); + + configuration.set( + path + ".reason", + transaction.reason() + ); + if (transaction.relatedTransactionId() != null) { configuration.set( - transactionsPath, - newTransactions + path + ".relatedTransactionId", + transaction.relatedTransactionId() + .toString() ); + } - save(); + List> changes = + new ArrayList<>(); - } catch (RuntimeException e) { + for (AccountChange change + : transaction.accountChanges()) { - configuration.set( - balancePath, - oldBalance + Map data = + new LinkedHashMap<>(); + + data.put( + "accountId", + change.accountId().toString() ); - configuration.set( - transactionsPath, - oldTransactions + data.put( + "balanceBefore", + change.balanceBefore() ); - throw e; + data.put( + "balanceAfter", + change.balanceAfter() + ); + + changes.add(data); } + + configuration.set( + path + ".accountChanges", + changes + ); } - @Override - public List getTransactions(UUID playerId) { - String path = "players." + playerId + ".transactions"; + private Transaction readTransaction( + String transactionId + ) { - List> storedTransactions = - configuration.getMapList(path); + String path = + transactionPath( + UUID.fromString(transactionId) + ); - List result = new ArrayList<>(); + UUID id = + UUID.fromString(transactionId); - for (Map data : storedTransactions) { - try { - TransactionType type = TransactionType.valueOf( - String.valueOf(data.get("type")) + TransactionType type = + TransactionType.valueOf( + Objects.requireNonNull( + configuration.getString( + path + ".type" + ) + ) ); - long amount = Long.parseLong( - String.valueOf(data.get("amount")) + long amount = + configuration.getLong( + path + ".amount" ); - Instant timestamp = Instant.parse( - String.valueOf(data.get("timestamp")) + Instant timestamp = + Instant.parse( + Objects.requireNonNull( + configuration.getString( + path + ".timestamp" + ) + ) ); - result.add( - new Transaction( - type, - amount, - timestamp + TransactionSource source = + TransactionSource.valueOf( + Objects.requireNonNull( + configuration.getString( + path + ".source" + ) ) ); - } catch (Exception e) { - plugin.getLogger().warning( - "Не удалось прочитать операцию игрока " - + playerId + InitiatorType initiatorType = + InitiatorType.valueOf( + Objects.requireNonNull( + configuration.getString( + path + ".initiator.type" + ) + ) ); - } + + String initiatorId = + Objects.requireNonNull( + configuration.getString( + path + ".initiator.id" + ) + ); + + String reason = + Objects.requireNonNull( + configuration.getString( + path + ".reason" + ) + ); + + String relatedId = + configuration.getString( + path + ".relatedTransactionId" + ); + + UUID relatedTransactionId = + relatedId == null + ? null + : UUID.fromString(relatedId); + + List changes = + new ArrayList<>(); + + for (Map data + : configuration.getMapList( + path + ".accountChanges" + )) { + + changes.add( + new AccountChange( + UUID.fromString( + String.valueOf( + data.get("accountId") + ) + ), + Long.parseLong( + String.valueOf( + data.get("balanceBefore") + ) + ), + Long.parseLong( + String.valueOf( + data.get("balanceAfter") + ) + ) + ) + ); } - return result; + return new Transaction( + id, + type, + amount, + timestamp, + changes, + source, + new TransactionInitiator( + initiatorType, + initiatorId + ), + reason, + relatedTransactionId + ); + } + + private String balancePath(UUID accountId) { + return "players." + + accountId + + ".balance"; + } + + private String transactionPath( + UUID transactionId + ) { + return "transactions." + + transactionId; } private void save() { Path target = file.toPath(); - Path temp = target.resolveSibling( - file.getName() + ".tmp" - ); + Path temp = + target.resolveSibling( + file.getName() + ".tmp" + ); try { @@ -191,7 +442,9 @@ private void save() { StandardCopyOption.REPLACE_EXISTING ); - } catch (AtomicMoveNotSupportedException e) { + } catch ( + AtomicMoveNotSupportedException e + ) { Files.move( temp,