commit 94404156d97c9f588c3cc38ef1a48e4bb68449d9 Author: gpatruno Date: Tue May 19 15:35:55 2026 +0200 first diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7496ec --- /dev/null +++ b/README.md @@ -0,0 +1,393 @@ +# SpaceShipProject + +Plugin Paper / Spigot **1.21+** qui permet à chaque joueur d'avoir un (ou plusieurs) spaceship personnel. +Le vaisseau est chargé directement dans le ciel de la map actuelle ; il sert d'île volante privée que le +joueur peut décorer, fortifier puis ranger dans une "télécommande" en forme de **Recovery Compass**. + +> Inspiré du fonctionnement des îles de Skyllia, mais sans monde dédié : tout reste sur la map du joueur, +> en plein ciel. + +--- + +## Sommaire + +- [Fonctionnalités](#fonctionnalités) +- [Pré-requis](#pré-requis) +- [Installation](#installation) +- [Compilation](#compilation) +- [Configuration](#configuration) +- [Utilisation](#utilisation) + - [Télécommande (Recovery Compass)](#télécommande-recovery-compass) + - [GUI principal](#gui-principal) + - [Saut Spatial](#saut-spatial) +- [Commandes joueur (`/spaceship`, `/ss`, `/ship`)](#commandes-joueur-spaceship-ss-ship) +- [Commandes admin (`/spaceshipadmin`, `/ssa`)](#commandes-admin-spaceshipadmin-ssa) +- [Économie (Vault / EssentialsEconomy)](#économie-vault--essentialseconomy) +- [Système de niveaux et de tailles](#système-de-niveaux-et-de-tailles) +- [Stockage et base de données](#stockage-et-base-de-données) +- [Modèles de spaceship](#modèles-de-spaceship) +- [Permissions](#permissions) +- [FAQ](#faq) + +--- + +## Fonctionnalités + +- Spaceships personnels matérialisés en plein ciel **sur la même map** que le joueur. +- Télécommande **Recovery Compass** par spaceship (clic gauche / droit / shift+droit). +- **Système de niveaux** : chaque level-up agrandit physiquement le vaisseau (x/z en alternance, + +1 y tous les 4 niveaux). Les nouvelles limites sont indiquées par des **Verdant Froglight** + aux 8 coins au premier chargement après l'upgrade. +- **Saut spatial** : déplacement instantané du spaceship le long d'un axe, avec un coût en money + proportionnel à la distance. +- **Sauvegarde complète** : chaque dépose stocke les blocs **et le contenu des coffres / barils / + fournaises / shulker boxes / etc.** dans la base de données. +- **Protection** : seul le propriétaire (ou un admin) peut casser/poser des blocs dans un + spaceship chargé. Les autres joueurs sont les bienvenus comme passagers. +- **Vault / EssentialsEconomy** : tous les coûts (invocation, upgrade, saut spatial) sont payés + via Vault. Aucun système monétaire interne — le solde du joueur est la source unique. +- **Auto-déchargement à la déconnexion** : tous les spaceships du joueur sont sauvegardés et + retirés du monde quand il se déconnecte. +- **Respawn sur le vaisseau** : si le joueur meurt sur un de ses spaceships chargés, il y respawn. +- **Modèles éditables** : un admin peut définir un plan par défaut (chargé en jeu, édité comme + des blocs normaux puis sauvegardé) qui sert de base à chaque nouveau spaceship. +- **GUI** complet pour gérer le ship : charger/décharger, téléport, set spawn, renommer, upgrade, + supprimer, saut spatial. + +--- + +## Pré-requis + +- **Java 21+** +- **Paper / Purpur / Folia (compat partielle) 1.21.x** +- **Vault** (obligatoire pour l'économie) +- Un fournisseur d'économie compatible Vault (recommandé : **EssentialsEconomy** via EssentialsX) + +> Sans Vault, le plugin charge mais désactive toutes les actions payantes. + +--- + +## Installation + +1. Copier `SpaceShipProject-1.0-SNAPSHOT.jar` dans le dossier `plugins/` du serveur. +2. S'assurer que `Vault` (+ un provider, ex. EssentialsX/EssentialsEconomy) est présent. +3. Démarrer le serveur. Le dossier `plugins/SpaceShipProject/` sera créé avec `config.yml`, + `models/` et `spaceships.db`. + +--- + +## Compilation + +Depuis la racine du dépôt `mcplugin/` : + +```bash +./build.sh SpaceShipProject +``` + +Le JAR final est dans `SpaceShipProject/target/SpaceShipProject-1.0-SNAPSHOT.jar`. + +Pré-requis : Maven 3.6+, JDK 21. Le script gère automatiquement `JAVA_HOME` sur Arch Linux. + +--- + +## Configuration + +Fichier `plugins/SpaceShipProject/config.yml` : + +```yaml +max-ships-per-player: 5 + +default-size: # taille initiale (level 1) + x: 4 + y: 4 + z: 6 + +max-size: # plafond atteignable + x: 16 + y: 8 + z: 16 + +costs: + summon: 100 # cout pour charger un spaceship + unload: 0 # cout pour decharger + upgrade-base: 1000 # cout d'amelioration = upgrade-base * niveau actuel + space-jump-per-block: 1 + +space-jump: + blocks-per-level: 100 # distance max = blocks-per-level * niveau + min-blocks: 1 + max-blocks-hard-cap: 5000 + +summon-offset-y: 30 # hauteur du ship au-dessus du joueur a l'invocation + +default-platform-material: SMOOTH_QUARTZ +default-edge-material: QUARTZ_PILLAR +default-glass-material: WHITE_STAINED_GLASS + +default-ship-name: "Spaceship #%n%" + +safety: + check-collision: true + min-y-above-player: 10 + +messages: + prefix: "&8[&bSpaceShip&8] &7" + # ... (voir le fichier complet) +``` + +Toutes les clés peuvent être modifiées à chaud via : + +``` +/ssa setconfig +/ssa getconfig +/ssa reload +``` + +--- + +## Utilisation + +### Télécommande (Recovery Compass) + +Chaque spaceship a sa propre télécommande. L'item est un **Recovery Compass** marqué par PDC. +La lore montre l'ID, le nom, le niveau et la taille du ship. + +| Action sur la télécommande | Effet | +|---|---| +| **Clic gauche** (au sol, ship non chargé) | Invoque le spaceship au-dessus du joueur (coût : `costs.summon`) | +| **Clic gauche** (au sol, ship chargé) | Décharge le ship et sauvegarde toutes les modifications | +| **Clic gauche** (sur le ship) | Refuse (utiliser clic droit pour redescendre d'abord) | +| **Clic droit** (au sol, ship non chargé) | Invoque puis téléporte directement sur le ship | +| **Clic droit** (au sol, ship chargé) | Téléporte sur le spawn du ship | +| **Clic droit** (sur le ship) | Redescend au sol, à la position initiale | +| **Shift + clic droit** | Ouvre le GUI de paramètres | + +### GUI principal + +Ouvert via **shift + clic droit** sur la télécommande. 36 slots : + +``` +. . . . I . . . . I = info (niveau, taille, statut, solde Vault) +. . . . . . . . . +. T E . S . . . . T = toggle charge/decharge + E = teleport up/down + S = set spawn (sur le ship) + J = Saut Spatial (sur le ship) + U = Ameliorer (level-up) +. . . . R . . . D R = renommer (par chat) + D = supprimer (shift-clic pour confirmer) +``` + +### Saut Spatial + +Disponible **uniquement quand le joueur est sur son spaceship**. + +1. Ouvrir le GUI principal, cliquer sur **Saut Spatial**. +2. Choisir un axe : `X-`, `X+`, `Y-`, `Y+`, `Z-`, `Z+`. +3. Ajuster la distance avec les boutons (`-50 -10 -1 / +1 +10 +50`, shift = ×10). +4. Confirmer. + +- Distance max = `space-jump.blocks-per-level × niveau` (par défaut 100 / niveau). +- Coût = `costs.space-jump-per-block × distance` (par défaut 1 money / bloc). +- Le ship est capturé, déchargé à l'ancienne position, rechargé à la nouvelle, et le joueur est + téléporté sur son spawn. + +--- + +## Commandes joueur (`/spaceship`, `/ss`, `/ship`) + +| Commande | Description | Permission | +|---|---|---| +| `/ss help` | Aide | `spaceship.use` | +| `/ss create` | Crée un nouveau spaceship + donne sa télécommande | `spaceship.create` | +| `/ss give ` | Redonne la télécommande perdue à un joueur (no-op si déjà présente, dédoublonne) | `spaceship.give` | +| `/ss remote ` | Redonne sa propre télécommande | `spaceship.give` | +| `/ss list [joueur]` | Liste compacte des ships | `spaceship.use` | +| `/ss info [joueur]` | Infos détaillées (niveau, taille actuelle/suivante, saut max, etc.) | `spaceship.use` / `spaceship.info.other` | +| `/ss delete ` | Supprime un ship et retire sa télécommande de l'inventaire | `spaceship.delete` | +| `/ss reload` | Recharge la config | `spaceship.reload` | + +--- + +## Commandes admin (`/spaceshipadmin`, `/ssa`) + +| Commande | Description | +|---|---| +| `/ssa reload` | Recharge config + modèles | +| `/ssa list [joueur]` | Liste tous les ships, ou ceux d'un joueur, ou les ships actuellement chargés | +| `/ssa delete ` | Supprime n'importe quel ship | +| `/ssa addmoney ` | Dépose de la money via Vault sur le solde du joueur | +| `/ssa setlevel ` | Force le niveau d'un ship (la taille s'ajuste au prochain chargement) | +| `/ssa setname ` | Renomme un ship | +| `/ssa setconfig ` | Modifie une clé du `config.yml` (auto-reload) | +| `/ssa getconfig ` | Lit une clé | +| `/ssa loadmodel [nom]` | Matérialise un modèle dans le ciel pour l'éditer (par défaut `default`) | +| `/ssa savemodel` | Capture la session en cours et l'écrit dans `models/.json` | +| `/ssa cancelmodel` | Annule la session et nettoie la zone | + +Toutes ces commandes requièrent `spaceship.admin`. + +--- + +## Économie (Vault / EssentialsEconomy) + +Le plugin n'a **aucune** monnaie interne. Tout passe par **Vault** : + +- Charger un spaceship → débite `costs.summon` au propriétaire. +- Améliorer un spaceship → débite `costs.upgrade-base × niveau actuel`. +- Saut spatial → débite `distance × costs.space-jump-per-block`. + +La permission `spaceship.bypass.cost` permet de tester sans payer (par défaut OP). + +Si Vault est absent / sans provider, le plugin log un warning et toutes les actions payantes +sont bloquées (`Vault indisponible`). + +--- + +## Système de niveaux et de tailles + +Au niveau 1, la taille est celle de `default-size` (par défaut **4 × 4 × 6**, x/y/z). +Chaque niveau gagné applique : + +- **+1 sur X** (level-ups impairs : L1→L2, L3→L4, ...) +- **+1 sur Z** (level-ups pairs : L2→L3, L4→L5, ...) +- **+1 sur Y** tous les **4 level-ups** (L4→L5, L8→L9, ...) + +…jusqu'au plafond `max-size` (par défaut **16 × 8 × 16**, atteint vers le niveau 27). + +| Level | Taille (x,y,z) | +|------|----------------| +| 1 | 4 × 4 × 6 | +| 2 | 5 × 4 × 6 | +| 3 | 5 × 4 × 7 | +| 4 | 6 × 4 × 7 | +| 5 | 6 × 5 × 8 | +| ... | ... | +| 27+ | 16 × 8 × 16 | + +Quand un ship est chargé pour la première fois après un upgrade, les nouvelles limites sont +matérialisées par des blocs **Verdant Froglight** aux 8 coins. Le joueur peut ensuite les +casser et décorer ses nouveaux murs librement — au prochain chargement, ces froglights ne +réapparaissent pas (sauf nouvel upgrade). + +--- + +## Stockage et base de données + +SQLite local : `plugins/SpaceShipProject/spaceships.db`. + +Trois tables : + +```sql +players( + uuid TEXT PRIMARY KEY, name TEXT, settings TEXT, + created_at TIMESTAMP, updated_at TIMESTAMP +) + +spaceships( + id INTEGER PK, + owner_uuid TEXT, + name TEXT, + size_x, size_y, size_z INTEGER, + spawn_x, spawn_y, spawn_z INTEGER, + schematic TEXT, -- JSON (palette + RLE + containers) + settings TEXT, -- JSON libre + level INTEGER DEFAULT 1, + money INTEGER DEFAULT 0, -- obsolète (Vault), conservé pour migration + last_loaded_level INTEGER, -- pour le marquage Verdant Froglight + created_at TIMESTAMP, updated_at TIMESTAMP +) + +spaceships_loaded( + spaceship_id INTEGER PK, + world TEXT, + origin_x, origin_y, origin_z INTEGER, + loaded_at TIMESTAMP +) +``` + +Le champ `schematic` est un JSON : + +```json +{ + "size": [sx, sy, sz], + "spawn": [ox, oy, oz], + "palette": ["minecraft:air", "minecraft:smooth_quartz", ...], + "blocks": [[paletteIdx, runLength], ...], + "containers": [ + {"x":1,"y":2,"z":3,"items":[{"slot":0,"data":"base64..."}, ...]} + ] +} +``` + +Les `items` sont sérialisés via `ItemStack#serializeAsBytes()` : enchantements, NBT, custom +data, etc. sont entièrement préservés. + +--- + +## Modèles de spaceship + +Stockés dans `plugins/SpaceShipProject/models/.json` (format identique au champ +`schematic`). + +Workflow admin : + +```text +/ssa loadmodel # place le modele actuel devant l'admin (sinon genere une plateforme) +# ... edite librement les blocs (admin = bypass de protection) ... +/ssa savemodel # capture la zone -> models/default.json, nettoie le ciel +``` + +Le prochain `/ss create` utilisera ce nouveau plan comme base. On peut créer plusieurs modèles +nommés (`/ssa loadmodel arena`, etc.). + +Si `models/default.json` n'existe pas, le plugin génère une plateforme procédurale (sol + +piliers aux coins, vitres latérales). + +--- + +## Permissions + +```yaml +spaceship.use: true # base : utiliser sa propre boussole +spaceship.create: true # /ss create +spaceship.give: true # /ss give (sa propre télécommande) +spaceship.info.other: op # /ss info +spaceship.delete: true # supprimer un de ses ships +spaceship.reload: op +spaceship.admin: op # bypass de propriété + accès à /ssa +spaceship.bypass.cost: op # ne paie pas summon / upgrade / saut +``` + +--- + +## FAQ + +**Que se passe-t-il si je me déconnecte alors que mon ship est chargé ?** +Le plugin capture l'état complet (y compris coffres) et décharge le ship. Vous êtes téléporté +au sol au préalable pour ne pas réapparaître en plein vol à la reconnexion. + +**Et si je meurs sur mon ship ?** +Vous respawnez directement sur le spawn du ship si celui-ci est encore chargé. Sinon, respawn +normal. + +**Le ship peut-il chevaucher des constructions du joueur ?** +Non : le plugin vérifie qu'aucun bloc solide n'occupe la zone avant l'invocation +(`safety.check-collision`). Si oui, l'invocation échoue avec un message d'erreur. + +**Puis-je inviter d'autres joueurs sur mon ship ?** +Oui — ils peuvent y monter (téléport-relay si tu leur indiques la position). Cependant, ils ne +peuvent **pas modifier les blocs** : seuls le propriétaire et les admins peuvent casser/poser. + +**Comment réinitialiser un ship à zéro ?** +Le supprimer (`/ss delete `) puis en recréer un (`/ss create`). La télécommande de l'ancien +est retirée automatiquement. + +**Que se passe-t-il quand le ship atteint la taille maximale ?** +Le bouton "Améliorer" devient inactif. Vous gardez votre ship à taille max. + +--- + +## License + +Plugin développé pour un usage privé. Pas de licence formelle, à utiliser librement. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8e6988a --- /dev/null +++ b/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + com.spaceshipproject + SpaceShipProject + 1.0-SNAPSHOT + SpaceShipProject + + + 21 + UTF-8 + + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + jitpack.io + https://jitpack.io + + + + + + io.papermc.paper + paper-api + 1.21.1-R0.1-SNAPSHOT + provided + + + org.xerial + sqlite-jdbc + 3.44.1.0 + + + com.github.MilkBowl + VaultAPI + 1.7.1 + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + false + + + org.sqlite + com.spaceshipproject.libs.sqlite + + + + + + + + + diff --git a/src/main/java/com/spaceshipproject/BlockProtectionListener.java b/src/main/java/com/spaceshipproject/BlockProtectionListener.java new file mode 100644 index 0000000..f95fd77 --- /dev/null +++ b/src/main/java/com/spaceshipproject/BlockProtectionListener.java @@ -0,0 +1,78 @@ +package com.spaceshipproject; + +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.player.PlayerBucketEmptyEvent; +import org.bukkit.event.player.PlayerBucketFillEvent; +import org.bukkit.event.player.PlayerInteractEvent; + +/** + * Empeche tout joueur autre que le proprietaire (ou un admin) de modifier les blocs + * a l'interieur de la bounding box d'un spaceship charge. + */ +public class BlockProtectionListener implements Listener { + + private final SpaceShipProject plugin; + private final SpaceshipManager manager; + private final ConfigManager cfg; + + public BlockProtectionListener(SpaceShipProject plugin) { + this.plugin = plugin; + this.manager = plugin.getSpaceshipManager(); + this.cfg = plugin.getConfigManager(); + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onBreak(BlockBreakEvent event) { + if (denyIfNotOwner(event.getPlayer(), event.getBlock().getLocation())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPlace(BlockPlaceEvent event) { + if (denyIfNotOwner(event.getPlayer(), event.getBlock().getLocation())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onBucketEmpty(PlayerBucketEmptyEvent event) { + if (denyIfNotOwner(event.getPlayer(), event.getBlockClicked().getRelative(event.getBlockFace()).getLocation())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onBucketFill(PlayerBucketFillEvent event) { + if (denyIfNotOwner(event.getPlayer(), event.getBlockClicked().getLocation())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onInteract(PlayerInteractEvent event) { + Block b = event.getClickedBlock(); + if (b == null) return; + if (denyIfNotOwner(event.getPlayer(), b.getLocation())) { + event.setCancelled(true); + } + } + + private boolean denyIfNotOwner(Player player, Location loc) { + if (loc.getWorld() == null) return false; + SpaceshipManager.LoadedShipOwnerInfo info = manager.findShipOwnerAt( + loc.getWorld(), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + if (info == null) return false; + if (info.ownerUuid().equals(player.getUniqueId())) return false; + if (player.hasPermission("spaceship.admin")) return false; + player.sendMessage(cfg.msgNotOwner()); + return true; + } +} diff --git a/src/main/java/com/spaceshipproject/ConfigManager.java b/src/main/java/com/spaceshipproject/ConfigManager.java new file mode 100644 index 0000000..e63b92b --- /dev/null +++ b/src/main/java/com/spaceshipproject/ConfigManager.java @@ -0,0 +1,96 @@ +package com.spaceshipproject; + +import org.bukkit.Material; +import org.bukkit.configuration.file.FileConfiguration; + +/** + * Acces type-safe a la config (config.yml). + */ +public class ConfigManager { + + private final SpaceShipProject plugin; + + public ConfigManager(SpaceShipProject plugin) { + this.plugin = plugin; + } + + public void reload() { + plugin.reloadConfig(); + } + + private FileConfiguration cfg() { + return plugin.getConfig(); + } + + public int maxShipsPerPlayer() { return cfg().getInt("max-ships-per-player", 5); } + + public int defaultSizeX() { return cfg().getInt("default-size.x", 4); } + public int defaultSizeY() { return cfg().getInt("default-size.y", 4); } + public int defaultSizeZ() { return cfg().getInt("default-size.z", 6); } + + public int maxSizeX() { return cfg().getInt("max-size.x", 16); } + public int maxSizeY() { return cfg().getInt("max-size.y", 8); } + public int maxSizeZ() { return cfg().getInt("max-size.z", 16); } + + public double costSummon() { return cfg().getDouble("costs.summon", 100); } + public double costUnload() { return cfg().getDouble("costs.unload", 0); } + public double costUpgradeBase() { return cfg().getDouble("costs.upgrade-base", 1000); } + public double costSpaceJumpPerBlock(){ return cfg().getDouble("costs.space-jump-per-block", 1); } + + public int spaceJumpBlocksPerLevel() { return cfg().getInt("space-jump.blocks-per-level", 100); } + public int spaceJumpMin() { return cfg().getInt("space-jump.min-blocks", 1); } + public int spaceJumpHardCap() { return cfg().getInt("space-jump.max-blocks-hard-cap", 5000); } + + public int summonOffsetY() { return cfg().getInt("summon-offset-y", 30); } + + public Material defaultPlatformMaterial() { return parseMat("default-platform-material", Material.SMOOTH_QUARTZ); } + public Material defaultEdgeMaterial() { return parseMat("default-edge-material", Material.QUARTZ_PILLAR); } + public Material defaultGlassMaterial() { return parseMat("default-glass-material", Material.WHITE_STAINED_GLASS); } + + public String defaultShipName() { return cfg().getString("default-ship-name", "Spaceship #%n%"); } + + public boolean checkCollision() { return cfg().getBoolean("safety.check-collision", true); } + public int minYAbovePlayer() { return cfg().getInt("safety.min-y-above-player", 10); } + + private Material parseMat(String key, Material def) { + String s = cfg().getString(key); + if (s == null) return def; + Material m = Material.matchMaterial(s); + return m != null ? m : def; + } + + /* ============ MESSAGES ============ */ + + public String prefix() { return color(cfg().getString("messages.prefix", "&8[&bSpaceShip&8] &7")); } + public String msgNoPerm() { return prefix() + color(cfg().getString("messages.no-permission", "&cNo perm")); } + public String msgNotOwner() { return prefix() + color(cfg().getString("messages.not-owner", "&cNot owner")); } + public String msgGiven(String n) { return prefix() + color(cfg().getString("messages.ship-given", "&aGiven")).replace("%name%", n); } + public String msgSummoned(String n) { return prefix() + color(cfg().getString("messages.ship-summoned", "&aSummoned")).replace("%name%", n); } + public String msgUnloaded(String n) { return prefix() + color(cfg().getString("messages.ship-unloaded", "&aUnloaded")).replace("%name%", n); } + public String msgTpUp(String n) { return prefix() + color(cfg().getString("messages.ship-teleport-up", "&aTP up")).replace("%name%", n); } + public String msgTpDown() { return prefix() + color(cfg().getString("messages.ship-teleport-down", "&aTP down")); } + public String msgAlreadyLoaded() { return prefix() + color(cfg().getString("messages.ship-already-loaded", "&eAlready loaded")); } + public String msgNotLoaded() { return prefix() + color(cfg().getString("messages.ship-not-loaded", "&eNot loaded")); } + public String msgCollision() { return prefix() + color(cfg().getString("messages.ship-collision", "&cCollision")); } + public String msgMaxShips(int max) { return prefix() + color(cfg().getString("messages.max-ships-reached", "&cMax")).replace("%max%", String.valueOf(max)); } + public String msgDeleted() { return prefix() + color(cfg().getString("messages.ship-deleted", "&cDeleted")); } + public String msgNotEnoughMoney(String need, String have) { + return prefix() + color(cfg().getString("messages.not-enough-money", "&cFonds insuffisants : %need%/%have%")) + .replace("%need%", need).replace("%have%", have); + } + public String msgVaultUnavailable() { return prefix() + color(cfg().getString("messages.vault-unavailable", "&cVault indisponible")); } + public String msgSpaceJumpSuccess(int dist, String dir, String cost) { + return prefix() + color(cfg().getString("messages.space-jump-success", "&aJump %dist% %dir% (-%cost%)")) + .replace("%dist%", String.valueOf(dist)).replace("%dir%", dir).replace("%cost%", cost); + } + public String msgSpaceJumpNotOnShip() { return prefix() + color(cfg().getString("messages.space-jump-not-on-ship", "&cPas sur le ship")); } + public String msgSpaceJumpTooFar(int max, int lvl) { + return prefix() + color(cfg().getString("messages.space-jump-too-far", "&cTrop loin (max %max% lvl %lvl%)")) + .replace("%max%", String.valueOf(max)).replace("%lvl%", String.valueOf(lvl)); + } + + public static String color(String s) { + if (s == null) return ""; + return s.replace('&', '\u00a7'); + } +} diff --git a/src/main/java/com/spaceshipproject/DatabaseManager.java b/src/main/java/com/spaceshipproject/DatabaseManager.java new file mode 100644 index 0000000..75f3c5a --- /dev/null +++ b/src/main/java/com/spaceshipproject/DatabaseManager.java @@ -0,0 +1,396 @@ +package com.spaceshipproject; + +import org.bukkit.entity.Player; + +import java.io.File; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Gestionnaire de la base de données SQLite. + * + * Tables : + * - players(uuid, name, settings, created_at, updated_at) + * - spaceships(id, owner_uuid, name, size_x/y/z, spawn_x/y/z, schematic, settings, created_at, updated_at) + * - spaceships_loaded(spaceship_id, world, origin_x/y/z, loaded_at) + */ +public class DatabaseManager { + + private final SpaceShipProject plugin; + private Connection connection; + + public DatabaseManager(SpaceShipProject plugin) { + this.plugin = plugin; + } + + public void initialize() { + try { + File dataFolder = plugin.getDataFolder(); + if (!dataFolder.exists()) { + dataFolder.mkdirs(); + } + File dbFile = new File(dataFolder, "spaceships.db"); + connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath()); + try (Statement stmt = connection.createStatement()) { + stmt.execute("PRAGMA foreign_keys = ON;"); + } + createTables(); + plugin.getLogger().info("Base de donnees SpaceShipProject initialisee."); + } catch (SQLException e) { + plugin.getLogger().severe("Erreur a l'initialisation de la base : " + e.getMessage()); + } + } + + public void close() { + try { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } catch (SQLException e) { + plugin.getLogger().severe("Erreur a la fermeture de la base : " + e.getMessage()); + } + } + + private void createTables() throws SQLException { + String players = """ + CREATE TABLE IF NOT EXISTS players ( + uuid TEXT PRIMARY KEY, + name TEXT, + settings TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """; + String spaceships = """ + CREATE TABLE IF NOT EXISTS spaceships ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + owner_uuid TEXT NOT NULL, + name TEXT NOT NULL, + size_x INTEGER NOT NULL, + size_y INTEGER NOT NULL, + size_z INTEGER NOT NULL, + spawn_x INTEGER NOT NULL, + spawn_y INTEGER NOT NULL, + spawn_z INTEGER NOT NULL, + schematic TEXT, + settings TEXT, + level INTEGER NOT NULL DEFAULT 1, + money INTEGER NOT NULL DEFAULT 0, + last_loaded_level INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (owner_uuid) REFERENCES players(uuid) ON DELETE CASCADE + ) + """; + String loaded = """ + CREATE TABLE IF NOT EXISTS spaceships_loaded ( + spaceship_id INTEGER PRIMARY KEY, + world TEXT NOT NULL, + origin_x INTEGER NOT NULL, + origin_y INTEGER NOT NULL, + origin_z INTEGER NOT NULL, + loaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (spaceship_id) REFERENCES spaceships(id) ON DELETE CASCADE + ) + """; + try (Statement stmt = connection.createStatement()) { + stmt.execute(players); + stmt.execute(spaceships); + stmt.execute(loaded); + // Migrations (idempotentes) pour les bases creees avant l'ajout de level/money. + tryAlter(stmt, "ALTER TABLE spaceships ADD COLUMN level INTEGER NOT NULL DEFAULT 1"); + tryAlter(stmt, "ALTER TABLE spaceships ADD COLUMN money INTEGER NOT NULL DEFAULT 0"); + tryAlter(stmt, "ALTER TABLE spaceships ADD COLUMN last_loaded_level INTEGER NOT NULL DEFAULT 1"); + } + } + + private void tryAlter(Statement stmt, String sql) { + try { stmt.execute(sql); } catch (SQLException ignored) { /* colonne deja presente */ } + } + + /* ========================= PLAYERS ========================= */ + + public void upsertPlayer(Player player) { + String sql = """ + INSERT INTO players(uuid, name, updated_at) + VALUES(?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(uuid) DO UPDATE SET name = excluded.name, updated_at = CURRENT_TIMESTAMP + """; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, player.getUniqueId().toString()); + stmt.setString(2, player.getName()); + stmt.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().severe("Erreur upsertPlayer : " + e.getMessage()); + } + } + + public void setPlayerSettings(UUID uuid, String settingsJson) { + String sql = "UPDATE players SET settings = ?, updated_at = CURRENT_TIMESTAMP WHERE uuid = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, settingsJson); + stmt.setString(2, uuid.toString()); + stmt.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().severe("Erreur setPlayerSettings : " + e.getMessage()); + } + } + + public String getPlayerSettings(UUID uuid) { + String sql = "SELECT settings FROM players WHERE uuid = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, uuid.toString()); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) return rs.getString("settings"); + } + } catch (SQLException e) { + plugin.getLogger().severe("Erreur getPlayerSettings : " + e.getMessage()); + } + return null; + } + + /* ========================= SPACESHIPS ========================= */ + + public long createSpaceship(UUID owner, String name, + int sizeX, int sizeY, int sizeZ, + int spawnX, int spawnY, int spawnZ, + String schematicJson, String settingsJson) { + String sql = """ + INSERT INTO spaceships(owner_uuid, name, size_x, size_y, size_z, + spawn_x, spawn_y, spawn_z, schematic, settings, level, money, last_loaded_level) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, 1) + """; + try (PreparedStatement stmt = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + stmt.setString(1, owner.toString()); + stmt.setString(2, name); + stmt.setInt(3, sizeX); + stmt.setInt(4, sizeY); + stmt.setInt(5, sizeZ); + stmt.setInt(6, spawnX); + stmt.setInt(7, spawnY); + stmt.setInt(8, spawnZ); + stmt.setString(9, schematicJson); + stmt.setString(10, settingsJson); + stmt.executeUpdate(); + try (ResultSet keys = stmt.getGeneratedKeys()) { + if (keys.next()) return keys.getLong(1); + } + } catch (SQLException e) { + plugin.getLogger().severe("Erreur createSpaceship : " + e.getMessage()); + } + return -1; + } + + public Spaceship getSpaceship(long id) { + String sql = "SELECT * FROM spaceships WHERE id = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setLong(1, id); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) return rowToSpaceship(rs); + } + } catch (SQLException e) { + plugin.getLogger().severe("Erreur getSpaceship : " + e.getMessage()); + } + return null; + } + + public List getSpaceshipsByOwner(UUID owner) { + List list = new ArrayList<>(); + String sql = "SELECT * FROM spaceships WHERE owner_uuid = ? ORDER BY id ASC"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, owner.toString()); + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) list.add(rowToSpaceship(rs)); + } + } catch (SQLException e) { + plugin.getLogger().severe("Erreur getSpaceshipsByOwner : " + e.getMessage()); + } + return list; + } + + public int countSpaceshipsByOwner(UUID owner) { + String sql = "SELECT COUNT(*) FROM spaceships WHERE owner_uuid = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, owner.toString()); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) return rs.getInt(1); + } + } catch (SQLException e) { + plugin.getLogger().severe("Erreur countSpaceshipsByOwner : " + e.getMessage()); + } + return 0; + } + + @SuppressWarnings("deprecation") + public void updateSpaceship(Spaceship ship) { + String sql = """ + UPDATE spaceships SET + name = ?, size_x = ?, size_y = ?, size_z = ?, + spawn_x = ?, spawn_y = ?, spawn_z = ?, + schematic = ?, settings = ?, level = ?, money = ?, last_loaded_level = ?, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + """; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, ship.getName()); + stmt.setInt(2, ship.getSizeX()); + stmt.setInt(3, ship.getSizeY()); + stmt.setInt(4, ship.getSizeZ()); + stmt.setInt(5, ship.getSpawnX()); + stmt.setInt(6, ship.getSpawnY()); + stmt.setInt(7, ship.getSpawnZ()); + stmt.setString(8, ship.getSchematicJson()); + stmt.setString(9, ship.getSettingsJson()); + stmt.setInt(10, ship.getLevel()); + stmt.setInt(11, ship.getMoney()); + stmt.setInt(12, ship.getLastLoadedLevel()); + stmt.setLong(13, ship.getId()); + stmt.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().severe("Erreur updateSpaceship : " + e.getMessage()); + } + } + + public void deleteSpaceship(long id) { + try (PreparedStatement st1 = connection.prepareStatement("DELETE FROM spaceships_loaded WHERE spaceship_id = ?"); + PreparedStatement st2 = connection.prepareStatement("DELETE FROM spaceships WHERE id = ?")) { + st1.setLong(1, id); + st1.executeUpdate(); + st2.setLong(1, id); + st2.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().severe("Erreur deleteSpaceship : " + e.getMessage()); + } + } + + private Spaceship rowToSpaceship(ResultSet rs) throws SQLException { + int level = 1, money = 0, lastLoadedLevel = 1; + try { level = rs.getInt("level"); if (rs.wasNull()) level = 1; } catch (SQLException ignored) {} + try { money = rs.getInt("money"); } catch (SQLException ignored) {} + try { lastLoadedLevel = rs.getInt("last_loaded_level"); if (rs.wasNull()) lastLoadedLevel = 1; } catch (SQLException ignored) {} + return new Spaceship( + rs.getLong("id"), + UUID.fromString(rs.getString("owner_uuid")), + rs.getString("name"), + rs.getInt("size_x"), rs.getInt("size_y"), rs.getInt("size_z"), + rs.getInt("spawn_x"), rs.getInt("spawn_y"), rs.getInt("spawn_z"), + rs.getString("schematic"), + rs.getString("settings"), + level, money, lastLoadedLevel + ); + } + + /* ========================= SPACESHIPS_LOADED ========================= */ + + public void markLoaded(long spaceshipId, String world, int x, int y, int z) { + String sql = """ + INSERT INTO spaceships_loaded(spaceship_id, world, origin_x, origin_y, origin_z, loaded_at) + VALUES(?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(spaceship_id) DO UPDATE SET + world = excluded.world, + origin_x = excluded.origin_x, + origin_y = excluded.origin_y, + origin_z = excluded.origin_z, + loaded_at = CURRENT_TIMESTAMP + """; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setLong(1, spaceshipId); + stmt.setString(2, world); + stmt.setInt(3, x); + stmt.setInt(4, y); + stmt.setInt(5, z); + stmt.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().severe("Erreur markLoaded : " + e.getMessage()); + } + } + + public void markUnloaded(long spaceshipId) { + String sql = "DELETE FROM spaceships_loaded WHERE spaceship_id = ?"; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setLong(1, spaceshipId); + stmt.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().severe("Erreur markUnloaded : " + e.getMessage()); + } + } + + public LoadedSpaceship getLoaded(long spaceshipId) { + String sql = """ + SELECT l.spaceship_id, l.world, l.origin_x, l.origin_y, l.origin_z, + s.size_x, s.size_y, s.size_z + FROM spaceships_loaded l + JOIN spaceships s ON s.id = l.spaceship_id + WHERE l.spaceship_id = ? + """; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setLong(1, spaceshipId); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return new LoadedSpaceship( + rs.getLong("spaceship_id"), + rs.getString("world"), + rs.getInt("origin_x"), rs.getInt("origin_y"), rs.getInt("origin_z"), + rs.getInt("size_x"), rs.getInt("size_y"), rs.getInt("size_z") + ); + } + } + } catch (SQLException e) { + plugin.getLogger().severe("Erreur getLoaded : " + e.getMessage()); + } + return null; + } + + public List getLoadedByOwner(UUID owner) { + List list = new ArrayList<>(); + String sql = """ + SELECT l.spaceship_id, l.world, l.origin_x, l.origin_y, l.origin_z, + s.size_x, s.size_y, s.size_z + FROM spaceships_loaded l + JOIN spaceships s ON s.id = l.spaceship_id + WHERE s.owner_uuid = ? + """; + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, owner.toString()); + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + list.add(new LoadedSpaceship( + rs.getLong("spaceship_id"), + rs.getString("world"), + rs.getInt("origin_x"), rs.getInt("origin_y"), rs.getInt("origin_z"), + rs.getInt("size_x"), rs.getInt("size_y"), rs.getInt("size_z") + )); + } + } + } catch (SQLException e) { + plugin.getLogger().severe("Erreur getLoadedByOwner : " + e.getMessage()); + } + return list; + } + + public List getAllLoaded() { + List list = new ArrayList<>(); + String sql = """ + SELECT l.spaceship_id, l.world, l.origin_x, l.origin_y, l.origin_z, + s.size_x, s.size_y, s.size_z + FROM spaceships_loaded l + JOIN spaceships s ON s.id = l.spaceship_id + """; + try (PreparedStatement stmt = connection.prepareStatement(sql); + ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + list.add(new LoadedSpaceship( + rs.getLong("spaceship_id"), + rs.getString("world"), + rs.getInt("origin_x"), rs.getInt("origin_y"), rs.getInt("origin_z"), + rs.getInt("size_x"), rs.getInt("size_y"), rs.getInt("size_z") + )); + } + } catch (SQLException e) { + plugin.getLogger().severe("Erreur getAllLoaded : " + e.getMessage()); + } + return list; + } +} diff --git a/src/main/java/com/spaceshipproject/EconomyHook.java b/src/main/java/com/spaceshipproject/EconomyHook.java new file mode 100644 index 0000000..b92adfc --- /dev/null +++ b/src/main/java/com/spaceshipproject/EconomyHook.java @@ -0,0 +1,76 @@ +package com.spaceshipproject; + +import net.milkbowl.vault.economy.Economy; +import net.milkbowl.vault.economy.EconomyResponse; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.plugin.RegisteredServiceProvider; + +import java.util.UUID; + +/** + * Wrapper autour de Vault. La seule source d'argent du plugin est l'economie expose par Vault + * (EssentialsEconomy par defaut). Aucune monnaie n'est stockee dans la base du plugin. + */ +public class EconomyHook { + + private final SpaceShipProject plugin; + private Economy economy; + + public EconomyHook(SpaceShipProject plugin) { + this.plugin = plugin; + } + + /** Initialise l'integration Vault. Retourne true si OK. */ + public boolean setup() { + if (plugin.getServer().getPluginManager().getPlugin("Vault") == null) { + plugin.getLogger().warning("Vault est introuvable : les fonctionnalites economiques seront desactivees."); + return false; + } + RegisteredServiceProvider rsp = plugin.getServer().getServicesManager().getRegistration(Economy.class); + if (rsp == null) { + plugin.getLogger().warning("Aucun fournisseur d'economie Vault detecte (EssentialsEconomy ?). Economie desactivee."); + return false; + } + this.economy = rsp.getProvider(); + plugin.getLogger().info("Vault detecte, fournisseur : " + economy.getName()); + return true; + } + + public boolean isReady() { return economy != null; } + + public double getBalance(UUID uuid) { + if (!isReady()) return 0; + OfflinePlayer p = Bukkit.getOfflinePlayer(uuid); + return economy.getBalance(p); + } + + public boolean has(UUID uuid, double amount) { + if (!isReady()) return false; + OfflinePlayer p = Bukkit.getOfflinePlayer(uuid); + return economy.has(p, amount); + } + + /** Retire `amount` au joueur. True si succes. */ + public boolean withdraw(UUID uuid, double amount) { + if (!isReady()) return false; + if (amount <= 0) return true; + OfflinePlayer p = Bukkit.getOfflinePlayer(uuid); + EconomyResponse r = economy.withdrawPlayer(p, amount); + return r.transactionSuccess(); + } + + /** Depose `amount` au joueur. True si succes. */ + public boolean deposit(UUID uuid, double amount) { + if (!isReady()) return false; + if (amount <= 0) return true; + OfflinePlayer p = Bukkit.getOfflinePlayer(uuid); + EconomyResponse r = economy.depositPlayer(p, amount); + return r.transactionSuccess(); + } + + public String format(double amount) { + if (!isReady()) return String.valueOf(amount); + return economy.format(amount); + } +} diff --git a/src/main/java/com/spaceshipproject/LoadedSpaceship.java b/src/main/java/com/spaceshipproject/LoadedSpaceship.java new file mode 100644 index 0000000..8eb130a --- /dev/null +++ b/src/main/java/com/spaceshipproject/LoadedSpaceship.java @@ -0,0 +1,59 @@ +package com.spaceshipproject; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.util.BoundingBox; + +/** + * Représente un spaceship actuellement chargé dans le monde. + * Stocké en table spaceships_loaded. + */ +public class LoadedSpaceship { + + private final long spaceshipId; + private final String worldName; + private final int originX; + private final int originY; + private final int originZ; + private final int sizeX; + private final int sizeY; + private final int sizeZ; + + public LoadedSpaceship(long spaceshipId, String worldName, + int originX, int originY, int originZ, + int sizeX, int sizeY, int sizeZ) { + this.spaceshipId = spaceshipId; + this.worldName = worldName; + this.originX = originX; + this.originY = originY; + this.originZ = originZ; + this.sizeX = sizeX; + this.sizeY = sizeY; + this.sizeZ = sizeZ; + } + + public long getSpaceshipId() { return spaceshipId; } + public String getWorldName() { return worldName; } + public int getOriginX() { return originX; } + public int getOriginY() { return originY; } + public int getOriginZ() { return originZ; } + public int getSizeX() { return sizeX; } + public int getSizeY() { return sizeY; } + public int getSizeZ() { return sizeZ; } + + public World getWorld() { + return Bukkit.getWorld(worldName); + } + + public BoundingBox getBoundingBox() { + return new BoundingBox(originX, originY, originZ, + originX + sizeX, originY + sizeY, originZ + sizeZ); + } + + public boolean contains(Location loc) { + if (loc == null || loc.getWorld() == null) return false; + if (!loc.getWorld().getName().equals(worldName)) return false; + return getBoundingBox().contains(loc.toVector()); + } +} diff --git a/src/main/java/com/spaceshipproject/ModelManager.java b/src/main/java/com/spaceshipproject/ModelManager.java new file mode 100644 index 0000000..3ef3a7a --- /dev/null +++ b/src/main/java/com/spaceshipproject/ModelManager.java @@ -0,0 +1,205 @@ +package com.spaceshipproject; + +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Gere les modeles (templates) de spaceship : + * - lecture / ecriture des schematiques modeles sur disque (dossier "models/") + * - sessions d'edition par admin (loadModel / saveModel) + * + * Le modele "default.json" est utilise par /ss create quand il existe. + */ +public class ModelManager { + + private static final String DEFAULT = "default"; + + private final SpaceShipProject plugin; + private final File modelsDir; + + /** Sessions d'edition actives : adminUuid -> session. */ + private final Map sessions = new HashMap<>(); + + public ModelManager(SpaceShipProject plugin) { + this.plugin = plugin; + this.modelsDir = new File(plugin.getDataFolder(), "models"); + if (!modelsDir.exists()) modelsDir.mkdirs(); + } + + /** Recharge depuis disque (ne fait rien d'autre, lecture a la demande). */ + public void reload() { /* no-op : on lit a la demande */ } + + /** Charge un modele du disque, ou null si absent. */ + public String loadModelJson(String name) { + File file = new File(modelsDir, sanitize(name) + ".json"); + if (!file.exists()) return null; + try { + return Files.readString(file.toPath(), StandardCharsets.UTF_8); + } catch (IOException e) { + plugin.getLogger().warning("Lecture modele " + name + " : " + e.getMessage()); + return null; + } + } + + /** Ecrit un modele sur disque. */ + public boolean saveModelJson(String name, String json) { + File file = new File(modelsDir, sanitize(name) + ".json"); + try { + Files.writeString(file.toPath(), json, StandardCharsets.UTF_8); + return true; + } catch (IOException e) { + plugin.getLogger().warning("Ecriture modele " + name + " : " + e.getMessage()); + return false; + } + } + + /** Charge le modele "default", ou null si absent. */ + public String loadDefaultModelJson() { return loadModelJson(DEFAULT); } + + /** True si "default.json" est present sur disque. */ + public boolean hasDefaultModel() { return loadDefaultModelJson() != null; } + + /* ============================ EDIT SESSIONS ============================ */ + + /** + * Demarre une session d'edition : place la schematique du modele dans le monde devant l'admin. + * Retourne true si OK. + */ + public boolean loadForEdit(Player admin, String modelName) { + if (sessions.containsKey(admin.getUniqueId())) { + admin.sendMessage(ConfigManager.color("&cVous avez deja une session d'edition. /ssa savemodel ou /ssa cancelmodel.")); + return false; + } + ConfigManager cfg = plugin.getConfigManager(); + String json = loadModelJson(modelName); + if (json == null) { + // Generer un modele initial a partir de la config par defaut. + json = SchematicHelper.buildDefaultSchematic( + cfg.defaultSizeX(), cfg.defaultSizeY(), cfg.defaultSizeZ(), + cfg.defaultPlatformMaterial(), + cfg.defaultEdgeMaterial(), + cfg.defaultGlassMaterial()); + } + int[] size = SchematicHelper.readSize(json); + World world = admin.getWorld(); + int ox = admin.getLocation().getBlockX() - size[0] / 2; + int oz = admin.getLocation().getBlockZ() - size[2] / 2; + int oy = admin.getLocation().getBlockY() + cfg.summonOffsetY(); + // Empeche d'ecraser des spaceships charges. + if (SchematicHelper.hasCollision(world, ox, oy, oz, size[0], size[1], size[2])) { + admin.sendMessage(ConfigManager.color("&cZone obstruee, deplacez-vous puis reessayez.")); + return false; + } + try { + SchematicHelper.restoreRegion(json, world, ox, oy, oz); + } catch (Exception ex) { + admin.sendMessage(ConfigManager.color("&cErreur restauration modele : " + ex.getMessage())); + return false; + } + sessions.put(admin.getUniqueId(), + new ModelEditSession(modelName, world.getName(), ox, oy, oz, size[0], size[1], size[2])); + admin.sendMessage(ConfigManager.color("&aModele &b" + modelName + " &acharge en x=" + ox + " y=" + oy + " z=" + oz + ". Editez puis /ssa savemodel.")); + return true; + } + + /** Capture la session active de l'admin et l'ecrit sur disque. */ + public boolean saveEdit(Player admin) { + ModelEditSession s = sessions.get(admin.getUniqueId()); + if (s == null) { + admin.sendMessage(ConfigManager.color("&cAucune session d'edition active.")); + return false; + } + World world = plugin.getServer().getWorld(s.worldName); + if (world == null) { + admin.sendMessage(ConfigManager.color("&cMonde introuvable.")); + sessions.remove(admin.getUniqueId()); + return false; + } + // Spawn = milieu sur le sol +1, comme le defaut, mais conserve celui du modele original si present. + int spawnX = s.sizeX / 2, spawnY = 1, spawnZ = s.sizeZ / 2; + String existing = loadModelJson(s.modelName); + if (existing != null) { + try { + int[] sp = SchematicHelper.readSpawn(existing); + spawnX = sp[0]; spawnY = sp[1]; spawnZ = sp[2]; + } catch (Exception ignored) {} + } + String json = SchematicHelper.captureRegion(world, s.originX, s.originY, s.originZ, + s.sizeX, s.sizeY, s.sizeZ, spawnX, spawnY, spawnZ); + boolean ok = saveModelJson(s.modelName, json); + if (ok) { + SchematicHelper.clearRegion(world, s.originX, s.originY, s.originZ, s.sizeX, s.sizeY, s.sizeZ); + sessions.remove(admin.getUniqueId()); + admin.sendMessage(ConfigManager.color("&aModele &b" + s.modelName + " &asauvegarde.")); + } else { + admin.sendMessage(ConfigManager.color("&cErreur d'ecriture du modele.")); + } + return ok; + } + + /** Annule la session : nettoie la zone et oublie la session. */ + public boolean cancelEdit(Player admin) { + ModelEditSession s = sessions.remove(admin.getUniqueId()); + if (s == null) { + admin.sendMessage(ConfigManager.color("&cAucune session active.")); + return false; + } + World world = plugin.getServer().getWorld(s.worldName); + if (world != null) { + SchematicHelper.clearRegion(world, s.originX, s.originY, s.originZ, s.sizeX, s.sizeY, s.sizeZ); + } + admin.sendMessage(ConfigManager.color("&7Session d'edition annulee.")); + return true; + } + + public ModelEditSession getSession(UUID admin) { return sessions.get(admin); } + + /** Decharge toutes les sessions au shutdown (sauvegarde best-effort + nettoyage). */ + public void unloadAllOnShutdown() { + for (Map.Entry entry : sessions.entrySet()) { + ModelEditSession s = entry.getValue(); + World world = plugin.getServer().getWorld(s.worldName); + if (world != null) { + try { + String json = SchematicHelper.captureRegion(world, s.originX, s.originY, s.originZ, + s.sizeX, s.sizeY, s.sizeZ, s.sizeX / 2, 1, s.sizeZ / 2); + saveModelJson(s.modelName, json); + SchematicHelper.clearRegion(world, s.originX, s.originY, s.originZ, s.sizeX, s.sizeY, s.sizeZ); + } catch (Exception ex) { + plugin.getLogger().warning("Erreur shutdown session modele : " + ex.getMessage()); + } + } + } + sessions.clear(); + } + + private String sanitize(String name) { + if (name == null || name.isBlank()) return DEFAULT; + return name.toLowerCase().replaceAll("[^a-z0-9_-]", ""); + } + + /** Petit conteneur immuable. */ + public static final class ModelEditSession { + public final String modelName; + public final String worldName; + public final int originX, originY, originZ; + public final int sizeX, sizeY, sizeZ; + + public ModelEditSession(String modelName, String worldName, + int originX, int originY, int originZ, + int sizeX, int sizeY, int sizeZ) { + this.modelName = modelName; + this.worldName = worldName; + this.originX = originX; this.originY = originY; this.originZ = originZ; + this.sizeX = sizeX; this.sizeY = sizeY; this.sizeZ = sizeZ; + } + } +} diff --git a/src/main/java/com/spaceshipproject/PlayerListener.java b/src/main/java/com/spaceshipproject/PlayerListener.java new file mode 100644 index 0000000..a73553a --- /dev/null +++ b/src/main/java/com/spaceshipproject/PlayerListener.java @@ -0,0 +1,95 @@ +package com.spaceshipproject; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerRespawnEvent; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * - Connexion : insere/maj le joueur dans la table players, nettoie ses telecommandes. + * - Deconnexion : decharge tous ses spaceships (sauvegarde les modifications + le contenu des coffres). + */ +public class PlayerListener implements Listener { + + private final SpaceShipProject plugin; + /** Memorise le ship sur lequel le joueur est mort, pour respawn. */ + private final Map deathOnShip = new HashMap<>(); + + public PlayerListener(SpaceShipProject plugin) { + this.plugin = plugin; + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + plugin.getDatabaseManager().upsertPlayer(player); + int removed = plugin.getSpaceshipManager().sanitizePlayerRemotes(player); + if (removed > 0) { + player.sendMessage(plugin.getConfigManager().prefix() + + ConfigManager.color("&7" + removed + " telecommande(s) invalide(s) ont ete supprimees.")); + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onDeath(PlayerDeathEvent event) { + Player player = event.getEntity(); + // Cherche un spaceship du joueur dont la BB contient la position du deces. + Location loc = player.getLocation(); + if (loc.getWorld() == null) return; + // Verifie d'abord les ships dont le joueur est proprietaire et qui sont charges. + for (LoadedSpaceship loaded : plugin.getDatabaseManager().getLoadedByOwner(player.getUniqueId())) { + if (loaded.contains(loc)) { + deathOnShip.put(player.getUniqueId(), loaded.getSpaceshipId()); + return; + } + } + // Fallback : premier ship charge du joueur, le cas echeant. + List all = plugin.getDatabaseManager().getLoadedByOwner(player.getUniqueId()); + if (!all.isEmpty()) deathOnShip.put(player.getUniqueId(), all.get(0).getSpaceshipId()); + } + + @EventHandler(priority = EventPriority.HIGH) + public void onRespawn(PlayerRespawnEvent event) { + Player player = event.getPlayer(); + Long shipId = deathOnShip.remove(player.getUniqueId()); + if (shipId == null) return; + Spaceship ship = plugin.getDatabaseManager().getSpaceship(shipId); + if (ship == null) return; + LoadedSpaceship loaded = plugin.getDatabaseManager().getLoaded(shipId); + if (loaded == null) return; + World world = loaded.getWorld(); + if (world == null) return; + Location target = new Location(world, + loaded.getOriginX() + ship.getSpawnX() + 0.5, + loaded.getOriginY() + ship.getSpawnY(), + loaded.getOriginZ() + ship.getSpawnZ() + 0.5); + event.setRespawnLocation(target); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + try { + // Si le joueur est sur l'un de ses ships, le tp au sol avant de decharger + // (pour qu'il ne reapparaisse pas en plein ciel a sa prochaine connexion). + Spaceship onShip = plugin.getSpaceshipManager().findOwnedShipPlayerIsOn(player); + if (onShip != null) { + plugin.getSpaceshipManager().teleportDown(player, onShip); + } + plugin.getSpaceshipManager().unloadAllForPlayer(player.getUniqueId()); + } catch (Exception ex) { + plugin.getLogger().warning("Erreur quit pour " + player.getName() + " : " + ex.getMessage()); + } + } +} diff --git a/src/main/java/com/spaceshipproject/RemoteListener.java b/src/main/java/com/spaceshipproject/RemoteListener.java new file mode 100644 index 0000000..2a5682d --- /dev/null +++ b/src/main/java/com/spaceshipproject/RemoteListener.java @@ -0,0 +1,124 @@ +package com.spaceshipproject; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; + +/** + * Gere les interactions avec la telecommande boussole. + * + * - Clic gauche + * - ship non charge -> invoquer + * - ship charge && joueur PAS dessus -> decharger (sauvegarde les modifications) + * - ship charge && joueur dessus -> rien (utilise clic droit pour redescendre) + * + * - Clic droit + * - ship non charge -> invoquer puis teleporter + * - ship charge && joueur PAS dessus -> teleporter sur le ship + * - ship charge && joueur dessus -> redescendre au sol + * + * - Shift + clic droit -> ouvrir le GUI parametres + */ +public class RemoteListener implements Listener { + + private final SpaceShipProject plugin; + private final SpaceshipManager manager; + private final RemoteManager remotes; + private final ConfigManager cfg; + + public RemoteListener(SpaceShipProject plugin) { + this.plugin = plugin; + this.manager = plugin.getSpaceshipManager(); + this.remotes = plugin.getRemoteManager(); + this.cfg = plugin.getConfigManager(); + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = false) + public void onInteract(PlayerInteractEvent event) { + if (event.getHand() != EquipmentSlot.HAND) return; + + ItemStack item = event.getItem(); + if (item == null || !remotes.isRemote(item)) return; + + Player player = event.getPlayer(); + long shipId = remotes.getShipId(item); + if (shipId <= 0) return; + + Spaceship ship = plugin.getDatabaseManager().getSpaceship(shipId); + if (ship == null) { + player.sendMessage(cfg.prefix() + ConfigManager.color("&cCe spaceship n'existe plus, telecommande retiree.")); + // Retire la telecommande obsolete pour eviter le spam. + int slot = player.getInventory().getHeldItemSlot(); + ItemStack mainHand = player.getInventory().getItemInMainHand(); + if (plugin.getRemoteManager().isRemote(mainHand) + && plugin.getRemoteManager().getShipId(mainHand) == shipId) { + player.getInventory().setItem(slot, null); + } + event.setCancelled(true); + return; + } + + // Securite : seul le proprietaire (ou un admin) peut utiliser la telecommande. + if (!ship.getOwnerUuid().equals(player.getUniqueId()) + && !player.hasPermission("spaceship.admin")) { + player.sendMessage(cfg.msgNotOwner()); + event.setCancelled(true); + return; + } + + Action action = event.getAction(); + + // Shift + clic droit -> GUI parametres + if (player.isSneaking() + && (action == Action.RIGHT_CLICK_AIR || action == Action.RIGHT_CLICK_BLOCK)) { + event.setCancelled(true); + new SpaceshipGUI(plugin, player, ship).open(); + return; + } + + // Clic gauche + if (action == Action.LEFT_CLICK_AIR || action == Action.LEFT_CLICK_BLOCK) { + event.setCancelled(true); + handleLeftClick(player, ship); + return; + } + + // Clic droit + if (action == Action.RIGHT_CLICK_AIR || action == Action.RIGHT_CLICK_BLOCK) { + event.setCancelled(true); + handleRightClick(player, ship); + } + } + + private void handleLeftClick(Player player, Spaceship ship) { + boolean loaded = manager.isLoaded(ship.getId()); + if (!loaded) { + manager.summon(player, ship); + } else if (manager.isPlayerOnShip(player, ship)) { + // Sur le ship -> on n'unload pas (le joueur tomberait), on indique la marche a suivre + player.sendMessage(cfg.prefix() + ConfigManager.color( + "&eUtilisez le clic droit pour redescendre avant de decharger.")); + } else { + manager.unload(player, ship); + } + } + + private void handleRightClick(Player player, Spaceship ship) { + boolean loaded = manager.isLoaded(ship.getId()); + if (!loaded) { + // Invoque puis teleporte + if (manager.summon(player, ship)) { + manager.teleportToShip(player, ship); + } + } else if (manager.isPlayerOnShip(player, ship)) { + manager.teleportDown(player, ship); + } else { + manager.teleportToShip(player, ship); + } + } +} diff --git a/src/main/java/com/spaceshipproject/RemoteManager.java b/src/main/java/com/spaceshipproject/RemoteManager.java new file mode 100644 index 0000000..6cf3f29 --- /dev/null +++ b/src/main/java/com/spaceshipproject/RemoteManager.java @@ -0,0 +1,88 @@ +package com.spaceshipproject; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +import java.util.ArrayList; +import java.util.List; + +/** + * Cree, identifie et met a jour les boussoles "telecommande" de spaceship. + * Chaque boussole porte dans son PersistentDataContainer l'id du spaceship associe. + */ +public class RemoteManager { + + private final SpaceShipProject plugin; + private final NamespacedKey shipIdKey; + private final NamespacedKey markerKey; + + public RemoteManager(SpaceShipProject plugin) { + this.plugin = plugin; + this.shipIdKey = new NamespacedKey(plugin, "spaceship_id"); + this.markerKey = new NamespacedKey(plugin, "spaceship_remote"); + } + + public NamespacedKey getShipIdKey() { return shipIdKey; } + public NamespacedKey getMarkerKey() { return markerKey; } + + /** True si l'item est une telecommande SpaceShipProject. */ + public boolean isRemote(ItemStack item) { + if (item == null || item.getType() != Material.RECOVERY_COMPASS) return false; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return false; + return meta.getPersistentDataContainer().has(markerKey, PersistentDataType.BYTE); + } + + /** Lit l'id du spaceship associe a la telecommande, ou -1. */ + public long getShipId(ItemStack item) { + if (!isRemote(item)) return -1; + ItemMeta meta = item.getItemMeta(); + Long id = meta.getPersistentDataContainer().get(shipIdKey, PersistentDataType.LONG); + return id == null ? -1 : id; + } + + /** Construit une telecommande pour le spaceship donne. */ + public ItemStack buildRemote(Spaceship ship) { + ItemStack item = new ItemStack(Material.RECOVERY_COMPASS); + ItemMeta meta = item.getItemMeta(); + if (meta == null) return item; + + PersistentDataContainer pdc = meta.getPersistentDataContainer(); + pdc.set(markerKey, PersistentDataType.BYTE, (byte) 1); + pdc.set(shipIdKey, PersistentDataType.LONG, ship.getId()); + + applyDisplay(meta, ship); + + item.setItemMeta(meta); + return item; + } + + /** + * Reconstruit la telecommande sur place (apres renommage par exemple). + * Conserve la quantite et l'emplacement de l'item. + */ + public void refreshRemote(ItemStack item, Spaceship ship) { + if (!isRemote(item) || ship == null) return; + ItemMeta meta = item.getItemMeta(); + if (meta == null) return; + applyDisplay(meta, ship); + item.setItemMeta(meta); + } + + private void applyDisplay(ItemMeta meta, Spaceship ship) { + meta.setDisplayName(ConfigManager.color("&b&l\u2730 &fTelecommande &7- &b" + ship.getName())); + List lore = new ArrayList<>(); + lore.add(ConfigManager.color("&7ID : &f#" + ship.getId())); + lore.add(ConfigManager.color("&7Niveau : &e" + ship.getLevel())); + lore.add(ConfigManager.color("&7Taille : &f" + ship.getSizeX() + "x" + ship.getSizeY() + "x" + ship.getSizeZ())); + lore.add(""); + lore.add(ConfigManager.color("&eClic gauche &8: &7invoquer / decharger")); + lore.add(ConfigManager.color("&eClic droit &8: &7monter / redescendre")); + lore.add(ConfigManager.color("&eShift + clic droit &8: &7parametres")); + meta.setLore(lore); + } +} diff --git a/src/main/java/com/spaceshipproject/SchematicHelper.java b/src/main/java/com/spaceshipproject/SchematicHelper.java new file mode 100644 index 0000000..26f75b8 --- /dev/null +++ b/src/main/java/com/spaceshipproject/SchematicHelper.java @@ -0,0 +1,441 @@ +package com.spaceshipproject; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.Container; +import org.bukkit.block.data.BlockData; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Capture / restauration / nettoyage / generation de schematiques compactes (palette + RLE) + * stockees en JSON pour le plugin SpaceShipProject. + * + * Format JSON : + * { + * "size":[sx,sy,sz], + * "spawn":[ox,oy,oz], + * "palette":["minecraft:air","minecraft:smooth_quartz", ...], + * "blocks":[[paletteIndex, runLength], ...], + * "containers":[ + * {"x":1,"y":2,"z":3,"items":[{"slot":0,"data":"base64..."}, ...]} + * ] + * } + */ +public final class SchematicHelper { + + private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create(); + private static final String AIR = "minecraft:air"; + + private SchematicHelper() {} + + /* ============================ CAPTURE ============================ */ + + /** + * Capture la region [origin, origin+size) du monde dans une representation JSON compacte. + * Conserve le spawn relatif (pour ne pas le perdre lors d'un re-load). + */ + public static String captureRegion(World world, + int originX, int originY, int originZ, + int sizeX, int sizeY, int sizeZ, + int spawnX, int spawnY, int spawnZ) { + Map paletteIndex = new LinkedHashMap<>(); + List palette = new ArrayList<>(); + List blocks = new ArrayList<>(); + JsonArray containers = new JsonArray(); + + int currentIdx = -1; + int run = 0; + + for (int y = 0; y < sizeY; y++) { + for (int z = 0; z < sizeZ; z++) { + for (int x = 0; x < sizeX; x++) { + Block b = world.getBlockAt(originX + x, originY + y, originZ + z); + String state = b.getBlockData().getAsString(); + Integer idx = paletteIndex.get(state); + if (idx == null) { + idx = palette.size(); + palette.add(state); + paletteIndex.put(state, idx); + } + if (idx == currentIdx) { + run++; + } else { + if (currentIdx != -1) blocks.add(new int[]{currentIdx, run}); + currentIdx = idx; + run = 1; + } + + // Capture du contenu des conteneurs (coffres, barils, fournaises, etc.) + BlockState bs = b.getState(false); + if (bs instanceof Container container) { + JsonObject entry = serializeContainer(container, x, y, z); + if (entry != null) containers.add(entry); + } + } + } + } + if (run > 0) blocks.add(new int[]{currentIdx, run}); + + JsonObject root = new JsonObject(); + JsonArray sizeArr = new JsonArray(); + sizeArr.add(sizeX); sizeArr.add(sizeY); sizeArr.add(sizeZ); + root.add("size", sizeArr); + + JsonArray spawnArr = new JsonArray(); + spawnArr.add(spawnX); spawnArr.add(spawnY); spawnArr.add(spawnZ); + root.add("spawn", spawnArr); + + JsonArray paletteArr = new JsonArray(); + for (String p : palette) paletteArr.add(p); + root.add("palette", paletteArr); + + JsonArray blocksArr = new JsonArray(); + for (int[] run2 : blocks) { + JsonArray ja = new JsonArray(); + ja.add(run2[0]); + ja.add(run2[1]); + blocksArr.add(ja); + } + root.add("blocks", blocksArr); + + if (containers.size() > 0) root.add("containers", containers); + + return GSON.toJson(root); + } + + private static JsonObject serializeContainer(Container container, int x, int y, int z) { + Inventory inv = container.getSnapshotInventory(); + if (inv == null) return null; + JsonArray items = new JsonArray(); + for (int slot = 0; slot < inv.getSize(); slot++) { + ItemStack stack = inv.getItem(slot); + if (stack == null || stack.getType().isAir()) continue; + JsonObject e = new JsonObject(); + e.addProperty("slot", slot); + e.addProperty("data", Base64.getEncoder().encodeToString(stack.serializeAsBytes())); + items.add(e); + } + if (items.size() == 0) return null; + JsonObject obj = new JsonObject(); + obj.addProperty("x", x); + obj.addProperty("y", y); + obj.addProperty("z", z); + obj.add("items", items); + return obj; + } + + /* ============================ RESTORE ============================ */ + + /** + * Restaure une schematique JSON dans [origin, origin+size). + * Les blocs "minecraft:air" sont remplaces par de l'air (efface ce qui etait la). + */ + public static void restoreRegion(String json, World world, + int originX, int originY, int originZ) { + JsonObject root = JsonParser.parseString(json).getAsJsonObject(); + JsonArray sizeArr = root.getAsJsonArray("size"); + int sizeX = sizeArr.get(0).getAsInt(); + int sizeY = sizeArr.get(1).getAsInt(); + int sizeZ = sizeArr.get(2).getAsInt(); + + JsonArray paletteArr = root.getAsJsonArray("palette"); + BlockData[] palette = new BlockData[paletteArr.size()]; + for (int i = 0; i < paletteArr.size(); i++) { + String state = paletteArr.get(i).getAsString(); + try { + palette[i] = Bukkit.createBlockData(state); + } catch (IllegalArgumentException ex) { + palette[i] = Bukkit.createBlockData(Material.AIR); + } + } + + JsonArray blocksArr = root.getAsJsonArray("blocks"); + int cursor = 0; // index dans l'ordre Y -> Z -> X + int total = sizeX * sizeY * sizeZ; + for (int i = 0; i < blocksArr.size() && cursor < total; i++) { + JsonArray pair = blocksArr.get(i).getAsJsonArray(); + int idx = pair.get(0).getAsInt(); + int len = pair.get(1).getAsInt(); + BlockData data = palette[idx]; + for (int j = 0; j < len && cursor < total; j++, cursor++) { + int x = cursor % sizeX; + int z = (cursor / sizeX) % sizeZ; + int y = cursor / (sizeX * sizeZ); + Block b = world.getBlockAt(originX + x, originY + y, originZ + z); + b.setBlockData(data, false); + } + } + + // Restauration du contenu des conteneurs (apres avoir pose les blocs). + JsonElement contEl = root.get("containers"); + if (contEl != null && contEl.isJsonArray()) { + for (JsonElement el : contEl.getAsJsonArray()) { + JsonObject obj = el.getAsJsonObject(); + int x = obj.get("x").getAsInt(); + int y = obj.get("y").getAsInt(); + int z = obj.get("z").getAsInt(); + Block b = world.getBlockAt(originX + x, originY + y, originZ + z); + BlockState bs = b.getState(false); + if (!(bs instanceof Container container)) continue; + Inventory inv = container.getInventory(); + if (inv == null) continue; + JsonArray items = obj.getAsJsonArray("items"); + for (JsonElement itemEl : items) { + JsonObject itemObj = itemEl.getAsJsonObject(); + int slot = itemObj.get("slot").getAsInt(); + String b64 = itemObj.get("data").getAsString(); + try { + byte[] bytes = Base64.getDecoder().decode(b64); + ItemStack stack = ItemStack.deserializeBytes(bytes); + if (slot >= 0 && slot < inv.getSize()) inv.setItem(slot, stack); + } catch (IllegalArgumentException ignored) { /* item invalide */ } + } + } + } + } + + public static int[] readSize(String json) { + JsonObject root = JsonParser.parseString(json).getAsJsonObject(); + JsonArray sz = root.getAsJsonArray("size"); + return new int[]{sz.get(0).getAsInt(), sz.get(1).getAsInt(), sz.get(2).getAsInt()}; + } + + public static int[] readSpawn(String json) { + JsonObject root = JsonParser.parseString(json).getAsJsonObject(); + JsonArray sp = root.getAsJsonArray("spawn"); + return new int[]{sp.get(0).getAsInt(), sp.get(1).getAsInt(), sp.get(2).getAsInt()}; + } + + /* ============================ EXPANSION ============================ */ + + /** + * Etend la schematique a la nouvelle taille (les dimensions doivent etre >= a l'ancienne). + * Les blocs ajoutes sont de l'air. Le spawn est conserve. + * Si {@code markCornerMaterial} != null, place ce materiau aux 8 coins de la nouvelle boite + * pour signaler visuellement les nouvelles limites. + */ + public static String expandSchematic(String oldJson, int newX, int newY, int newZ, + Material markCornerMaterial) { + JsonObject root = JsonParser.parseString(oldJson).getAsJsonObject(); + JsonArray oldSize = root.getAsJsonArray("size"); + int ox = oldSize.get(0).getAsInt(); + int oy = oldSize.get(1).getAsInt(); + int oz = oldSize.get(2).getAsInt(); + if (newX < ox || newY < oy || newZ < oz) { + throw new IllegalArgumentException("Les nouvelles dimensions doivent etre >= aux anciennes."); + } + + JsonArray paletteArr = root.getAsJsonArray("palette"); + List palette = new ArrayList<>(paletteArr.size()); + for (JsonElement e : paletteArr) palette.add(e.getAsString()); + if (palette.isEmpty() || !palette.get(0).equals(AIR)) { + // S'assurer que l'air est connu : si absent, l'ajouter en fin de palette. + int airIdx = palette.indexOf(AIR); + if (airIdx < 0) palette.add(AIR); + } + int airIdx = palette.indexOf(AIR); + if (airIdx < 0) airIdx = palette.size() - 1; + + // Decoder l'ancien RLE en un tableau d'indices [oy*oz*ox]. + int[] oldFlat = decodeBlocks(root.getAsJsonArray("blocks"), ox * oy * oz); + + int totalNew = newX * newY * newZ; + int[] newFlat = new int[totalNew]; + for (int i = 0; i < totalNew; i++) newFlat[i] = airIdx; + // Copier dans le nouveau plan (ordre Y -> Z -> X). + for (int y = 0; y < oy; y++) { + for (int z = 0; z < oz; z++) { + for (int x = 0; x < ox; x++) { + int oldIdx = y * (oz * ox) + z * ox + x; + int newIdx = y * (newZ * newX) + z * newX + x; + newFlat[newIdx] = oldFlat[oldIdx]; + } + } + } + + // Optionnellement marquer les 8 coins. + if (markCornerMaterial != null) { + String state = markCornerMaterial.createBlockData().getAsString(); + int markIdx = palette.indexOf(state); + if (markIdx < 0) { palette.add(state); markIdx = palette.size() - 1; } + int[][] corners = { + {0, 0, 0}, + {newX - 1, 0, 0}, + {0, 0, newZ - 1}, + {newX - 1, 0, newZ - 1}, + {0, newY - 1, 0}, + {newX - 1, newY - 1, 0}, + {0, newY - 1, newZ - 1}, + {newX - 1, newY - 1, newZ - 1} + }; + for (int[] c : corners) { + int idx = c[1] * (newZ * newX) + c[2] * newX + c[0]; + newFlat[idx] = markIdx; + } + } + + // Re-encoder en RLE. + List runs = new ArrayList<>(); + int currentIdx = -1, run = 0; + for (int v : newFlat) { + if (v == currentIdx) { run++; } + else { + if (currentIdx != -1) runs.add(new int[]{currentIdx, run}); + currentIdx = v; run = 1; + } + } + if (run > 0) runs.add(new int[]{currentIdx, run}); + + JsonObject out = new JsonObject(); + JsonArray sizeArr = new JsonArray(); sizeArr.add(newX); sizeArr.add(newY); sizeArr.add(newZ); + out.add("size", sizeArr); + out.add("spawn", root.getAsJsonArray("spawn")); // spawn conserve + JsonArray pal = new JsonArray(); + for (String p : palette) pal.add(p); + out.add("palette", pal); + JsonArray blocks = new JsonArray(); + for (int[] r : runs) { JsonArray ja = new JsonArray(); ja.add(r[0]); ja.add(r[1]); blocks.add(ja); } + out.add("blocks", blocks); + // Conserve les containers (positions invariantes car ancres au coin 0,0,0). + if (root.has("containers")) out.add("containers", root.get("containers")); + return GSON.toJson(out); + } + + /** Decode le RLE en tableau d'indices pleins. */ + private static int[] decodeBlocks(JsonArray blocksArr, int total) { + int[] out = new int[total]; + int cursor = 0; + for (JsonElement el : blocksArr) { + JsonArray pair = el.getAsJsonArray(); + int idx = pair.get(0).getAsInt(); + int len = pair.get(1).getAsInt(); + for (int j = 0; j < len && cursor < total; j++, cursor++) out[cursor] = idx; + } + return out; + } + + /* ============================ UTILITAIRES ============================ */ + + /** Vide la region (place de l'air partout). */ + public static void clearRegion(World world, + int originX, int originY, int originZ, + int sizeX, int sizeY, int sizeZ) { + BlockData air = Bukkit.createBlockData(Material.AIR); + for (int y = 0; y < sizeY; y++) { + for (int z = 0; z < sizeZ; z++) { + for (int x = 0; x < sizeX; x++) { + Block b = world.getBlockAt(originX + x, originY + y, originZ + z); + if (b.getType() != Material.AIR) b.setBlockData(air, false); + } + } + } + } + + /** True si la zone contient au moins un bloc non-air. */ + public static boolean hasCollision(World world, + int originX, int originY, int originZ, + int sizeX, int sizeY, int sizeZ) { + for (int y = 0; y < sizeY; y++) { + for (int z = 0; z < sizeZ; z++) { + for (int x = 0; x < sizeX; x++) { + Block b = world.getBlockAt(originX + x, originY + y, originZ + z); + if (b.getType() != Material.AIR) return true; + } + } + } + return false; + } + + /* ============================ DEFAULT SCHEMATIC ============================ */ + + /** + * Genere une schematique par defaut : sol plein, mur d'un bloc avec piliers aux coins, + * et bordure de verre tout autour au niveau y=1. Le reste est de l'air. + * Le spawn par defaut est centre, sur le sol +1. + */ + public static String buildDefaultSchematic(int sizeX, int sizeY, int sizeZ, + Material platform, Material edge, Material glass) { + String platformState = platform.createBlockData().getAsString(); + String edgeState = edge.createBlockData().getAsString(); + String glassState = glass.createBlockData().getAsString(); + + Map paletteIndex = new LinkedHashMap<>(); + List palette = new ArrayList<>(); + // 0 = air toujours + paletteIndex.put(AIR, 0); palette.add(AIR); + + int pIdx = paletteIndex.computeIfAbsent(platformState, s -> { palette.add(s); return palette.size() - 1; }); + int eIdx = paletteIndex.computeIfAbsent(edgeState, s -> { palette.add(s); return palette.size() - 1; }); + int gIdx = paletteIndex.computeIfAbsent(glassState, s -> { palette.add(s); return palette.size() - 1; }); + + List blocks = new ArrayList<>(); + int currentIdx = -1, run = 0; + + for (int y = 0; y < sizeY; y++) { + for (int z = 0; z < sizeZ; z++) { + for (int x = 0; x < sizeX; x++) { + int idx; + boolean isEdgeXZ = (x == 0 || x == sizeX - 1) && (z == 0 || z == sizeZ - 1); + boolean onBorder = (x == 0 || x == sizeX - 1 || z == 0 || z == sizeZ - 1); + if (y == 0) { + idx = pIdx; + } else if (y == 1 && onBorder) { + idx = isEdgeXZ ? eIdx : gIdx; + } else if (y == sizeY - 1 && isEdgeXZ) { + idx = eIdx; + } else { + idx = 0; + } + if (idx == currentIdx) { + run++; + } else { + if (currentIdx != -1) blocks.add(new int[]{currentIdx, run}); + currentIdx = idx; + run = 1; + } + } + } + } + if (run > 0) blocks.add(new int[]{currentIdx, run}); + + JsonObject root = new JsonObject(); + JsonArray sizeArr = new JsonArray(); + sizeArr.add(sizeX); sizeArr.add(sizeY); sizeArr.add(sizeZ); + root.add("size", sizeArr); + + JsonArray spawnArr = new JsonArray(); + spawnArr.add(sizeX / 2); spawnArr.add(1); spawnArr.add(sizeZ / 2); + root.add("spawn", spawnArr); + + JsonArray paletteArr = new JsonArray(); + for (String p : palette) paletteArr.add(p); + root.add("palette", paletteArr); + + JsonArray blocksArr = new JsonArray(); + for (int[] run2 : blocks) { + JsonArray ja = new JsonArray(); + ja.add(run2[0]); + ja.add(run2[1]); + blocksArr.add(ja); + } + root.add("blocks", blocksArr); + + return GSON.toJson(root); + } +} diff --git a/src/main/java/com/spaceshipproject/ShipSizing.java b/src/main/java/com/spaceshipproject/ShipSizing.java new file mode 100644 index 0000000..83e1c8e --- /dev/null +++ b/src/main/java/com/spaceshipproject/ShipSizing.java @@ -0,0 +1,39 @@ +package com.spaceshipproject; + +/** + * Calcule la taille effective d'un spaceship en fonction de son niveau. + * + * Regle : + * - level 1 = taille de creation + * - chaque level-up alterne +1 sur x puis +1 sur z (x en premier) + * - tous les 4 level-ups : +1 sur y + * - les valeurs sont plafonnees a la taille maximale configuree. + */ +public final class ShipSizing { + + private ShipSizing() {} + + /** Renvoie [sizeX, sizeY, sizeZ] pour le niveau donne. */ + public static int[] sizeForLevel(ConfigManager cfg, int level) { + if (level < 1) level = 1; + int additions = level - 1; + int xAdds = (additions + 1) / 2; // ceil(additions / 2) + int zAdds = additions / 2; + int yAdds = additions / 4; + + int sx = clamp(cfg.defaultSizeX() + xAdds, cfg.defaultSizeX(), cfg.maxSizeX()); + int sy = clamp(cfg.defaultSizeY() + yAdds, cfg.defaultSizeY(), cfg.maxSizeY()); + int sz = clamp(cfg.defaultSizeZ() + zAdds, cfg.defaultSizeZ(), cfg.maxSizeZ()); + return new int[]{sx, sy, sz}; + } + + /** Niveau a partir duquel toutes les dimensions atteignent leur max. */ + public static int maxUsefulLevel(ConfigManager cfg) { + int xLevels = (cfg.maxSizeX() - cfg.defaultSizeX()) * 2; // x = ceil(adds/2) + int zLevels = (cfg.maxSizeZ() - cfg.defaultSizeZ()) * 2 + 1; // z = floor(adds/2) + int yLevels = (cfg.maxSizeY() - cfg.defaultSizeY()) * 4; + return 1 + Math.max(Math.max(xLevels, zLevels), yLevels); + } + + private static int clamp(int v, int min, int max) { return Math.max(min, Math.min(max, v)); } +} diff --git a/src/main/java/com/spaceshipproject/SpaceJumpGUI.java b/src/main/java/com/spaceshipproject/SpaceJumpGUI.java new file mode 100644 index 0000000..68db6c6 --- /dev/null +++ b/src/main/java/com/spaceshipproject/SpaceJumpGUI.java @@ -0,0 +1,204 @@ +package com.spaceshipproject; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +/** + * GUI "Saut Spatial". + * + * Layout (36 slots) : + * Slot 4 : info (niveau, max, cout courant) + * Slots 11..16 : selection de direction (X-, X+, Z-, Z+, Y-, Y+) + * Slots 19..25 : ajustement de distance (-50 -10 -1 [val] +1 +10 +50) + * Slot 31 : confirmer + * Slot 35 : annuler + */ +public class SpaceJumpGUI implements SpaceshipGuiPanel { + + private static final int SIZE = 36; + private static final int SLOT_INFO = 4; + private static final int SLOT_X_MINUS = 11; + private static final int SLOT_X_PLUS = 12; + private static final int SLOT_Z_MINUS = 13; + private static final int SLOT_Z_PLUS = 14; + private static final int SLOT_Y_MINUS = 15; + private static final int SLOT_Y_PLUS = 16; + private static final int SLOT_M50 = 19, SLOT_M10 = 20, SLOT_M1 = 21; + private static final int SLOT_DIST = 22; + private static final int SLOT_P1 = 23, SLOT_P10 = 24, SLOT_P50 = 25; + private static final int SLOT_CONFIRM = 31; + private static final int SLOT_CANCEL = 35; + + private final SpaceShipProject plugin; + private final Player player; + private final Spaceship ship; + private final SpaceshipGUIHolder holder; + private Inventory inv; + + private char axis = 'X'; + private int signedDistance = 10; + + public SpaceJumpGUI(SpaceShipProject plugin, Player player, Spaceship ship) { + this.plugin = plugin; + this.player = player; + this.ship = ship; + this.holder = new SpaceshipGUIHolder(this); + } + + public void open() { + String title = ConfigManager.color("&8\u27FC &bSaut Spatial &8\u2014 &f" + ship.getName()); + inv = Bukkit.createInventory(holder, SIZE, title); + holder.setInventory(inv); + render(); + SpaceshipGUIListener.registerOpenJump(player, this); + player.openInventory(inv); + } + + public Spaceship getShip() { return ship; } + + private void render() { + for (int i = 0; i < SIZE; i++) inv.setItem(i, filler()); + + int max = plugin.getSpaceshipManager().spaceJumpMax(ship.getLevel()); + if (Math.abs(signedDistance) > max) { + signedDistance = (signedDistance < 0 ? -1 : 1) * max; + } + if (Math.abs(signedDistance) < 1) signedDistance = 1; + + double cost = plugin.getSpaceshipManager().spaceJumpCost(Math.abs(signedDistance)); + EconomyHook eco = plugin.getEconomy(); + String balance = eco.isReady() ? eco.format(eco.getBalance(player.getUniqueId())) : "-"; + String costStr = eco.isReady() ? eco.format(cost) : String.valueOf(cost); + + inv.setItem(SLOT_INFO, item(Material.ENDER_EYE, + "&b&l\u2730 Saut Spatial", + List.of( + "&7Spaceship : &f" + ship.getName(), + "&7Niveau : &e" + ship.getLevel() + " &7(max &f" + max + " &7blocs)", + "&7Direction : &b" + dirLabel(), + "&7Distance : &f" + Math.abs(signedDistance), + "&7Cout : &6" + costStr, + "&7Solde : &6" + balance + ))); + + inv.setItem(SLOT_X_MINUS, dirButton('X', -1, "&aX-", "Ouest")); + inv.setItem(SLOT_X_PLUS, dirButton('X', 1, "&aX+", "Est")); + inv.setItem(SLOT_Z_MINUS, dirButton('Z', -1, "&aZ-", "Nord")); + inv.setItem(SLOT_Z_PLUS, dirButton('Z', 1, "&aZ+", "Sud")); + inv.setItem(SLOT_Y_MINUS, dirButton('Y', -1, "&aY-", "Bas")); + inv.setItem(SLOT_Y_PLUS, dirButton('Y', 1, "&aY+", "Haut")); + + inv.setItem(SLOT_M50, item(Material.RED_CONCRETE, "&c-50", List.of("&7Reduit la distance."))); + inv.setItem(SLOT_M10, item(Material.RED_CONCRETE, "&c-10", List.of("&7Reduit la distance."))); + inv.setItem(SLOT_M1, item(Material.RED_CONCRETE, "&c-1", List.of("&7Reduit la distance."))); + inv.setItem(SLOT_DIST, item(Material.PAPER, + "&f&lDistance : " + Math.abs(signedDistance), + List.of("&7Cout : &6" + costStr, + "&7Max : &f" + max, + "&7Direction : &b" + dirLabel()))); + inv.setItem(SLOT_P1, item(Material.LIME_CONCRETE, "&a+1", List.of("&7Augmente la distance."))); + inv.setItem(SLOT_P10, item(Material.LIME_CONCRETE, "&a+10", List.of("&7Augmente la distance."))); + inv.setItem(SLOT_P50, item(Material.LIME_CONCRETE, "&a+50", List.of("&7Augmente la distance."))); + + boolean canAfford = !eco.isReady() || eco.has(player.getUniqueId(), cost); + inv.setItem(SLOT_CONFIRM, item( + canAfford ? Material.EMERALD_BLOCK : Material.REDSTONE_BLOCK, + canAfford ? "&a&l\u2714 CONFIRMER" : "&c\u2718 Fonds insuffisants", + List.of( + "&7Direction : &b" + dirLabel(), + "&7Distance : &f" + Math.abs(signedDistance), + "&7Cout : &6" + costStr, + "", + canAfford ? "&aClic pour lancer le saut." : "&cVotre solde est trop bas." + ) + )); + inv.setItem(SLOT_CANCEL, item(Material.BARRIER, "&c\u2718 Annuler", List.of("&7Ferme la fenetre."))); + } + + private String dirLabel() { + return switch (axis) { + case 'X' -> signedDistance >= 0 ? "Est (X+)" : "Ouest (X-)"; + case 'Y' -> signedDistance >= 0 ? "Haut (Y+)" : "Bas (Y-)"; + case 'Z' -> signedDistance >= 0 ? "Sud (Z+)" : "Nord (Z-)"; + default -> "?"; + }; + } + + private ItemStack dirButton(char a, int sign, String name, String label) { + boolean selected = axis == a && Integer.signum(signedDistance) == sign; + return item(selected ? Material.LIME_STAINED_GLASS_PANE : Material.GRAY_STAINED_GLASS_PANE, + (selected ? "&a&l" : "&7") + name + " &7(" + label + ")", + List.of(selected ? "&aSelectionne" : "&7Clic pour selectionner")); + } + + private ItemStack filler() { + ItemStack stack = new ItemStack(Material.BLACK_STAINED_GLASS_PANE); + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { meta.setDisplayName(" "); stack.setItemMeta(meta); } + return stack; + } + + private ItemStack item(Material mat, String name, List lore) { + ItemStack stack = new ItemStack(mat); + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + meta.setDisplayName(ConfigManager.color(name)); + List colored = new ArrayList<>(lore.size()); + for (String s : lore) colored.add(ConfigManager.color(s)); + meta.setLore(colored); + stack.setItemMeta(meta); + } + return stack; + } + + public void handleClick(InventoryClickEvent event) { + int slot = event.getRawSlot(); + switch (slot) { + case SLOT_X_MINUS -> { axis = 'X'; signedDistance = -Math.abs(currentDist()); render(); } + case SLOT_X_PLUS -> { axis = 'X'; signedDistance = Math.abs(currentDist()); render(); } + case SLOT_Z_MINUS -> { axis = 'Z'; signedDistance = -Math.abs(currentDist()); render(); } + case SLOT_Z_PLUS -> { axis = 'Z'; signedDistance = Math.abs(currentDist()); render(); } + case SLOT_Y_MINUS -> { axis = 'Y'; signedDistance = -Math.abs(currentDist()); render(); } + case SLOT_Y_PLUS -> { axis = 'Y'; signedDistance = Math.abs(currentDist()); render(); } + case SLOT_M50 -> { adjust(-50, event.getClick()); } + case SLOT_M10 -> { adjust(-10, event.getClick()); } + case SLOT_M1 -> { adjust(-1, event.getClick()); } + case SLOT_P1 -> { adjust( 1, event.getClick()); } + case SLOT_P10 -> { adjust( 10, event.getClick()); } + case SLOT_P50 -> { adjust( 50, event.getClick()); } + case SLOT_CONFIRM -> { + int sign = Integer.signum(signedDistance); + if (sign == 0) sign = 1; + int abs = Math.abs(signedDistance); + player.closeInventory(); + plugin.getSpaceshipManager().spaceJump(player, ship, axis, sign * abs); + } + case SLOT_CANCEL -> player.closeInventory(); + default -> { /* ignore */ } + } + } + + private int currentDist() { int d = Math.abs(signedDistance); return d == 0 ? 1 : d; } + + private void adjust(int delta, ClickType click) { + // Shift = x10 pour aller plus vite. + if (click == ClickType.SHIFT_LEFT || click == ClickType.SHIFT_RIGHT) delta *= 10; + int sign = Integer.signum(signedDistance); + if (sign == 0) sign = 1; + int abs = Math.abs(signedDistance) + delta; + if (abs < 1) abs = 1; + int max = plugin.getSpaceshipManager().spaceJumpMax(ship.getLevel()); + if (abs > max) abs = max; + signedDistance = sign * abs; + render(); + } +} diff --git a/src/main/java/com/spaceshipproject/SpaceShipAdminCommand.java b/src/main/java/com/spaceshipproject/SpaceShipAdminCommand.java new file mode 100644 index 0000000..a5f99dc --- /dev/null +++ b/src/main/java/com/spaceshipproject/SpaceShipAdminCommand.java @@ -0,0 +1,282 @@ +package com.spaceshipproject; + +import org.bukkit.OfflinePlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Commande admin /spaceshipadmin (/ssa). + * + * Sous-commandes : + * - reload : recharge config + modeles + * - list [joueur] : liste les ships (tous ou d'un joueur) + * - delete : supprime un ship n'importe lequel + * - addmoney : donne de la money a un ship + * - setlevel : force le niveau d'un ship + * - setname : renomme un ship + * - setconfig : modifie config.yml + * - getconfig : lit config.yml + * - loadmodel [nom] : place le modele dans le monde pour edition + * - savemodel : sauvegarde la session active + * - cancelmodel : annule la session active + */ +public class SpaceShipAdminCommand implements CommandExecutor, TabCompleter { + + private final SpaceShipProject plugin; + + public SpaceShipAdminCommand(SpaceShipProject plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("spaceship.admin")) { + sender.sendMessage(plugin.getConfigManager().msgNoPerm()); + return true; + } + if (args.length == 0 || args[0].equalsIgnoreCase("help")) { + help(sender); + return true; + } + switch (args[0].toLowerCase()) { + case "reload" -> handleReload(sender); + case "list" -> handleList(sender, args); + case "delete" -> handleDelete(sender, args); + case "addmoney" -> handleAddMoney(sender, args); + case "setlevel" -> handleSetLevel(sender, args); + case "setname" -> handleSetName(sender, args); + case "setconfig" -> handleSetConfig(sender, args); + case "getconfig" -> handleGetConfig(sender, args); + case "loadmodel" -> handleLoadModel(sender, args); + case "savemodel" -> handleSaveModel(sender); + case "cancelmodel" -> handleCancelModel(sender); + default -> sender.sendMessage(ConfigManager.color("&cSous-commande inconnue. /ssa help")); + } + return true; + } + + private void help(CommandSender s) { + s.sendMessage(ConfigManager.color("&c&l=== SpaceShipAdmin ===")); + s.sendMessage(ConfigManager.color("&7/ssa reload")); + s.sendMessage(ConfigManager.color("&7/ssa list [joueur]")); + s.sendMessage(ConfigManager.color("&7/ssa delete ")); + s.sendMessage(ConfigManager.color("&7/ssa addmoney &8(via Vault)")); + s.sendMessage(ConfigManager.color("&7/ssa setlevel ")); + s.sendMessage(ConfigManager.color("&7/ssa setname ")); + s.sendMessage(ConfigManager.color("&7/ssa setconfig ")); + s.sendMessage(ConfigManager.color("&7/ssa getconfig ")); + s.sendMessage(ConfigManager.color("&7/ssa loadmodel [nom]")); + s.sendMessage(ConfigManager.color("&7/ssa savemodel")); + s.sendMessage(ConfigManager.color("&7/ssa cancelmodel")); + } + + /* ============================ RELOAD ============================ */ + + private void handleReload(CommandSender s) { + plugin.getConfigManager().reload(); + plugin.getModelManager().reload(); + s.sendMessage(ConfigManager.color("&aConfig et modeles recharges.")); + } + + /* ============================ LIST ============================ */ + + private void handleList(CommandSender s, String[] args) { + if (args.length >= 2) { + OfflinePlayer p = resolveOfflinePlayer(args[1]); + if (p == null) { s.sendMessage(ConfigManager.color("&cJoueur introuvable.")); return; } + List ships = plugin.getDatabaseManager().getSpaceshipsByOwner(p.getUniqueId()); + s.sendMessage(ConfigManager.color("&bShips de &f" + (p.getName() != null ? p.getName() : args[1]) + + " &b: &f" + ships.size())); + for (Spaceship ship : ships) { + s.sendMessage(ConfigManager.color("&7- &f#" + ship.getId() + " &b" + ship.getName() + + " &7Lv&f" + ship.getLevel() + " &7Taille&f " + ship.getSizeX() + "x" + ship.getSizeY() + "x" + ship.getSizeZ())); + } + } else { + // Liste de tous les loaded actuels + List loaded = plugin.getDatabaseManager().getAllLoaded(); + s.sendMessage(ConfigManager.color("&bSpaceships actuellement charges : &f" + loaded.size())); + for (LoadedSpaceship l : loaded) { + Spaceship ship = plugin.getDatabaseManager().getSpaceship(l.getSpaceshipId()); + if (ship == null) continue; + s.sendMessage(ConfigManager.color("&7- &f#" + ship.getId() + " &b" + ship.getName() + + " &7@ " + l.getWorldName() + " " + l.getOriginX() + "," + l.getOriginY() + "," + l.getOriginZ())); + } + } + } + + /* ============================ DELETE ============================ */ + + private void handleDelete(CommandSender s, String[] args) { + if (args.length < 2) { s.sendMessage(ConfigManager.color("&cUsage : /ssa delete ")); return; } + long id; + try { id = Long.parseLong(args[1]); } catch (NumberFormatException e) { + s.sendMessage(ConfigManager.color("&cID invalide.")); return; + } + Spaceship ship = plugin.getDatabaseManager().getSpaceship(id); + if (ship == null) { s.sendMessage(ConfigManager.color("&cShip introuvable.")); return; } + plugin.getSpaceshipManager().delete(ship); + s.sendMessage(ConfigManager.color("&aShip #" + id + " supprime.")); + } + + /* ============================ MONEY / LEVEL / NAME ============================ */ + + private void handleAddMoney(CommandSender s, String[] args) { + if (args.length < 3) { s.sendMessage(ConfigManager.color("&cUsage : /ssa addmoney ")); return; } + OfflinePlayer p = resolveOfflinePlayer(args[1]); + if (p == null) { s.sendMessage(ConfigManager.color("&cJoueur introuvable.")); return; } + double amount; + try { amount = Double.parseDouble(args[2]); } catch (NumberFormatException e) { + s.sendMessage(ConfigManager.color("&cMontant invalide.")); return; + } + EconomyHook eco = plugin.getEconomy(); + if (!eco.isReady()) { s.sendMessage(plugin.getConfigManager().msgVaultUnavailable()); return; } + if (!eco.deposit(p.getUniqueId(), amount)) { + s.sendMessage(ConfigManager.color("&cEchec du depot.")); + return; + } + s.sendMessage(ConfigManager.color("&a+" + eco.format(amount) + " depose sur le solde Vault de " + + (p.getName() != null ? p.getName() : args[1]) + ".")); + } + + private void handleSetLevel(CommandSender s, String[] args) { + if (args.length < 4) { s.sendMessage(ConfigManager.color("&cUsage : /ssa setlevel ")); return; } + Spaceship ship = resolveShip(s, args[1], args[2]); + if (ship == null) return; + int level; + try { level = Math.max(1, Integer.parseInt(args[3])); } catch (NumberFormatException e) { + s.sendMessage(ConfigManager.color("&cNiveau invalide.")); return; + } + ship.setLevel(level); + plugin.getDatabaseManager().updateSpaceship(ship); + s.sendMessage(ConfigManager.color("&aNiveau de #" + ship.getId() + " : &e" + level)); + } + + private void handleSetName(CommandSender s, String[] args) { + if (args.length < 4) { s.sendMessage(ConfigManager.color("&cUsage : /ssa setname ")); return; } + Spaceship ship = resolveShip(s, args[1], args[2]); + if (ship == null) return; + StringBuilder sb = new StringBuilder(); + for (int i = 3; i < args.length; i++) { + if (i > 3) sb.append(' '); + sb.append(args[i]); + } + String name = sb.toString(); + if (name.length() > 32) name = name.substring(0, 32); + ship.setName(name); + plugin.getDatabaseManager().updateSpaceship(ship); + s.sendMessage(ConfigManager.color("&aShip renomme : &b" + name)); + } + + private Spaceship resolveShip(CommandSender s, String playerName, String idStr) { + OfflinePlayer p = resolveOfflinePlayer(playerName); + if (p == null) { s.sendMessage(ConfigManager.color("&cJoueur introuvable.")); return null; } + long id; + try { id = Long.parseLong(idStr); } catch (NumberFormatException e) { + s.sendMessage(ConfigManager.color("&cID invalide.")); return null; + } + Spaceship ship = plugin.getDatabaseManager().getSpaceship(id); + if (ship == null) { s.sendMessage(ConfigManager.color("&cShip introuvable.")); return null; } + if (!ship.getOwnerUuid().equals(p.getUniqueId())) { + s.sendMessage(ConfigManager.color("&cLe ship #" + id + " n'appartient pas a " + playerName + ".")); return null; + } + return ship; + } + + /* ============================ CONFIG ============================ */ + + private void handleSetConfig(CommandSender s, String[] args) { + if (args.length < 3) { s.sendMessage(ConfigManager.color("&cUsage : /ssa setconfig ")); return; } + String path = args[1]; + StringBuilder valueB = new StringBuilder(); + for (int i = 2; i < args.length; i++) { if (i > 2) valueB.append(' '); valueB.append(args[i]); } + Object parsed = parseValue(valueB.toString()); + FileConfiguration cfg = plugin.getConfig(); + cfg.set(path, parsed); + plugin.saveConfig(); + plugin.getConfigManager().reload(); + s.sendMessage(ConfigManager.color("&aConfig &f" + path + " &asetee a &f" + parsed)); + } + + private void handleGetConfig(CommandSender s, String[] args) { + if (args.length < 2) { s.sendMessage(ConfigManager.color("&cUsage : /ssa getconfig ")); return; } + Object v = plugin.getConfig().get(args[1]); + s.sendMessage(ConfigManager.color("&b" + args[1] + " &7= &f" + v)); + } + + private Object parseValue(String s) { + if (s.equalsIgnoreCase("true") || s.equalsIgnoreCase("false")) return Boolean.parseBoolean(s); + try { return Integer.parseInt(s); } catch (NumberFormatException ignored) {} + try { return Double.parseDouble(s); } catch (NumberFormatException ignored) {} + return s; + } + + /* ============================ MODEL ============================ */ + + private void handleLoadModel(CommandSender s, String[] args) { + if (!(s instanceof Player player)) { s.sendMessage(ConfigManager.color("&cCommande joueur uniquement.")); return; } + String modelName = args.length >= 2 ? args[1] : "default"; + plugin.getModelManager().loadForEdit(player, modelName); + } + + private void handleSaveModel(CommandSender s) { + if (!(s instanceof Player player)) { s.sendMessage(ConfigManager.color("&cCommande joueur uniquement.")); return; } + plugin.getModelManager().saveEdit(player); + } + + private void handleCancelModel(CommandSender s) { + if (!(s instanceof Player player)) { s.sendMessage(ConfigManager.color("&cCommande joueur uniquement.")); return; } + plugin.getModelManager().cancelEdit(player); + } + + /* ============================ HELPERS ============================ */ + + private OfflinePlayer resolveOfflinePlayer(String name) { + Player p = plugin.getServer().getPlayer(name); + if (p != null) return p; + return plugin.getServer().getOfflinePlayerIfCached(name); + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (!sender.hasPermission("spaceship.admin")) return new ArrayList<>(); + if (args.length == 1) { + return Arrays.asList("help", "reload", "list", "delete", "addmoney", "setlevel", + "setname", "setconfig", "getconfig", "loadmodel", "savemodel", "cancelmodel"); + } + if (args.length == 2 && (args[0].equalsIgnoreCase("list") + || args[0].equalsIgnoreCase("addmoney") + || args[0].equalsIgnoreCase("setlevel") + || args[0].equalsIgnoreCase("setname"))) { + List out = new ArrayList<>(); + for (Player p : plugin.getServer().getOnlinePlayers()) out.add(p.getName()); + return out; + } + if (args.length == 3 && (args[0].equalsIgnoreCase("setlevel") + || args[0].equalsIgnoreCase("setname"))) { + Player target = plugin.getServer().getPlayer(args[1]); + if (target != null) { + List out = new ArrayList<>(); + for (Spaceship s : plugin.getDatabaseManager().getSpaceshipsByOwner(target.getUniqueId())) { + out.add(String.valueOf(s.getId())); + } + return out; + } + } + if (args.length == 2 && args[0].equalsIgnoreCase("loadmodel")) { + return Arrays.asList("default"); + } + if (args.length == 2 && args[0].equalsIgnoreCase("setconfig")) { + return Arrays.asList("max-ships-per-player", "default-size.x", "default-size.y", "default-size.z", + "summon-offset-y", "safety.check-collision", "safety.min-y-above-player"); + } + return new ArrayList<>(); + } +} diff --git a/src/main/java/com/spaceshipproject/SpaceShipCommand.java b/src/main/java/com/spaceshipproject/SpaceShipCommand.java new file mode 100644 index 0000000..684bfee --- /dev/null +++ b/src/main/java/com/spaceshipproject/SpaceShipCommand.java @@ -0,0 +1,342 @@ +package com.spaceshipproject; + +import org.bukkit.OfflinePlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; + +/** + * Commandes joueur de SpaceShipProject : + * - /spaceship help + * - /spaceship create : cree un nouveau spaceship (boussole donnee) + * - /spaceship give : recupere une telecommande perdue (proprietaire requis) + * - /spaceship list [joueur] : liste les ships + * - /spaceship info [joueur] : infos detaillees + * - /spaceship delete : supprime un de ses ships (boussole retiree) + * - /spaceship remote : alias de give pour soi-meme + * - /spaceship reload : recharge la config + */ +public class SpaceShipCommand implements CommandExecutor, TabCompleter { + + private final SpaceShipProject plugin; + + public SpaceShipCommand(SpaceShipProject plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (args.length == 0 || args[0].equalsIgnoreCase("help")) { + help(sender); + return true; + } + + switch (args[0].toLowerCase()) { + case "create" -> handleCreate(sender); + case "give" -> handleGive(sender, args); + case "remote" -> handleRemoteSelf(sender, args); + case "list" -> handleList(sender, args); + case "info" -> handleInfo(sender, args); + case "delete" -> handleDelete(sender, args); + case "reload" -> handleReload(sender); + default -> sender.sendMessage(ConfigManager.color("&cCommande inconnue. /spaceship help")); + } + return true; + } + + private void help(CommandSender s) { + s.sendMessage(ConfigManager.color("&b&l=== SpaceShipProject ===")); + s.sendMessage(ConfigManager.color("&7/spaceship create &8- creer un nouveau spaceship")); + s.sendMessage(ConfigManager.color("&7/spaceship give &8- recuperer une telecommande perdue")); + s.sendMessage(ConfigManager.color("&7/spaceship remote &8- recuperer sa propre telecommande")); + s.sendMessage(ConfigManager.color("&7/spaceship list [joueur] &8- lister les spaceships")); + s.sendMessage(ConfigManager.color("&7/spaceship info [joueur] &8- infos detaillees")); + s.sendMessage(ConfigManager.color("&7/spaceship delete &8- supprimer un spaceship")); + s.sendMessage(ConfigManager.color("&7/spaceship reload &8- recharger la config")); + s.sendMessage(ConfigManager.color("&7Telecommande : ")); + s.sendMessage(ConfigManager.color("&7 &eClic gauche &8: invoquer / decharger")); + s.sendMessage(ConfigManager.color("&7 &eClic droit &8: monter / redescendre")); + s.sendMessage(ConfigManager.color("&7 &eShift + droit &8: parametres")); + } + + /* ============================ CREATE ============================ */ + + private void handleCreate(CommandSender sender) { + if (!(sender instanceof Player player)) { + sender.sendMessage(ConfigManager.color("&cCommande joueur uniquement.")); + return; + } + if (!player.hasPermission("spaceship.create")) { + player.sendMessage(plugin.getConfigManager().msgNoPerm()); + return; + } + long id = plugin.getSpaceshipManager().createNewSpaceship(player); + if (id < 0) return; // message deja envoye (quota) + Spaceship ship = plugin.getDatabaseManager().getSpaceship(id); + if (ship == null) { + sender.sendMessage(ConfigManager.color("&cErreur interne : ship introuvable apres creation.")); + return; + } + giveRemote(player, ship); + player.sendMessage(plugin.getConfigManager().msgGiven(ship.getName())); + } + + /* ============================ GIVE (recover) ============================ */ + + private void handleGive(CommandSender sender, String[] args) { + if (args.length < 3) { + sender.sendMessage(ConfigManager.color("&cUsage : /spaceship give ")); + return; + } + if (!sender.hasPermission("spaceship.give")) { + sender.sendMessage(plugin.getConfigManager().msgNoPerm()); + return; + } + Player target = plugin.getServer().getPlayer(args[1]); + if (target == null) { + sender.sendMessage(ConfigManager.color("&cJoueur introuvable (doit etre en ligne).")); + return; + } + long id; + try { id = Long.parseLong(args[2]); } + catch (NumberFormatException e) { + sender.sendMessage(ConfigManager.color("&cID invalide.")); + return; + } + Spaceship ship = plugin.getDatabaseManager().getSpaceship(id); + if (ship == null) { + sender.sendMessage(ConfigManager.color("&cSpaceship introuvable.")); + return; + } + if (!ship.getOwnerUuid().equals(target.getUniqueId()) + && !sender.hasPermission("spaceship.admin")) { + sender.sendMessage(ConfigManager.color("&cLe spaceship #" + id + " n'appartient pas a " + target.getName() + ".")); + return; + } + + // Nettoie d'eventuels doublons et compte ce qui reste. + plugin.getSpaceshipManager().sanitizePlayerRemotes(target); + int existing = countRemotes(target, id); + if (existing >= 1) { + sender.sendMessage(ConfigManager.color("&7" + target.getName() + + " possede deja la telecommande #" + id + " (rien a faire).")); + return; + } + giveRemote(target, ship); + sender.sendMessage(ConfigManager.color("&aTelecommande #" + id + " donnee a " + target.getName() + ".")); + if (target != sender) { + target.sendMessage(plugin.getConfigManager().prefix() + + ConfigManager.color("&aTelecommande &b" + ship.getName() + " &arendue.")); + } + } + + /** Version courte pour soi-meme : /spaceship remote . */ + private void handleRemoteSelf(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage(ConfigManager.color("&cCommande joueur uniquement.")); + return; + } + if (args.length < 2) { + sender.sendMessage(ConfigManager.color("&cUsage : /spaceship remote ")); + return; + } + // Reutilise la logique de give pour soi-meme. + handleGive(sender, new String[]{"give", player.getName(), args[1]}); + } + + private int countRemotes(Player p, long id) { + int n = 0; + for (ItemStack it : p.getInventory().getContents()) { + if (it == null || !plugin.getRemoteManager().isRemote(it)) continue; + long sid = plugin.getRemoteManager().getShipId(it); + if (sid == id) n++; + } + return n; + } + + private void giveRemote(Player target, Spaceship ship) { + ItemStack remote = plugin.getRemoteManager().buildRemote(ship); + HashMap remaining = target.getInventory().addItem(remote); + if (!remaining.isEmpty()) { + target.getWorld().dropItemNaturally(target.getLocation(), remote); + } + } + + /* ============================ LIST ============================ */ + + private void handleList(CommandSender sender, String[] args) { + UUID target; String name; + if (args.length >= 2) { + if (!sender.hasPermission("spaceship.admin")) { + sender.sendMessage(plugin.getConfigManager().msgNoPerm()); + return; + } + OfflinePlayer p = resolveOfflinePlayer(args[1]); + if (p == null) { + sender.sendMessage(ConfigManager.color("&cJoueur introuvable.")); + return; + } + target = p.getUniqueId(); + name = p.getName() != null ? p.getName() : args[1]; + } else { + if (!(sender instanceof Player p)) { + sender.sendMessage(ConfigManager.color("&cSpecifiez un joueur.")); + return; + } + target = p.getUniqueId(); + name = p.getName(); + } + + List ships = plugin.getDatabaseManager().getSpaceshipsByOwner(target); + sender.sendMessage(ConfigManager.color("&bSpaceships de &f" + name + " &b: &f" + ships.size())); + for (Spaceship s : ships) { + boolean loaded = plugin.getSpaceshipManager().isLoaded(s.getId()); + sender.sendMessage(ConfigManager.color("&7- &f#" + s.getId() + " &b" + s.getName() + + " &7Lv&f" + s.getLevel() + " &7" + s.getSizeX() + "x" + s.getSizeY() + "x" + s.getSizeZ() + + " &7" + (loaded ? "&aCHARGE" : "&8decharge"))); + } + } + + /* ============================ INFO ============================ */ + + private void handleInfo(CommandSender sender, String[] args) { + UUID target; String name; + if (args.length >= 2) { + if (!sender.hasPermission("spaceship.info.other") && !sender.hasPermission("spaceship.admin")) { + sender.sendMessage(plugin.getConfigManager().msgNoPerm()); + return; + } + OfflinePlayer p = resolveOfflinePlayer(args[1]); + if (p == null) { + sender.sendMessage(ConfigManager.color("&cJoueur introuvable.")); + return; + } + target = p.getUniqueId(); + name = p.getName() != null ? p.getName() : args[1]; + } else { + if (!(sender instanceof Player p)) { + sender.sendMessage(ConfigManager.color("&cSpecifiez un joueur.")); + return; + } + target = p.getUniqueId(); + name = p.getName(); + } + + List ships = plugin.getDatabaseManager().getSpaceshipsByOwner(target); + EconomyHook eco = plugin.getEconomy(); + double balance = eco.isReady() ? eco.getBalance(target) : 0; + sender.sendMessage(ConfigManager.color("&b&l== Infos spaceships de &f" + name + " &b&l(" + ships.size() + ") ==")); + sender.sendMessage(ConfigManager.color("&7Solde Vault : &6" + (eco.isReady() ? eco.format(balance) : "-"))); + for (Spaceship s : ships) { + LoadedSpaceship loaded = plugin.getSpaceshipManager().getLoaded(s.getId()); + String status = loaded != null + ? "&aCHARGE &7@ " + loaded.getWorldName() + " " + loaded.getOriginX() + "," + loaded.getOriginY() + "," + loaded.getOriginZ() + : "&8decharge"; + double cost = plugin.getSpaceshipManager().upgradeCost(s.getLevel()); + int[] nextSize = ShipSizing.sizeForLevel(plugin.getConfigManager(), s.getLevel() + 1); + int jumpMax = plugin.getSpaceshipManager().spaceJumpMax(s.getLevel()); + sender.sendMessage(ConfigManager.color("&7-------------------------")); + sender.sendMessage(ConfigManager.color("&b#" + s.getId() + " &f" + s.getName())); + sender.sendMessage(ConfigManager.color(" &7Statut : " + status)); + sender.sendMessage(ConfigManager.color(" &7Niveau : &e" + s.getLevel() + + " &7(upgrade : &6" + (eco.isReady() ? eco.format(cost) : cost) + "&7)")); + sender.sendMessage(ConfigManager.color(" &7Taille : &f" + s.getSizeX() + "x" + s.getSizeY() + "x" + s.getSizeZ() + + " &7(prochaine : &f" + nextSize[0] + "x" + nextSize[1] + "x" + nextSize[2] + "&7)")); + sender.sendMessage(ConfigManager.color(" &7Saut max : &f" + jumpMax + " &7blocs")); + sender.sendMessage(ConfigManager.color(" &7Spawn (rel.) : &f" + s.getSpawnX() + "," + s.getSpawnY() + "," + s.getSpawnZ())); + } + if (ships.isEmpty()) { + sender.sendMessage(ConfigManager.color("&7Aucun spaceship.")); + } + } + + /* ============================ DELETE ============================ */ + + private void handleDelete(CommandSender sender, String[] args) { + if (args.length < 2) { + sender.sendMessage(ConfigManager.color("&cUsage : /spaceship delete ")); + return; + } + long id; + try { id = Long.parseLong(args[1]); } + catch (NumberFormatException e) { + sender.sendMessage(ConfigManager.color("&cID invalide.")); + return; + } + Spaceship ship = plugin.getDatabaseManager().getSpaceship(id); + if (ship == null) { + sender.sendMessage(ConfigManager.color("&cSpaceship introuvable.")); + return; + } + if (!sender.hasPermission("spaceship.admin") + && (!(sender instanceof Player p) || !ship.getOwnerUuid().equals(p.getUniqueId()))) { + sender.sendMessage(plugin.getConfigManager().msgNotOwner()); + return; + } + plugin.getSpaceshipManager().delete(ship); + sender.sendMessage(plugin.getConfigManager().msgDeleted()); + } + + /* ============================ RELOAD ============================ */ + + private void handleReload(CommandSender sender) { + if (!sender.hasPermission("spaceship.reload")) { + sender.sendMessage(plugin.getConfigManager().msgNoPerm()); + return; + } + plugin.getConfigManager().reload(); + plugin.getModelManager().reload(); + sender.sendMessage(ConfigManager.color("&aConfiguration rechargee.")); + } + + /* ============================ HELPERS ============================ */ + + private OfflinePlayer resolveOfflinePlayer(String name) { + Player p = plugin.getServer().getPlayer(name); + if (p != null) return p; + OfflinePlayer off = plugin.getServer().getOfflinePlayerIfCached(name); + return off; // peut etre null + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (args.length == 1) { + return Arrays.asList("help", "create", "give", "remote", "list", "info", "delete", "reload"); + } + if (args.length == 2 && (args[0].equalsIgnoreCase("give") + || args[0].equalsIgnoreCase("list") + || args[0].equalsIgnoreCase("info"))) { + List out = new ArrayList<>(); + for (Player p : plugin.getServer().getOnlinePlayers()) out.add(p.getName()); + return out; + } + if (args.length == 2 && (args[0].equalsIgnoreCase("delete") || args[0].equalsIgnoreCase("remote")) + && sender instanceof Player p) { + List out = new ArrayList<>(); + for (Spaceship s : plugin.getDatabaseManager().getSpaceshipsByOwner(p.getUniqueId())) { + out.add(String.valueOf(s.getId())); + } + return out; + } + if (args.length == 3 && args[0].equalsIgnoreCase("give")) { + Player target = plugin.getServer().getPlayer(args[1]); + if (target != null) { + List out = new ArrayList<>(); + for (Spaceship s : plugin.getDatabaseManager().getSpaceshipsByOwner(target.getUniqueId())) { + out.add(String.valueOf(s.getId())); + } + return out; + } + } + return new ArrayList<>(); + } +} diff --git a/src/main/java/com/spaceshipproject/SpaceShipProject.java b/src/main/java/com/spaceshipproject/SpaceShipProject.java new file mode 100644 index 0000000..48f1425 --- /dev/null +++ b/src/main/java/com/spaceshipproject/SpaceShipProject.java @@ -0,0 +1,88 @@ +package com.spaceshipproject; + +import org.bukkit.plugin.java.JavaPlugin; + +/** + * Classe principale du plugin SpaceShipProject. + * + * Concept : + * - Chaque joueur peut posseder plusieurs spaceships personnels. + * - Chaque spaceship a sa propre telecommande (boussole). + * - Le spaceship est charge dans la meme map que le joueur, en plein ciel. + * - Seul le proprietaire peut le modifier. + * - Toutes les modifications sont sauvegardees en base au moment du dechargement. + */ +public class SpaceShipProject extends JavaPlugin { + + private ConfigManager configManager; + private DatabaseManager databaseManager; + private RemoteManager remoteManager; + private SpaceshipManager spaceshipManager; + private ModelManager modelManager; + private EconomyHook economy; + + @Override + public void onEnable() { + getLogger().info("SpaceShipProject : demarrage..."); + + saveDefaultConfig(); + configManager = new ConfigManager(this); + + databaseManager = new DatabaseManager(this); + databaseManager.initialize(); + + economy = new EconomyHook(this); + economy.setup(); + + remoteManager = new RemoteManager(this); + modelManager = new ModelManager(this); + spaceshipManager = new SpaceshipManager(this); + + getServer().getPluginManager().registerEvents(new RemoteListener(this), this); + getServer().getPluginManager().registerEvents(new BlockProtectionListener(this), this); + getServer().getPluginManager().registerEvents(new PlayerListener(this), this); + SpaceshipGUIListener.register(this); + + SpaceShipCommand cmd = new SpaceShipCommand(this); + if (getCommand("spaceship") != null) { + getCommand("spaceship").setExecutor(cmd); + getCommand("spaceship").setTabCompleter(cmd); + } + SpaceShipAdminCommand adminCmd = new SpaceShipAdminCommand(this); + if (getCommand("spaceshipadmin") != null) { + getCommand("spaceshipadmin").setExecutor(adminCmd); + getCommand("spaceshipadmin").setTabCompleter(adminCmd); + } + + getLogger().info("SpaceShipProject est actif !"); + } + + @Override + public void onDisable() { + if (modelManager != null) { + try { + modelManager.unloadAllOnShutdown(); + } catch (Exception ex) { + getLogger().warning("Erreur shutdown ModelManager : " + ex.getMessage()); + } + } + if (spaceshipManager != null) { + try { + spaceshipManager.unloadAllOnShutdown(); + } catch (Exception ex) { + getLogger().warning("Erreur lors du dechargement global : " + ex.getMessage()); + } + } + if (databaseManager != null) { + databaseManager.close(); + } + getLogger().info("SpaceShipProject est arrete."); + } + + public ConfigManager getConfigManager() { return configManager; } + public DatabaseManager getDatabaseManager() { return databaseManager; } + public RemoteManager getRemoteManager() { return remoteManager; } + public SpaceshipManager getSpaceshipManager() { return spaceshipManager; } + public ModelManager getModelManager() { return modelManager; } + public EconomyHook getEconomy() { return economy; } +} diff --git a/src/main/java/com/spaceshipproject/Spaceship.java b/src/main/java/com/spaceshipproject/Spaceship.java new file mode 100644 index 0000000..1dfcf97 --- /dev/null +++ b/src/main/java/com/spaceshipproject/Spaceship.java @@ -0,0 +1,76 @@ +package com.spaceshipproject; + +import java.util.UUID; + +/** + * Représentation en mémoire d'un spaceship. + * - id : identifiant unique en base + * - ownerUuid : propriétaire (seul lui peut modifier) + * - name : nom affiché sur la télécommande et dans les GUI + * - sizeX/Y/Z : dimensions de la boîte du spaceship + * - spawnX/Y/Z : point de spawn RELATIF à l'origine (téléportation sur le ship) + * - schematicJson : palette + blocs (RLE) sérialisés en JSON + * - settingsJson : paramètres modifiables par le joueur (JSON) + */ +public class Spaceship { + + private final long id; + private final UUID ownerUuid; + private String name; + private int sizeX; + private int sizeY; + private int sizeZ; + private int spawnX; + private int spawnY; + private int spawnZ; + private String schematicJson; + private String settingsJson; + private int level; + private int money; + private int lastLoadedLevel; + + public Spaceship(long id, UUID ownerUuid, String name, + int sizeX, int sizeY, int sizeZ, + int spawnX, int spawnY, int spawnZ, + String schematicJson, String settingsJson, + int level, int money, int lastLoadedLevel) { + this.id = id; + this.ownerUuid = ownerUuid; + this.name = name; + this.sizeX = sizeX; + this.sizeY = sizeY; + this.sizeZ = sizeZ; + this.spawnX = spawnX; + this.spawnY = spawnY; + this.spawnZ = spawnZ; + this.schematicJson = schematicJson; + this.settingsJson = settingsJson; + this.level = level; + this.money = money; + this.lastLoadedLevel = lastLoadedLevel; + } + + public long getId() { return id; } + public UUID getOwnerUuid() { return ownerUuid; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getSizeX() { return sizeX; } + public int getSizeY() { return sizeY; } + public int getSizeZ() { return sizeZ; } + public void setSize(int x, int y, int z) { this.sizeX = x; this.sizeY = y; this.sizeZ = z; } + public int getSpawnX() { return spawnX; } + public int getSpawnY() { return spawnY; } + public int getSpawnZ() { return spawnZ; } + public void setSpawn(int x, int y, int z) { this.spawnX = x; this.spawnY = y; this.spawnZ = z; } + public String getSchematicJson() { return schematicJson; } + public void setSchematicJson(String schematicJson) { this.schematicJson = schematicJson; } + public String getSettingsJson() { return settingsJson; } + public void setSettingsJson(String settingsJson) { this.settingsJson = settingsJson; } + public int getLevel() { return level; } + public void setLevel(int level) { this.level = level; } + /** @deprecated La money est maintenant geree via Vault. Conserve uniquement pour compatibilite DB. */ + @Deprecated public int getMoney() { return money; } + @Deprecated public void setMoney(int money) { this.money = money; } + public int getLastLoadedLevel() { return lastLoadedLevel; } + public void setLastLoadedLevel(int lastLoadedLevel) { this.lastLoadedLevel = lastLoadedLevel; } +} diff --git a/src/main/java/com/spaceshipproject/SpaceshipGUI.java b/src/main/java/com/spaceshipproject/SpaceshipGUI.java new file mode 100644 index 0000000..afb1fb7 --- /dev/null +++ b/src/main/java/com/spaceshipproject/SpaceshipGUI.java @@ -0,0 +1,298 @@ +package com.spaceshipproject; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +/** + * Interface de parametres d'un spaceship (ouverte via shift + clic droit sur la telecommande). + * + * Layout (27 slots) : + * - 4 : Info (tete : nom, taille, etat) + * - 11 : Invoquer / Decharger + * - 13 : Teleporter en haut / redescendre + * - 15 : Definir le spawn ici (uniquement si charge et joueur dessus) + * - 22 : Renommer le spaceship + * - 26 : Supprimer le spaceship (confirmation) + * - reste : verre de remplissage + */ +public class SpaceshipGUI implements SpaceshipGuiPanel { + + private static final int SIZE = 36; + private static final int SLOT_INFO = 4; + private static final int SLOT_TOGGLE = 19; + private static final int SLOT_TELEPORT = 20; + private static final int SLOT_SET_SPAWN = 22; + private static final int SLOT_SPACE_JUMP = 24; + private static final int SLOT_UPGRADE = 25; + private static final int SLOT_RENAME = 31; + private static final int SLOT_DELETE = 35; + + private final SpaceShipProject plugin; + private final Player player; + private final Spaceship ship; + private final SpaceshipGUIHolder holder; + private Inventory inv; + + public SpaceshipGUI(SpaceShipProject plugin, Player player, Spaceship ship) { + this.plugin = plugin; + this.player = player; + this.ship = ship; + this.holder = new SpaceshipGUIHolder(this); + } + + public void open() { + String title = ConfigManager.color("&8\u26F0 &bSpaceship &8\u2014 &f" + ship.getName()); + inv = Bukkit.createInventory(holder, SIZE, title); + holder.setInventory(inv); + render(); + SpaceshipGUIListener.registerOpen(player, this); + player.openInventory(inv); + } + + public Spaceship getShip() { return ship; } + public Player getPlayer() { return player; } + + private void render() { + for (int i = 0; i < SIZE; i++) inv.setItem(i, filler()); + + SpaceshipManager mgr = plugin.getSpaceshipManager(); + boolean loaded = mgr.isLoaded(ship.getId()); + boolean onShip = loaded && mgr.isPlayerOnShip(player, ship); + + inv.setItem(SLOT_INFO, infoItem(loaded)); + + inv.setItem(SLOT_TOGGLE, item( + loaded ? Material.REDSTONE_TORCH : Material.LEVER, + loaded ? "&c\u25A0 Decharger le spaceship" : "&a\u25B6 Invoquer le spaceship", + loaded + ? List.of("&7Les modifications seront sauvegardees.") + : List.of("&7Charge le spaceship dans le ciel,", + "&7au-dessus du sol ou vous etes.") + )); + + inv.setItem(SLOT_TELEPORT, item( + Material.ENDER_PEARL, + onShip ? "&a\u2B0B Redescendre au sol" : "&b\u2B06 Teleporter sur le spaceship", + onShip + ? List.of("&7Retourne a votre position au sol.") + : List.of("&7Vous teleporte sur le point de spawn", + "&7du spaceship (l'invoque si besoin).") + )); + + if (onShip) { + inv.setItem(SLOT_SET_SPAWN, item( + Material.RESPAWN_ANCHOR, + "&e\u2605 Definir le spawn ici", + List.of("&7Sauvegarde votre position actuelle", + "&7comme nouveau point d'apparition.") + )); + } else { + inv.setItem(SLOT_SET_SPAWN, item( + Material.GRAY_DYE, + "&8Definir le spawn", + List.of("&8(Indisponible : vous n'etes pas", + "&8actuellement sur le spaceship)") + )); + } + + EconomyHook eco = plugin.getEconomy(); + double balance = eco.isReady() ? eco.getBalance(player.getUniqueId()) : 0; + double cost = mgr.upgradeCost(ship.getLevel()); + boolean atMax = ship.getLevel() >= ShipSizing.maxUsefulLevel(plugin.getConfigManager()); + boolean canAfford = eco.isReady() && balance >= cost; + int[] currentSize = {ship.getSizeX(), ship.getSizeY(), ship.getSizeZ()}; + int[] nextSize = ShipSizing.sizeForLevel(plugin.getConfigManager(), ship.getLevel() + 1); + inv.setItem(SLOT_UPGRADE, item( + atMax ? Material.BARRIER : (canAfford ? Material.EXPERIENCE_BOTTLE : Material.GLASS_BOTTLE), + atMax + ? "&8&l\u2728 Niveau maximum atteint" + : (canAfford ? "&a&l\u2728 Ameliorer (Lv " : "&7&l\u2728 Ameliorer (Lv ") + + ship.getLevel() + " \u279C " + (ship.getLevel() + 1) + ")", + atMax ? List.of("&7Le ship est deja a sa taille maximale.") + : List.of( + "&7Niveau actuel : &e" + ship.getLevel(), + "&7Taille : &f" + currentSize[0] + "x" + currentSize[1] + "x" + currentSize[2] + + " &7\u279C &f" + nextSize[0] + "x" + nextSize[1] + "x" + nextSize[2], + "&7Cout : &6" + (eco.isReady() ? eco.format(cost) : cost), + "&7Solde : &6" + (eco.isReady() ? eco.format(balance) : "-"), + "", + canAfford ? "&aClic &7pour ameliorer." : "&cFonds insuffisants." + ) + )); + + // Bouton Saut Spatial : actif uniquement quand le joueur est sur le ship. + int jumpMax = mgr.spaceJumpMax(ship.getLevel()); + if (onShip) { + inv.setItem(SLOT_SPACE_JUMP, item( + Material.ENDER_EYE, + "&d&l\u27FC Saut Spatial", + List.of( + "&7Deplace le spaceship le long d'un axe.", + "&7Distance max : &f" + jumpMax + " &7blocs", + "&7Cout : &6" + (eco.isReady() ? eco.format(1) : "1") + " &7par bloc", + "", + "&eClic pour ouvrir le panneau de saut." + ) + )); + } else { + inv.setItem(SLOT_SPACE_JUMP, item( + Material.GRAY_DYE, + "&8\u27FC Saut Spatial", + List.of("&8(Indisponible : vous n'etes pas", + "&8actuellement sur le spaceship)") + )); + } + + inv.setItem(SLOT_RENAME, item( + Material.NAME_TAG, + "&e\u270F Renommer", + List.of("&7Tapez le nouveau nom dans le chat.", + "&7Tapez &ccancel &7pour annuler.") + )); + + inv.setItem(SLOT_DELETE, item( + Material.BARRIER, + "&c\u2620 Supprimer le spaceship", + List.of("&7Suppression definitive (telecommande", + "&7incluse). Decharge le ship d'abord.", + "&cShift + clic pour confirmer.") + )); + } + + private ItemStack infoItem(boolean loaded) { + ItemStack head = new ItemStack(Material.NETHER_STAR); + ItemMeta meta = head.getItemMeta(); + if (meta != null) { + meta.setDisplayName(ConfigManager.color("&b&l\u2730 &f" + ship.getName())); + EconomyHook eco = plugin.getEconomy(); + double balance = eco.isReady() ? eco.getBalance(player.getUniqueId()) : 0; + List lore = new ArrayList<>(); + lore.add(ConfigManager.color("&7ID : &f#" + ship.getId())); + lore.add(ConfigManager.color("&7Niveau : &e" + ship.getLevel())); + lore.add(ConfigManager.color("&7Votre solde : &6" + (eco.isReady() ? eco.format(balance) : "-"))); + lore.add(ConfigManager.color("&7Taille : &f" + + ship.getSizeX() + "x" + ship.getSizeY() + "x" + ship.getSizeZ())); + lore.add(ConfigManager.color("&7Spawn (rel.) : &f" + + ship.getSpawnX() + ", " + ship.getSpawnY() + ", " + ship.getSpawnZ())); + lore.add(ConfigManager.color("&7Statut : " + (loaded ? "&aCHARGE" : "&7decharge"))); + meta.setLore(lore); + head.setItemMeta(meta); + } + return head; + } + + private ItemStack filler() { + ItemStack stack = new ItemStack(Material.BLACK_STAINED_GLASS_PANE); + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + meta.setDisplayName(" "); + stack.setItemMeta(meta); + } + return stack; + } + + private ItemStack item(Material mat, String name, List lore) { + ItemStack stack = new ItemStack(mat); + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + meta.setDisplayName(ConfigManager.color(name)); + List colored = new ArrayList<>(lore.size()); + for (String s : lore) colored.add(ConfigManager.color(s)); + meta.setLore(colored); + stack.setItemMeta(meta); + } + return stack; + } + + public void handleClick(InventoryClickEvent event) { + int slot = event.getRawSlot(); + SpaceshipManager mgr = plugin.getSpaceshipManager(); + + switch (slot) { + case SLOT_TOGGLE -> { + if (mgr.isLoaded(ship.getId())) { + if (mgr.isPlayerOnShip(player, ship)) { + mgr.teleportDown(player, ship); + } + mgr.unload(player, ship); + } else { + mgr.summon(player, ship); + } + player.closeInventory(); + } + case SLOT_TELEPORT -> { + if (!mgr.isLoaded(ship.getId())) { + if (mgr.summon(player, ship)) mgr.teleportToShip(player, ship); + } else if (mgr.isPlayerOnShip(player, ship)) { + mgr.teleportDown(player, ship); + } else { + mgr.teleportToShip(player, ship); + } + player.closeInventory(); + } + case SLOT_SET_SPAWN -> { + if (!mgr.isLoaded(ship.getId()) || !mgr.isPlayerOnShip(player, ship)) return; + LoadedSpaceship loaded = mgr.getLoaded(ship.getId()); + if (loaded == null) return; + int rx = player.getLocation().getBlockX() - loaded.getOriginX(); + int ry = player.getLocation().getBlockY() - loaded.getOriginY(); + int rz = player.getLocation().getBlockZ() - loaded.getOriginZ(); + ship.setSpawn(rx, ry, rz); + plugin.getDatabaseManager().updateSpaceship(ship); + player.sendMessage(plugin.getConfigManager().prefix() + + ConfigManager.color("&aNouveau spawn enregistre.")); + render(); + } + case SLOT_UPGRADE -> { + double cost = mgr.upgradeCost(ship.getLevel()); + if (mgr.upgradeLevel(player, ship)) { + EconomyHook eco = plugin.getEconomy(); + String costStr = eco.isReady() ? eco.format(cost) : String.valueOf(cost); + player.sendMessage(plugin.getConfigManager().prefix() + + ConfigManager.color("&aSpaceship ameliore au niveau &e" + ship.getLevel() + " &a(-&6" + costStr + "&a).")); + refreshHandRemote(); + render(); + } + } + case SLOT_SPACE_JUMP -> { + if (mgr.isPlayerOnShip(player, ship)) { + new SpaceJumpGUI(plugin, player, ship).open(); + } + } + case SLOT_RENAME -> { + SpaceshipGUIListener.registerPendingRename(player, ship); + player.closeInventory(); + player.sendMessage(plugin.getConfigManager().prefix() + + ConfigManager.color("&eTapez le nouveau nom dans le chat (&ccancel&e pour annuler).")); + } + case SLOT_DELETE -> { + if (!event.isShiftClick()) { + player.sendMessage(plugin.getConfigManager().prefix() + + ConfigManager.color("&cShift + clic pour confirmer la suppression.")); + return; + } + mgr.delete(ship); + player.sendMessage(plugin.getConfigManager().msgDeleted()); + player.closeInventory(); + } + default -> { /* ignore */ } + } + } + + private void refreshHandRemote() { + ItemStack mainHand = player.getInventory().getItemInMainHand(); + if (plugin.getRemoteManager().isRemote(mainHand) + && plugin.getRemoteManager().getShipId(mainHand) == ship.getId()) { + plugin.getRemoteManager().refreshRemote(mainHand, ship); + player.getInventory().setItemInMainHand(mainHand); + } + } +} diff --git a/src/main/java/com/spaceshipproject/SpaceshipGUIHolder.java b/src/main/java/com/spaceshipproject/SpaceshipGUIHolder.java new file mode 100644 index 0000000..f780252 --- /dev/null +++ b/src/main/java/com/spaceshipproject/SpaceshipGUIHolder.java @@ -0,0 +1,28 @@ +package com.spaceshipproject; + +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; + +/** Holder generique pour les panneaux GUI du plugin (Spaceship + SpaceJump). */ +public class SpaceshipGUIHolder implements InventoryHolder { + + private final SpaceshipGuiPanel panel; + private Inventory inventory; + + public SpaceshipGUIHolder(SpaceshipGuiPanel panel) { + this.panel = panel; + } + + void setInventory(Inventory inventory) { + this.inventory = inventory; + } + + @Override + public Inventory getInventory() { + return inventory; + } + + public SpaceshipGuiPanel getPanel() { + return panel; + } +} diff --git a/src/main/java/com/spaceshipproject/SpaceshipGUIListener.java b/src/main/java/com/spaceshipproject/SpaceshipGUIListener.java new file mode 100644 index 0000000..b784991 --- /dev/null +++ b/src/main/java/com/spaceshipproject/SpaceshipGUIListener.java @@ -0,0 +1,115 @@ +package com.spaceshipproject; + +import io.papermc.paper.event.player.AsyncChatEvent; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** Listener partage pour les SpaceshipGUI : clics, fermeture, et renommage par chat. */ +public class SpaceshipGUIListener implements Listener { + + private static final Map OPEN_GUIS = new ConcurrentHashMap<>(); + private static final Map PENDING_RENAME = new ConcurrentHashMap<>(); + + private final SpaceShipProject plugin; + + private SpaceshipGUIListener(SpaceShipProject plugin) { + this.plugin = plugin; + } + + public static void register(SpaceShipProject plugin) { + plugin.getServer().getPluginManager().registerEvents(new SpaceshipGUIListener(plugin), plugin); + } + + static void registerOpen(Player player, SpaceshipGUI gui) { + OPEN_GUIS.put(player.getUniqueId(), gui); + } + + static void registerOpenJump(Player player, SpaceJumpGUI gui) { + OPEN_GUIS.put(player.getUniqueId(), gui); + } + + static void registerPendingRename(Player player, Spaceship ship) { + PENDING_RENAME.put(player.getUniqueId(), ship.getId()); + } + + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = false) + public void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player)) return; + Inventory top = event.getView().getTopInventory(); + if (!(top.getHolder() instanceof SpaceshipGUIHolder holder)) return; + event.setCancelled(true); + if (event.getClickedInventory() != null && event.getClickedInventory() == top) { + holder.getPanel().handleClick(event); + } + } + + @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = false) + public void onInventoryDrag(InventoryDragEvent event) { + if (!(event.getWhoClicked() instanceof Player)) return; + Inventory top = event.getView().getTopInventory(); + if (!(top.getHolder() instanceof SpaceshipGUIHolder)) return; + event.setCancelled(true); + } + + @EventHandler + public void onInventoryClose(InventoryCloseEvent event) { + if (!(event.getPlayer() instanceof Player player)) return; + if (event.getInventory().getHolder() instanceof SpaceshipGUIHolder) { + OPEN_GUIS.remove(player.getUniqueId()); + } + } + + @EventHandler(priority = EventPriority.HIGH) + public void onChat(AsyncChatEvent event) { + Player player = event.getPlayer(); + Long shipId = PENDING_RENAME.remove(player.getUniqueId()); + if (shipId == null) return; + + String message = PlainTextComponentSerializer.plainText().serialize(event.message()).trim(); + event.setCancelled(true); + + Bukkit.getScheduler().runTask(plugin, () -> { + if (message.equalsIgnoreCase("cancel") || message.isEmpty()) { + player.sendMessage(plugin.getConfigManager().prefix() + + ConfigManager.color("&7Renommage annule.")); + return; + } + String clean = message.length() > 32 ? message.substring(0, 32) : message; + Spaceship ship = plugin.getDatabaseManager().getSpaceship(shipId); + if (ship == null || !ship.getOwnerUuid().equals(player.getUniqueId())) { + player.sendMessage(plugin.getConfigManager().prefix() + + ConfigManager.color("&cImpossible de renommer ce spaceship.")); + return; + } + ship.setName(clean); + plugin.getDatabaseManager().updateSpaceship(ship); + + // Met a jour la telecommande dans la main si elle est presente + ItemStack mainHand = player.getInventory().getItemInMainHand(); + if (plugin.getRemoteManager().isRemote(mainHand) + && mainHand.getItemMeta() != null + && shipId.equals(mainHand.getItemMeta().getPersistentDataContainer() + .get(plugin.getRemoteManager().getShipIdKey(), PersistentDataType.LONG))) { + plugin.getRemoteManager().refreshRemote(mainHand, ship); + player.getInventory().setItemInMainHand(mainHand); + } + + player.sendMessage(plugin.getConfigManager().prefix() + + ConfigManager.color("&aSpaceship renomme : &b" + clean)); + }); + } +} diff --git a/src/main/java/com/spaceshipproject/SpaceshipGuiPanel.java b/src/main/java/com/spaceshipproject/SpaceshipGuiPanel.java new file mode 100644 index 0000000..657448a --- /dev/null +++ b/src/main/java/com/spaceshipproject/SpaceshipGuiPanel.java @@ -0,0 +1,8 @@ +package com.spaceshipproject; + +import org.bukkit.event.inventory.InventoryClickEvent; + +/** Interface commune aux differents panneaux de GUI du plugin. */ +public interface SpaceshipGuiPanel { + void handleClick(InventoryClickEvent event); +} diff --git a/src/main/java/com/spaceshipproject/SpaceshipManager.java b/src/main/java/com/spaceshipproject/SpaceshipManager.java new file mode 100644 index 0000000..0659e93 --- /dev/null +++ b/src/main/java/com/spaceshipproject/SpaceshipManager.java @@ -0,0 +1,605 @@ +package com.spaceshipproject; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.persistence.PersistentDataType; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Logique metier des spaceships : creation, invocation, decharge, teleportation. + * + * Etat en memoire : + * - returnLocations : ou redescendre le joueur quand il se trouve sur son ship. + */ +public class SpaceshipManager { + + private final SpaceShipProject plugin; + private final DatabaseManager db; + private final ConfigManager cfg; + + /** Position au sol memorisee lors de la teleportation vers un ship (par joueur). */ + private final Map returnLocations = new HashMap<>(); + + public SpaceshipManager(SpaceShipProject plugin) { + this.plugin = plugin; + this.db = plugin.getDatabaseManager(); + this.cfg = plugin.getConfigManager(); + } + + /* ============================ CREATION ============================ */ + + /** + * Cree un nouveau spaceship en base avec : + * - le modele "default.json" du dossier models/ s'il existe, + * - sinon la plateforme procedurale par defaut. + * Retourne l'id ou -1 si le quota est atteint. + */ + public long createNewSpaceship(Player owner) { + int max = cfg.maxShipsPerPlayer(); + int count = db.countSpaceshipsByOwner(owner.getUniqueId()); + if (max > 0 && count >= max) { + owner.sendMessage(cfg.msgMaxShips(max)); + return -1; + } + db.upsertPlayer(owner); + + String schematic = plugin.getModelManager().loadDefaultModelJson(); + int sx, sy, sz; + if (schematic != null) { + int[] size = SchematicHelper.readSize(schematic); + sx = size[0]; sy = size[1]; sz = size[2]; + } else { + sx = cfg.defaultSizeX(); + sy = cfg.defaultSizeY(); + sz = cfg.defaultSizeZ(); + schematic = SchematicHelper.buildDefaultSchematic(sx, sy, sz, + cfg.defaultPlatformMaterial(), + cfg.defaultEdgeMaterial(), + cfg.defaultGlassMaterial()); + } + + int[] spawn = SchematicHelper.readSpawn(schematic); + String name = cfg.defaultShipName().replace("%n%", String.valueOf(count + 1)); + return db.createSpaceship(owner.getUniqueId(), name, sx, sy, sz, + spawn[0], spawn[1], spawn[2], schematic, "{}"); + } + + /* ============================ STATE ============================ */ + + public boolean isLoaded(long shipId) { + return db.getLoaded(shipId) != null; + } + + public LoadedSpaceship getLoaded(long shipId) { + return db.getLoaded(shipId); + } + + /* ============================ SUMMON ============================ */ + + /** + * Invoque le spaceship au-dessus du joueur. Si deja charge, ne fait rien. + * Coute {@link ConfigManager#costSummon()} en monnaie Vault (paye par le proprietaire). + */ + public boolean summon(Player player, Spaceship ship) { + if (isLoaded(ship.getId())) { + player.sendMessage(cfg.msgAlreadyLoaded()); + return false; + } + + // Paiement (sauf admin). + double cost = cfg.costSummon(); + if (cost > 0 && !player.hasPermission("spaceship.bypass.cost")) { + EconomyHook eco = plugin.getEconomy(); + if (!eco.isReady()) { + player.sendMessage(cfg.msgVaultUnavailable()); + return false; + } + double balance = eco.getBalance(player.getUniqueId()); + if (balance < cost) { + player.sendMessage(cfg.msgNotEnoughMoney(eco.format(cost), eco.format(balance))); + return false; + } + } + + World world = player.getWorld(); + + // Si le niveau a augmente depuis le dernier chargement, on etend la schematique + // a la nouvelle taille en marquant les coins avec Verdant Froglight. + if (ship.getLevel() > ship.getLastLoadedLevel()) { + int[] target = ShipSizing.sizeForLevel(cfg, ship.getLevel()); + int nx = Math.max(target[0], ship.getSizeX()); + int ny = Math.max(target[1], ship.getSizeY()); + int nz = Math.max(target[2], ship.getSizeZ()); + if (nx != ship.getSizeX() || ny != ship.getSizeY() || nz != ship.getSizeZ()) { + try { + String expanded = SchematicHelper.expandSchematic( + ship.getSchematicJson(), nx, ny, nz, Material.VERDANT_FROGLIGHT); + ship.setSchematicJson(expanded); + ship.setSize(nx, ny, nz); + } catch (Exception ex) { + plugin.getLogger().warning("Erreur expansion ship #" + ship.getId() + " : " + ex.getMessage()); + } + } + ship.setLastLoadedLevel(ship.getLevel()); + db.updateSpaceship(ship); + } + + int sx = ship.getSizeX(), sy = ship.getSizeY(), sz = ship.getSizeZ(); + int ox = player.getLocation().getBlockX() - sx / 2; + int oz = player.getLocation().getBlockZ() - sz / 2; + int oy = clampY(world, player.getLocation().getBlockY() + cfg.summonOffsetY(), sy); + + if (cfg.checkCollision() && SchematicHelper.hasCollision(world, ox, oy, oz, sx, sy, sz)) { + player.sendMessage(cfg.msgCollision()); + return false; + } + + try { + SchematicHelper.restoreRegion(ship.getSchematicJson(), world, ox, oy, oz); + } catch (Exception ex) { + plugin.getLogger().severe("Erreur restauration ship #" + ship.getId() + " : " + ex.getMessage()); + return false; + } + + // Debit apres succes de la restauration. + if (cost > 0 && !player.hasPermission("spaceship.bypass.cost")) { + plugin.getEconomy().withdraw(player.getUniqueId(), cost); + } + + db.markLoaded(ship.getId(), world.getName(), ox, oy, oz); + player.sendMessage(cfg.msgSummoned(ship.getName())); + return true; + } + + /* ============================ UNLOAD ============================ */ + + /** + * Capture les modifications et decharge le ship du monde. + * Sauvegarde la nouvelle schematique en base. + */ + public boolean unload(Player player, Spaceship ship) { + LoadedSpaceship loaded = db.getLoaded(ship.getId()); + if (loaded == null) { + player.sendMessage(cfg.msgNotLoaded()); + return false; + } + World world = loaded.getWorld(); + if (world == null) { + plugin.getLogger().warning("Monde introuvable pour decharger le ship #" + ship.getId()); + db.markUnloaded(ship.getId()); + return false; + } + + try { + String updated = SchematicHelper.captureRegion(world, + loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(), + ship.getSizeX(), ship.getSizeY(), ship.getSizeZ(), + ship.getSpawnX(), ship.getSpawnY(), ship.getSpawnZ()); + ship.setSchematicJson(updated); + db.updateSpaceship(ship); + } catch (Exception ex) { + plugin.getLogger().severe("Erreur capture ship #" + ship.getId() + " : " + ex.getMessage()); + return false; + } + + SchematicHelper.clearRegion(world, + loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(), + ship.getSizeX(), ship.getSizeY(), ship.getSizeZ()); + + db.markUnloaded(ship.getId()); + player.sendMessage(cfg.msgUnloaded(ship.getName())); + return true; + } + + /* ============================ TELEPORT ============================ */ + + /** + * Teleporte le joueur sur le spawn du ship (le ship doit etre charge). + * Memorise la position de retour. + */ + public boolean teleportToShip(Player player, Spaceship ship) { + LoadedSpaceship loaded = db.getLoaded(ship.getId()); + if (loaded == null) { + player.sendMessage(cfg.msgNotLoaded()); + return false; + } + World world = loaded.getWorld(); + if (world == null) return false; + + returnLocations.put(player.getUniqueId(), player.getLocation().clone()); + + Location target = new Location(world, + loaded.getOriginX() + ship.getSpawnX() + 0.5, + loaded.getOriginY() + ship.getSpawnY(), + loaded.getOriginZ() + ship.getSpawnZ() + 0.5, + player.getLocation().getYaw(), + player.getLocation().getPitch()); + player.teleport(target); + player.sendMessage(cfg.msgTpUp(ship.getName())); + return true; + } + + /** + * Teleporte le joueur a sa position de retour. Si pas memorise, + * tombe sur le sol au-dessous du ship. + */ + public void teleportDown(Player player, Spaceship ship) { + Location stored = returnLocations.remove(player.getUniqueId()); + if (stored != null && stored.getWorld() != null) { + player.teleport(stored); + } else { + LoadedSpaceship loaded = db.getLoaded(ship.getId()); + if (loaded != null && loaded.getWorld() != null) { + int x = loaded.getOriginX() + ship.getSizeX() / 2; + int z = loaded.getOriginZ() + ship.getSizeZ() / 2; + int y = loaded.getWorld().getHighestBlockYAt(x, z) + 1; + player.teleport(new Location(loaded.getWorld(), x + 0.5, y, z + 0.5, + player.getLocation().getYaw(), player.getLocation().getPitch())); + } + } + player.sendMessage(cfg.msgTpDown()); + } + + /** + * True si le joueur se trouve actuellement DANS la bounding box du ship charge. + */ + public boolean isPlayerOnShip(Player player, Spaceship ship) { + LoadedSpaceship loaded = db.getLoaded(ship.getId()); + if (loaded == null) return false; + return loaded.contains(player.getLocation()); + } + + /* ============================ DELETE ============================ */ + + public void delete(Spaceship ship) { + LoadedSpaceship loaded = db.getLoaded(ship.getId()); + if (loaded != null && loaded.getWorld() != null) { + // Si le proprietaire est sur le ship, le tp au sol avant la disparition. + OfflinePlayer off = Bukkit.getOfflinePlayer(ship.getOwnerUuid()); + if (off.isOnline()) { + Player onlineOwner = off.getPlayer(); + if (onlineOwner != null && loaded.contains(onlineOwner.getLocation())) { + teleportDown(onlineOwner, ship); + } + } + SchematicHelper.clearRegion(loaded.getWorld(), + loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(), + ship.getSizeX(), ship.getSizeY(), ship.getSizeZ()); + } + db.deleteSpaceship(ship.getId()); + // Supprime egalement la telecommande dans l'inventaire (si proprietaire en ligne). + removeRemoteFromInventory(ship.getOwnerUuid(), ship.getId()); + } + + /** Supprime toutes les boussoles correspondant a shipId dans l'inventaire du joueur (s'il est en ligne). */ + public int removeRemoteFromInventory(UUID ownerUuid, long shipId) { + Player p = Bukkit.getPlayer(ownerUuid); + if (p == null) return 0; + return removeRemotesFromInventory(p.getInventory(), shipId); + } + + private int removeRemotesFromInventory(PlayerInventory inv, long shipId) { + int removed = 0; + RemoteManager rm = plugin.getRemoteManager(); + for (int i = 0; i < inv.getSize(); i++) { + ItemStack it = inv.getItem(i); + if (it == null || !rm.isRemote(it)) continue; + Long id = it.getItemMeta().getPersistentDataContainer().get(rm.getShipIdKey(), PersistentDataType.LONG); + if (id != null && id == shipId) { + inv.setItem(i, null); + removed++; + } + } + return removed; + } + + /** + * Reconcilie l'inventaire du joueur : + * - supprime les telecommandes dont le ship n'existe plus, + * - supprime les doublons (>1 stack pour le meme shipId). + * Renvoie le nombre d'items supprimes. + */ + public int sanitizePlayerRemotes(Player player) { + RemoteManager rm = plugin.getRemoteManager(); + PlayerInventory inv = player.getInventory(); + Map seen = new HashMap<>(); + int removed = 0; + for (int i = 0; i < inv.getSize(); i++) { + ItemStack it = inv.getItem(i); + if (it == null || !rm.isRemote(it)) continue; + Long id = it.getItemMeta().getPersistentDataContainer().get(rm.getShipIdKey(), PersistentDataType.LONG); + if (id == null) { inv.setItem(i, null); removed++; continue; } + if (db.getSpaceship(id) == null) { + inv.setItem(i, null); removed++; + continue; + } + Integer firstSlot = seen.put(id, i); + if (firstSlot != null) { + // Doublon : on retire l'item en cours et on garde le premier rencontre. + inv.setItem(i, null); + removed++; + } + } + return removed; + } + + /** + * Decharge (capture + clear) tous les spaceships actuellement charges + * appartenant au joueur. Utilise a la deconnexion. + */ + public int unloadAllForPlayer(UUID ownerUuid) { + int count = 0; + for (LoadedSpaceship loaded : db.getLoadedByOwner(ownerUuid)) { + Spaceship ship = db.getSpaceship(loaded.getSpaceshipId()); + if (ship == null) { + db.markUnloaded(loaded.getSpaceshipId()); + continue; + } + World world = loaded.getWorld(); + if (world == null) { + db.markUnloaded(loaded.getSpaceshipId()); + continue; + } + try { + String updated = SchematicHelper.captureRegion(world, + loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(), + ship.getSizeX(), ship.getSizeY(), ship.getSizeZ(), + ship.getSpawnX(), ship.getSpawnY(), ship.getSpawnZ()); + ship.setSchematicJson(updated); + db.updateSpaceship(ship); + SchematicHelper.clearRegion(world, + loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(), + ship.getSizeX(), ship.getSizeY(), ship.getSizeZ()); + db.markUnloaded(loaded.getSpaceshipId()); + count++; + } catch (Exception ex) { + plugin.getLogger().warning("Erreur unloadAllForPlayer ship #" + ship.getId() + " : " + ex.getMessage()); + } + } + return count; + } + + /* ============================ LEVEL / MONEY ============================ */ + + /** Cout pour passer du level N a N+1 : upgrade-base * N. */ + public double upgradeCost(int currentLevel) { + return Math.max(0, currentLevel) * cfg.costUpgradeBase(); + } + + /** + * Tente d'ameliorer le ship d'un niveau (paye avec Vault par le proprietaire {@code payer}). + * La taille effective grimpe selon la regle de progression mais le ship n'est physiquement + * agrandi qu'au prochain summon (avec marquage des coins). + * Renvoie true si l'achat est effectue. + */ + public boolean upgradeLevel(Player payer, Spaceship ship) { + if (ship.getLevel() >= ShipSizing.maxUsefulLevel(cfg)) return false; + EconomyHook eco = plugin.getEconomy(); + if (!eco.isReady()) { payer.sendMessage(cfg.msgVaultUnavailable()); return false; } + double cost = upgradeCost(ship.getLevel()); + if (!eco.has(payer.getUniqueId(), cost)) { + payer.sendMessage(cfg.msgNotEnoughMoney(eco.format(cost), eco.format(eco.getBalance(payer.getUniqueId())))); + return false; + } + if (!eco.withdraw(payer.getUniqueId(), cost)) return false; + ship.setLevel(ship.getLevel() + 1); + db.updateSpaceship(ship); + return true; + } + + /* ============================ SAUT SPATIAL ============================ */ + + /** Distance maximale d'un saut spatial pour le niveau donne. */ + public int spaceJumpMax(int level) { + return Math.min(cfg.spaceJumpHardCap(), Math.max(cfg.spaceJumpMin(), cfg.spaceJumpBlocksPerLevel() * Math.max(1, level))); + } + + /** Cout d'un saut spatial. */ + public double spaceJumpCost(int distance) { + return Math.max(0, distance) * cfg.costSpaceJumpPerBlock(); + } + + /** + * Effectue un saut spatial : decharge le ship a son emplacement courant, + * le recharge a la nouvelle position (delta sur un axe), teleporte le joueur sur le ship. + * + * @param axis 'X', 'Y' ou 'Z' + * @param signedDistance distance signee (negatif = direction opposee) + */ + public boolean spaceJump(Player player, Spaceship ship, char axis, int signedDistance) { + if (!ship.getOwnerUuid().equals(player.getUniqueId()) && !player.hasPermission("spaceship.admin")) { + player.sendMessage(cfg.msgNotOwner()); + return false; + } + LoadedSpaceship loaded = db.getLoaded(ship.getId()); + if (loaded == null) { player.sendMessage(cfg.msgNotLoaded()); return false; } + if (!isPlayerOnShip(player, ship)) { player.sendMessage(cfg.msgSpaceJumpNotOnShip()); return false; } + + int absDist = Math.abs(signedDistance); + int max = spaceJumpMax(ship.getLevel()); + if (absDist < cfg.spaceJumpMin() || absDist > max) { + player.sendMessage(cfg.msgSpaceJumpTooFar(max, ship.getLevel())); + return false; + } + double cost = spaceJumpCost(absDist); + EconomyHook eco = plugin.getEconomy(); + if (cost > 0) { + if (!eco.isReady()) { player.sendMessage(cfg.msgVaultUnavailable()); return false; } + if (!eco.has(player.getUniqueId(), cost)) { + player.sendMessage(cfg.msgNotEnoughMoney(eco.format(cost), eco.format(eco.getBalance(player.getUniqueId())))); + return false; + } + } + + World world = loaded.getWorld(); + if (world == null) return false; + + // Calcule la nouvelle origine. + int nx = loaded.getOriginX(), ny = loaded.getOriginY(), nz = loaded.getOriginZ(); + switch (Character.toUpperCase(axis)) { + case 'X' -> nx += signedDistance; + case 'Y' -> ny += signedDistance; + case 'Z' -> nz += signedDistance; + default -> { player.sendMessage(ConfigManager.color("&cAxe invalide.")); return false; } + } + ny = clampY(world, ny, ship.getSizeY()); + + if (cfg.checkCollision() && SchematicHelper.hasCollision(world, nx, ny, nz, + ship.getSizeX(), ship.getSizeY(), ship.getSizeZ())) { + player.sendMessage(cfg.msgCollision()); + return false; + } + + // 1) Capture l'etat actuel + try { + String updated = SchematicHelper.captureRegion(world, + loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(), + ship.getSizeX(), ship.getSizeY(), ship.getSizeZ(), + ship.getSpawnX(), ship.getSpawnY(), ship.getSpawnZ()); + ship.setSchematicJson(updated); + } catch (Exception ex) { + player.sendMessage(ConfigManager.color("&cErreur capture : " + ex.getMessage())); + return false; + } + // 2) Clear emplacement initial + SchematicHelper.clearRegion(world, loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(), + ship.getSizeX(), ship.getSizeY(), ship.getSizeZ()); + // 3) Restore au nouvel emplacement + try { + SchematicHelper.restoreRegion(ship.getSchematicJson(), world, nx, ny, nz); + } catch (Exception ex) { + // Rollback : recharge a l'ancien emplacement + try { + SchematicHelper.restoreRegion(ship.getSchematicJson(), world, + loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ()); + } catch (Exception ignored) {} + player.sendMessage(ConfigManager.color("&cErreur restauration : " + ex.getMessage())); + return false; + } + + db.updateSpaceship(ship); + db.markLoaded(ship.getId(), world.getName(), nx, ny, nz); + if (cost > 0) eco.withdraw(player.getUniqueId(), cost); + + // Teleporte le joueur sur le ship a son spawn. + Location target = new Location(world, + nx + ship.getSpawnX() + 0.5, + ny + ship.getSpawnY(), + nz + ship.getSpawnZ() + 0.5, + player.getLocation().getYaw(), player.getLocation().getPitch()); + player.teleport(target); + + String dir = axisLabel(axis, signedDistance); + player.sendMessage(cfg.msgSpaceJumpSuccess(absDist, dir, eco.isReady() ? eco.format(cost) : String.valueOf(cost))); + return true; + } + + private static String axisLabel(char axis, int signed) { + return switch (Character.toUpperCase(axis)) { + case 'X' -> signed >= 0 ? "Est (X+)" : "Ouest (X-)"; + case 'Y' -> signed >= 0 ? "Haut (Y+)" : "Bas (Y-)"; + case 'Z' -> signed >= 0 ? "Sud (Z+)" : "Nord (Z-)"; + default -> "?"; + }; + } + + /* ============================ HELPERS ============================ */ + + private int clampY(World world, int y, int sizeY) { + int min = world.getMinHeight() + 1; + int max = world.getMaxHeight() - sizeY - 1; + return Math.max(min, Math.min(max, y)); + } + + /** + * Retourne le LoadedSpaceship et le Spaceship si le joueur se tient sur l'un de SES ships. + * Pratique pour les listeners de protection. + */ + public Spaceship findOwnedShipPlayerIsOn(Player player) { + List ships = db.getSpaceshipsByOwner(player.getUniqueId()); + for (Spaceship s : ships) { + if (isPlayerOnShip(player, s)) return s; + } + return null; + } + + /** + * Retourne le proprietaire du ship dont la bounding box contient cette position, + * ou null sinon. Utilise par la protection de blocs. + */ + public LoadedShipOwnerInfo findShipOwnerAt(World world, int x, int y, int z) { + for (LoadedSpaceship loaded : db.getAllLoaded()) { + if (!loaded.getWorldName().equals(world.getName())) continue; + if (x < loaded.getOriginX() || x >= loaded.getOriginX() + loaded.getSizeX()) continue; + if (y < loaded.getOriginY() || y >= loaded.getOriginY() + loaded.getSizeY()) continue; + if (z < loaded.getOriginZ() || z >= loaded.getOriginZ() + loaded.getSizeZ()) continue; + Spaceship ship = db.getSpaceship(loaded.getSpaceshipId()); + if (ship == null) continue; + return new LoadedShipOwnerInfo(ship, loaded); + } + return null; + } + + /** Petit conteneur pour la protection. */ + public record LoadedShipOwnerInfo(Spaceship ship, LoadedSpaceship loaded) { + public UUID ownerUuid() { return ship.getOwnerUuid(); } + } + + /** + * Decharge tous les ships en base lors de la fermeture du serveur. + * (Pour eviter qu'au prochain demarrage le ship soit considere comme charge alors qu'il a ete genere a un endroit qui sera reset). + * On capture l'etat actuel pour ne rien perdre. + */ + public void unloadAllOnShutdown() { + for (LoadedSpaceship loaded : db.getAllLoaded()) { + Spaceship ship = db.getSpaceship(loaded.getSpaceshipId()); + if (ship == null) { + db.markUnloaded(loaded.getSpaceshipId()); + continue; + } + World world = loaded.getWorld(); + if (world == null) { + db.markUnloaded(loaded.getSpaceshipId()); + continue; + } + try { + String updated = SchematicHelper.captureRegion(world, + loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(), + ship.getSizeX(), ship.getSizeY(), ship.getSizeZ(), + ship.getSpawnX(), ship.getSpawnY(), ship.getSpawnZ()); + ship.setSchematicJson(updated); + db.updateSpaceship(ship); + SchematicHelper.clearRegion(world, + loaded.getOriginX(), loaded.getOriginY(), loaded.getOriginZ(), + ship.getSizeX(), ship.getSizeY(), ship.getSizeZ()); + } catch (Exception ex) { + plugin.getLogger().warning("Erreur shutdown ship #" + ship.getId() + " : " + ex.getMessage()); + } + db.markUnloaded(loaded.getSpaceshipId()); + } + } + + /** + * Verifie en boucle si des joueurs sont tombes du ship et corrige eventuellement. + * Pour l'instant ne fait rien, methode reservee a une evolution future. + */ + public void tick() { + // no-op + } + + public DatabaseManager getDatabase() { return db; } + + /** Test pratique : Bukkit#getWorld. */ + @SuppressWarnings("unused") + private static World worldOf(String name) { return Bukkit.getWorld(name); } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..dfcf76d --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,70 @@ +# ========================================================= +# SpaceShipProject - Configuration +# ========================================================= + +# Limite de spaceships par joueur (0 = illimite) +max-ships-per-player: 5 + +# Taille du spaceship a la creation (level 1) +default-size: + x: 4 + y: 4 + z: 6 + +# Taille maximale (atteinte avec les niveaux) +max-size: + x: 16 + y: 8 + z: 16 + +# Couts en money (Vault / EssentialsEconomy) +costs: + summon: 100 # cout pour charger un spaceship + unload: 0 # cout pour decharger + upgrade-base: 1000 # cout d'amelioration : upgrade-base * niveau actuel + space-jump-per-block: 1 # cout par bloc lors d'un saut spatial + +# Saut spatial +space-jump: + blocks-per-level: 100 # max blocs autorises = blocks-per-level * niveau + min-blocks: 1 + max-blocks-hard-cap: 5000 + +# Hauteur a laquelle le spaceship apparait au-dessus du joueur quand il est invoque +summon-offset-y: 30 + +# Materiau utilise pour la plateforme par defaut lors de la creation d'un nouveau spaceship +default-platform-material: SMOOTH_QUARTZ +default-edge-material: QUARTZ_PILLAR +default-glass-material: WHITE_STAINED_GLASS + +# Nom par defaut des nouveaux spaceships (%n% = numero) +default-ship-name: "Spaceship #%n%" + +# Verifications de securite +safety: + # Empecher l'invocation si des blocs solides sont presents a l'emplacement + check-collision: true + # Empecher la destruction du sol (le spaceship doit etre dans le ciel) + min-y-above-player: 10 + +# Messages (couleurs Minecraft, &x) +messages: + prefix: "&8[&bSpaceShip&8] &7" + no-permission: "&cVous n'avez pas la permission !" + not-owner: "&cCe spaceship ne vous appartient pas." + ship-given: "&aTelecommande du spaceship &b%name% &acreee et donnee." + ship-summoned: "&aSpaceship &b%name% &acharge dans le ciel !" + ship-unloaded: "&aSpaceship &b%name% &adecharge, modifications sauvegardees." + ship-teleport-up: "&aTeleportation sur le spaceship &b%name%&a..." + ship-teleport-down: "&aRedescente sur la terre ferme..." + ship-already-loaded: "&eVotre spaceship est deja charge." + ship-not-loaded: "&eVotre spaceship n'est pas charge." + ship-collision: "&cImpossible d'invoquer le spaceship ici : zone obstruee." + max-ships-reached: "&cVous avez atteint la limite de spaceships (%max%)." + ship-deleted: "&cSpaceship supprime." + not-enough-money: "&cFonds insuffisants : %need% requis, %have% disponible." + vault-unavailable: "&cVault/EssentialsEconomy est indisponible, action impossible." + space-jump-success: "&aSaut spatial : %dist% blocs vers %dir% (-%cost%)." + space-jump-not-on-ship: "&cVous devez etre sur le spaceship pour effectuer un saut." + space-jump-too-far: "&cDistance trop grande (max %max% au niveau %lvl%)." diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..37e74b9 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,56 @@ +name: SpaceShipProject +main: com.spaceshipproject.SpaceShipProject +version: 1.0 +api-version: 1.21 +author: CreatorOfNothing +description: Spaceships personnels charges en plein ciel, telecommande boussole. +softdepend: [Vault, EssentialsX] + +commands: + spaceship: + description: Commandes joueur du plugin SpaceShipProject + usage: /spaceship + aliases: [ss, ship] + permission: spaceship.use + spaceshipadmin: + description: Commandes d'administration du plugin SpaceShipProject + usage: /spaceshipadmin + aliases: [ssa, shipadmin] + permission: spaceship.admin + +permissions: + spaceship.*: + description: Toutes les permissions de SpaceShipProject + children: + spaceship.use: true + spaceship.create: true + spaceship.give: true + spaceship.info.other: true + spaceship.delete: true + spaceship.reload: true + spaceship.admin: true + default: op + spaceship.use: + description: Utiliser sa propre telecommande + default: true + spaceship.create: + description: Creer un nouveau spaceship (/ss create) + default: true + spaceship.give: + description: Recuperer une telecommande perdue (la sienne ou celle d'un autre avec admin) + default: true + spaceship.info.other: + description: Voir les infos detaillees d'un autre joueur + default: op + spaceship.delete: + description: Supprimer un de ses spaceships + default: true + spaceship.reload: + description: Recharger la configuration + default: op + spaceship.admin: + description: Permissions d'administration (bypass de propriete, /ssa, etc.) + default: op + spaceship.bypass.cost: + description: Permet de ne pas payer les couts d'invocation / saut / upgrade + default: op diff --git a/target/SpaceShipProject-1.0-SNAPSHOT.jar b/target/SpaceShipProject-1.0-SNAPSHOT.jar new file mode 100644 index 0000000..5764d1a Binary files /dev/null and b/target/SpaceShipProject-1.0-SNAPSHOT.jar differ diff --git a/target/classes/com/spaceshipproject/BlockProtectionListener.class b/target/classes/com/spaceshipproject/BlockProtectionListener.class new file mode 100644 index 0000000..0eb23f1 Binary files /dev/null and b/target/classes/com/spaceshipproject/BlockProtectionListener.class differ diff --git a/target/classes/com/spaceshipproject/ConfigManager.class b/target/classes/com/spaceshipproject/ConfigManager.class new file mode 100644 index 0000000..f9fc381 Binary files /dev/null and b/target/classes/com/spaceshipproject/ConfigManager.class differ diff --git a/target/classes/com/spaceshipproject/DatabaseManager.class b/target/classes/com/spaceshipproject/DatabaseManager.class new file mode 100644 index 0000000..be23a03 Binary files /dev/null and b/target/classes/com/spaceshipproject/DatabaseManager.class differ diff --git a/target/classes/com/spaceshipproject/EconomyHook.class b/target/classes/com/spaceshipproject/EconomyHook.class new file mode 100644 index 0000000..4456fe0 Binary files /dev/null and b/target/classes/com/spaceshipproject/EconomyHook.class differ diff --git a/target/classes/com/spaceshipproject/LoadedSpaceship.class b/target/classes/com/spaceshipproject/LoadedSpaceship.class new file mode 100644 index 0000000..a286342 Binary files /dev/null and b/target/classes/com/spaceshipproject/LoadedSpaceship.class differ diff --git a/target/classes/com/spaceshipproject/ModelManager$ModelEditSession.class b/target/classes/com/spaceshipproject/ModelManager$ModelEditSession.class new file mode 100644 index 0000000..d9bdfb7 Binary files /dev/null and b/target/classes/com/spaceshipproject/ModelManager$ModelEditSession.class differ diff --git a/target/classes/com/spaceshipproject/ModelManager.class b/target/classes/com/spaceshipproject/ModelManager.class new file mode 100644 index 0000000..940de70 Binary files /dev/null and b/target/classes/com/spaceshipproject/ModelManager.class differ diff --git a/target/classes/com/spaceshipproject/PlayerListener.class b/target/classes/com/spaceshipproject/PlayerListener.class new file mode 100644 index 0000000..1b43636 Binary files /dev/null and b/target/classes/com/spaceshipproject/PlayerListener.class differ diff --git a/target/classes/com/spaceshipproject/RemoteListener.class b/target/classes/com/spaceshipproject/RemoteListener.class new file mode 100644 index 0000000..b734f7b Binary files /dev/null and b/target/classes/com/spaceshipproject/RemoteListener.class differ diff --git a/target/classes/com/spaceshipproject/RemoteManager.class b/target/classes/com/spaceshipproject/RemoteManager.class new file mode 100644 index 0000000..2a3ac47 Binary files /dev/null and b/target/classes/com/spaceshipproject/RemoteManager.class differ diff --git a/target/classes/com/spaceshipproject/SchematicHelper.class b/target/classes/com/spaceshipproject/SchematicHelper.class new file mode 100644 index 0000000..a204e5b Binary files /dev/null and b/target/classes/com/spaceshipproject/SchematicHelper.class differ diff --git a/target/classes/com/spaceshipproject/ShipSizing.class b/target/classes/com/spaceshipproject/ShipSizing.class new file mode 100644 index 0000000..d633dd6 Binary files /dev/null and b/target/classes/com/spaceshipproject/ShipSizing.class differ diff --git a/target/classes/com/spaceshipproject/SpaceJumpGUI.class b/target/classes/com/spaceshipproject/SpaceJumpGUI.class new file mode 100644 index 0000000..0e2d071 Binary files /dev/null and b/target/classes/com/spaceshipproject/SpaceJumpGUI.class differ diff --git a/target/classes/com/spaceshipproject/SpaceShipAdminCommand.class b/target/classes/com/spaceshipproject/SpaceShipAdminCommand.class new file mode 100644 index 0000000..4447baa Binary files /dev/null and b/target/classes/com/spaceshipproject/SpaceShipAdminCommand.class differ diff --git a/target/classes/com/spaceshipproject/SpaceShipCommand.class b/target/classes/com/spaceshipproject/SpaceShipCommand.class new file mode 100644 index 0000000..17e6cd0 Binary files /dev/null and b/target/classes/com/spaceshipproject/SpaceShipCommand.class differ diff --git a/target/classes/com/spaceshipproject/SpaceShipProject.class b/target/classes/com/spaceshipproject/SpaceShipProject.class new file mode 100644 index 0000000..63e9393 Binary files /dev/null and b/target/classes/com/spaceshipproject/SpaceShipProject.class differ diff --git a/target/classes/com/spaceshipproject/Spaceship.class b/target/classes/com/spaceshipproject/Spaceship.class new file mode 100644 index 0000000..130d366 Binary files /dev/null and b/target/classes/com/spaceshipproject/Spaceship.class differ diff --git a/target/classes/com/spaceshipproject/SpaceshipGUI.class b/target/classes/com/spaceshipproject/SpaceshipGUI.class new file mode 100644 index 0000000..2fd4160 Binary files /dev/null and b/target/classes/com/spaceshipproject/SpaceshipGUI.class differ diff --git a/target/classes/com/spaceshipproject/SpaceshipGUIHolder.class b/target/classes/com/spaceshipproject/SpaceshipGUIHolder.class new file mode 100644 index 0000000..1e07011 Binary files /dev/null and b/target/classes/com/spaceshipproject/SpaceshipGUIHolder.class differ diff --git a/target/classes/com/spaceshipproject/SpaceshipGUIListener.class b/target/classes/com/spaceshipproject/SpaceshipGUIListener.class new file mode 100644 index 0000000..0ba8929 Binary files /dev/null and b/target/classes/com/spaceshipproject/SpaceshipGUIListener.class differ diff --git a/target/classes/com/spaceshipproject/SpaceshipGuiPanel.class b/target/classes/com/spaceshipproject/SpaceshipGuiPanel.class new file mode 100644 index 0000000..1011998 Binary files /dev/null and b/target/classes/com/spaceshipproject/SpaceshipGuiPanel.class differ diff --git a/target/classes/com/spaceshipproject/SpaceshipManager$LoadedShipOwnerInfo.class b/target/classes/com/spaceshipproject/SpaceshipManager$LoadedShipOwnerInfo.class new file mode 100644 index 0000000..16f8519 Binary files /dev/null and b/target/classes/com/spaceshipproject/SpaceshipManager$LoadedShipOwnerInfo.class differ diff --git a/target/classes/com/spaceshipproject/SpaceshipManager.class b/target/classes/com/spaceshipproject/SpaceshipManager.class new file mode 100644 index 0000000..0b1108c Binary files /dev/null and b/target/classes/com/spaceshipproject/SpaceshipManager.class differ diff --git a/target/classes/config.yml b/target/classes/config.yml new file mode 100644 index 0000000..dfcf76d --- /dev/null +++ b/target/classes/config.yml @@ -0,0 +1,70 @@ +# ========================================================= +# SpaceShipProject - Configuration +# ========================================================= + +# Limite de spaceships par joueur (0 = illimite) +max-ships-per-player: 5 + +# Taille du spaceship a la creation (level 1) +default-size: + x: 4 + y: 4 + z: 6 + +# Taille maximale (atteinte avec les niveaux) +max-size: + x: 16 + y: 8 + z: 16 + +# Couts en money (Vault / EssentialsEconomy) +costs: + summon: 100 # cout pour charger un spaceship + unload: 0 # cout pour decharger + upgrade-base: 1000 # cout d'amelioration : upgrade-base * niveau actuel + space-jump-per-block: 1 # cout par bloc lors d'un saut spatial + +# Saut spatial +space-jump: + blocks-per-level: 100 # max blocs autorises = blocks-per-level * niveau + min-blocks: 1 + max-blocks-hard-cap: 5000 + +# Hauteur a laquelle le spaceship apparait au-dessus du joueur quand il est invoque +summon-offset-y: 30 + +# Materiau utilise pour la plateforme par defaut lors de la creation d'un nouveau spaceship +default-platform-material: SMOOTH_QUARTZ +default-edge-material: QUARTZ_PILLAR +default-glass-material: WHITE_STAINED_GLASS + +# Nom par defaut des nouveaux spaceships (%n% = numero) +default-ship-name: "Spaceship #%n%" + +# Verifications de securite +safety: + # Empecher l'invocation si des blocs solides sont presents a l'emplacement + check-collision: true + # Empecher la destruction du sol (le spaceship doit etre dans le ciel) + min-y-above-player: 10 + +# Messages (couleurs Minecraft, &x) +messages: + prefix: "&8[&bSpaceShip&8] &7" + no-permission: "&cVous n'avez pas la permission !" + not-owner: "&cCe spaceship ne vous appartient pas." + ship-given: "&aTelecommande du spaceship &b%name% &acreee et donnee." + ship-summoned: "&aSpaceship &b%name% &acharge dans le ciel !" + ship-unloaded: "&aSpaceship &b%name% &adecharge, modifications sauvegardees." + ship-teleport-up: "&aTeleportation sur le spaceship &b%name%&a..." + ship-teleport-down: "&aRedescente sur la terre ferme..." + ship-already-loaded: "&eVotre spaceship est deja charge." + ship-not-loaded: "&eVotre spaceship n'est pas charge." + ship-collision: "&cImpossible d'invoquer le spaceship ici : zone obstruee." + max-ships-reached: "&cVous avez atteint la limite de spaceships (%max%)." + ship-deleted: "&cSpaceship supprime." + not-enough-money: "&cFonds insuffisants : %need% requis, %have% disponible." + vault-unavailable: "&cVault/EssentialsEconomy est indisponible, action impossible." + space-jump-success: "&aSaut spatial : %dist% blocs vers %dir% (-%cost%)." + space-jump-not-on-ship: "&cVous devez etre sur le spaceship pour effectuer un saut." + space-jump-too-far: "&cDistance trop grande (max %max% au niveau %lvl%)." diff --git a/target/classes/plugin.yml b/target/classes/plugin.yml new file mode 100644 index 0000000..37e74b9 --- /dev/null +++ b/target/classes/plugin.yml @@ -0,0 +1,56 @@ +name: SpaceShipProject +main: com.spaceshipproject.SpaceShipProject +version: 1.0 +api-version: 1.21 +author: CreatorOfNothing +description: Spaceships personnels charges en plein ciel, telecommande boussole. +softdepend: [Vault, EssentialsX] + +commands: + spaceship: + description: Commandes joueur du plugin SpaceShipProject + usage: /spaceship + aliases: [ss, ship] + permission: spaceship.use + spaceshipadmin: + description: Commandes d'administration du plugin SpaceShipProject + usage: /spaceshipadmin + aliases: [ssa, shipadmin] + permission: spaceship.admin + +permissions: + spaceship.*: + description: Toutes les permissions de SpaceShipProject + children: + spaceship.use: true + spaceship.create: true + spaceship.give: true + spaceship.info.other: true + spaceship.delete: true + spaceship.reload: true + spaceship.admin: true + default: op + spaceship.use: + description: Utiliser sa propre telecommande + default: true + spaceship.create: + description: Creer un nouveau spaceship (/ss create) + default: true + spaceship.give: + description: Recuperer une telecommande perdue (la sienne ou celle d'un autre avec admin) + default: true + spaceship.info.other: + description: Voir les infos detaillees d'un autre joueur + default: op + spaceship.delete: + description: Supprimer un de ses spaceships + default: true + spaceship.reload: + description: Recharger la configuration + default: op + spaceship.admin: + description: Permissions d'administration (bypass de propriete, /ssa, etc.) + default: op + spaceship.bypass.cost: + description: Permet de ne pas payer les couts d'invocation / saut / upgrade + default: op diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties new file mode 100644 index 0000000..2742343 --- /dev/null +++ b/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=SpaceShipProject +groupId=com.spaceshipproject +version=1.0-SNAPSHOT diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..f90504a --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,23 @@ +com/spaceshipproject/RemoteManager.class +com/spaceshipproject/EconomyHook.class +com/spaceshipproject/SpaceShipProject.class +com/spaceshipproject/SpaceshipManager.class +com/spaceshipproject/BlockProtectionListener.class +com/spaceshipproject/ModelManager.class +com/spaceshipproject/ShipSizing.class +com/spaceshipproject/PlayerListener.class +com/spaceshipproject/Spaceship.class +com/spaceshipproject/RemoteListener.class +com/spaceshipproject/DatabaseManager.class +com/spaceshipproject/ConfigManager.class +com/spaceshipproject/LoadedSpaceship.class +com/spaceshipproject/SpaceShipAdminCommand.class +com/spaceshipproject/SpaceShipCommand.class +com/spaceshipproject/SchematicHelper.class +com/spaceshipproject/SpaceshipGUI.class +com/spaceshipproject/SpaceshipGUIHolder.class +com/spaceshipproject/SpaceshipManager$LoadedShipOwnerInfo.class +com/spaceshipproject/SpaceJumpGUI.class +com/spaceshipproject/ModelManager$ModelEditSession.class +com/spaceshipproject/SpaceshipGUIListener.class +com/spaceshipproject/SpaceshipGuiPanel.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..fa793e7 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,21 @@ +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/ConfigManager.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/SchematicHelper.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/ModelManager.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/PlayerListener.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/SpaceshipManager.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/Spaceship.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/SpaceshipGUIListener.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/EconomyHook.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/SpaceJumpGUI.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/SpaceShipProject.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/SpaceshipGUI.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/SpaceshipGUIHolder.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/ShipSizing.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/SpaceshipGuiPanel.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/RemoteManager.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/SpaceShipCommand.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/RemoteListener.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/DatabaseManager.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/SpaceShipAdminCommand.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/BlockProtectionListener.java +/home/bat/github/mcplugin/SpaceShipProject/src/main/java/com/spaceshipproject/LoadedSpaceship.java diff --git a/target/original-SpaceShipProject-1.0-SNAPSHOT.jar b/target/original-SpaceShipProject-1.0-SNAPSHOT.jar new file mode 100644 index 0000000..05a3a0f Binary files /dev/null and b/target/original-SpaceShipProject-1.0-SNAPSHOT.jar differ