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/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 0000000..6173eed Binary files /dev/null and b/resourcepack/JoyBankPack/assets/minecraft/textures/item/joy_coin.png differ 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/EmptyPlugin.java b/src/main/java/ru/joutak/template/EmptyPlugin.java deleted file mode 100644 index d93430e..0000000 --- a/src/main/java/ru/joutak/template/EmptyPlugin.java +++ /dev/null @@ -1,23 +0,0 @@ -package ru.joutak.template; - -import lombok.Getter; -import org.bukkit.plugin.java.JavaPlugin; - -public final class EmptyPlugin extends JavaPlugin { - @Getter - private static EmptyPlugin instance; - - @Override - public void onEnable() { - instance = this; - - getLogger().info( - String.format("Плагин %s версии %s включен!", getPluginMeta().getName(), getPluginMeta().getVersion()) - ); - } - - @Override - public void onDisable() { - - } -} diff --git a/src/main/java/ru/joutak/template/JoyBankPlugin.java b/src/main/java/ru/joutak/template/JoyBankPlugin.java new file mode 100644 index 0000000..d44a5ef --- /dev/null +++ b/src/main/java/ru/joutak/template/JoyBankPlugin.java @@ -0,0 +1,90 @@ +package ru.joutak.template; + +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.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.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() { + instance = this; + coinService = new CoinService(this); + 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) + ); + 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 + public void onDisable() { + + } +} 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..df037ec --- /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() + ); + atmRepository.save(atm); + atms.put(atm.id(), 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) { + + atmRepository.delete(atmId); + atms.remove(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..21a6990 --- /dev/null +++ b/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java @@ -0,0 +1,282 @@ +package ru.joutak.template.atm.listener; + +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 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; + +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.cashDeposit( + player.getUniqueId(), + amount, + TransactionSource.ATM, + new TransactionInitiator( + InitiatorType.PLAYER, + player.getUniqueId().toString() + ), + "Внесение наличных через банкомат" + ); + + } 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 { + bankService.cashWithdraw( + player.getUniqueId(), + amount, + TransactionSource.ATM, + new TransactionInitiator( + InitiatorType.PLAYER, + player.getUniqueId().toString() + ), + "Снятие наличных через банкомат" + ); + + } 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..ac364dd --- /dev/null +++ b/src/main/java/ru/joutak/template/atm/listener/AtmProtectionListener.java @@ -0,0 +1,134 @@ +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 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/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 new file mode 100644 index 0000000..8f98e79 --- /dev/null +++ b/src/main/java/ru/joutak/template/bank/BankService.java @@ -0,0 +1,216 @@ +package ru.joutak.template.bank; + +import ru.joutak.template.storage.AccountRepository; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +public class BankService { + + private final AccountRepository accountRepository; + + public BankService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + public long getBalance(UUID accountId) { + return accountRepository.getBalance(accountId); + } + + public List getTransactions(UUID accountId) { + return accountRepository.getTransactions(accountId); + } + + 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; + } + + 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 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( + UUID.randomUUID(), + TransactionType.CASH_DEPOSIT, + amount, + Instant.now(), + List.of( + new AccountChange( + accountId, + before, + after + ) + ), + source, + initiator, + reason, + null + ); + accountRepository.applyTransaction(transaction); + return 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 Transaction transfer( + UUID fromAccountId, + UUID toAccountId, + long amount, + TransactionSource source, + TransactionInitiator initiator, + String reason + ) { + validateAmount(amount); + if (fromAccountId.equals(toAccountId)) { + throw new IllegalArgumentException( + "Нельзя переводить самому себе" + ); + } + long fromBefore = getBalance(fromAccountId); + if (fromBefore < amount) { + throw new IllegalStateException( + "Недостаточно средств" + ); + } + long toBefore = getBalance(toAccountId); + long fromAfter = fromBefore - amount; + long toAfter = Math.addExact(toBefore, amount); + Transaction transaction = new Transaction( + UUID.randomUUID(), + TransactionType.TRANSFER, + amount, + Instant.now(), + List.of( + new AccountChange( + fromAccountId, + fromBefore, + fromAfter + ), + new AccountChange( + toAccountId, + toBefore, + toAfter + ) + ), + source, + initiator, + reason, + null + ); + accountRepository.applyTransaction(transaction); + return transaction; + } + + 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 new file mode 100644 index 0000000..0ff7fba --- /dev/null +++ b/src/main/java/ru/joutak/template/bank/Transaction.java @@ -0,0 +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, + 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 new file mode 100644 index 0000000..10dc5ff --- /dev/null +++ b/src/main/java/ru/joutak/template/bank/TransactionType.java @@ -0,0 +1,9 @@ +package ru.joutak.template.bank; + +public enum TransactionType { + EMISSION, + RETIREMENT, + TRANSFER, + CASH_DEPOSIT, + CASH_WITHDRAWAL +} \ No newline at end of file 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..9b084fd --- /dev/null +++ b/src/main/java/ru/joutak/template/coin/CoinService.java @@ -0,0 +1,180 @@ +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; +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); + + ItemMeta meta = coin.getItemMeta(); + + meta.displayName( + Component.text( + "Джой", + NamedTextColor.GOLD + ) + ); + + meta.getPersistentDataContainer().set( + coinKey, + PersistentDataType.BYTE, + (byte) 1 + ); + + CustomModelDataComponent modelData = + meta.getCustomModelDataComponent(); + + modelData.setFloats( + List.of(JOY_MODEL_DATA) + ); + + meta.setCustomModelDataComponent(modelData); + + 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 + ); + } + + 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 new file mode 100644 index 0000000..eb0880f --- /dev/null +++ b/src/main/java/ru/joutak/template/command/JoyCommand.java @@ -0,0 +1,118 @@ +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; +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 { + + private final CoinService coinService; + private final BankService bankService; + + public JoyCommand(CoinService coinService, BankService bankService) { + this.coinService = coinService; + this.bankService = bankService; + } + + @Override + public boolean onCommand( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args + ){ + if(!(sender instanceof Player player)){ + sender.sendMessage("Эту команду может использовать только игрок"); + return true; + } + if(args.length == 0){ + 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( + player.getUniqueId() + ); + + player.sendMessage( + "Ваш баланс: " + balance + " Джой." + ); + + 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; + } + + boolean added = coinService.addCoins( + player.getInventory(), + amount + ); + + if (!added) { + player.sendMessage("В инвентаре недостаточно места."); + return true; + } + + player.sendMessage("Вы получили " + amount + " Джой."); + + return true; + } + player.sendMessage("Неизвестная команда"); + return true; + } +} 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 new file mode 100644 index 0000000..32189a7 --- /dev/null +++ b/src/main/java/ru/joutak/template/storage/AccountRepository.java @@ -0,0 +1,15 @@ +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 accountId); + + void applyTransaction(Transaction transaction); + + List getTransactions(UUID accountId); +} \ 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 new file mode 100644 index 0000000..9188cbf --- /dev/null +++ b/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java @@ -0,0 +1,469 @@ +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.*; + +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.time.Instant; +import java.util.*; + +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 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) + ); + } + + 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 List getTransactions( + UUID accountId + ) { + + ConfigurationSection section = + configuration.getConfigurationSection( + "transactions" + ); + + if (section == null) { + return List.of(); + } + + 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 + ); + } + } + + result.sort( + Comparator.comparing( + Transaction::timestamp + ) + ); + + return result; + } + + private void writeTransaction( + String path, + Transaction transaction + ) { + + configuration.set( + path + ".type", + transaction.type().name() + ); + + configuration.set( + path + ".amount", + transaction.amount() + ); + + configuration.set( + path + ".timestamp", + transaction.timestamp().toString() + ); + + configuration.set( + path + ".source", + transaction.source().name() + ); + + 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( + path + ".relatedTransactionId", + transaction.relatedTransactionId() + .toString() + ); + } + + List> changes = + new ArrayList<>(); + + for (AccountChange change + : transaction.accountChanges()) { + + Map data = + new LinkedHashMap<>(); + + data.put( + "accountId", + change.accountId().toString() + ); + + data.put( + "balanceBefore", + change.balanceBefore() + ); + + data.put( + "balanceAfter", + change.balanceAfter() + ); + + changes.add(data); + } + + configuration.set( + path + ".accountChanges", + changes + ); + } + + private Transaction readTransaction( + String transactionId + ) { + + String path = + transactionPath( + UUID.fromString(transactionId) + ); + + UUID id = + UUID.fromString(transactionId); + + TransactionType type = + TransactionType.valueOf( + Objects.requireNonNull( + configuration.getString( + path + ".type" + ) + ) + ); + + long amount = + configuration.getLong( + path + ".amount" + ); + + Instant timestamp = + Instant.parse( + Objects.requireNonNull( + configuration.getString( + path + ".timestamp" + ) + ) + ); + + TransactionSource source = + TransactionSource.valueOf( + Objects.requireNonNull( + configuration.getString( + path + ".source" + ) + ) + ); + + 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 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" + ); + + try { + + 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 + ); + } + } +} \ No newline at end of file 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 4428874..ec3bbe3 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,12 +1,27 @@ 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'] -contributors: ['technosimp'] +authors: ['russlan_1esse'] +contributors: ['EnderDissa', 'Stunnerer', 's4dnex', '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 + +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