diff --git a/.gitignore b/.gitignore index 18e7f9c..2e7cb99 100644 --- a/.gitignore +++ b/.gitignore @@ -43,9 +43,14 @@ libs/ # Local config overrides config.local.json +# Local build setup (multi-project wiring is machine-specific) +settings.gradle +gradle.properties + # AI Files CLAUDE.md .claude/ +.serena/ .cursorrules .cursorignore .cursor/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 345e36c..77ae0c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,320 @@ All notable changes to HyperEssentials will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +#### GUI Foundation Infrastructure +- `GuiColors` — centralized semantic color constants (brand gold, text, status, backgrounds, dividers) +- `UIPaths` — centralized UI template path constants for all shared, player, and admin pages +- `PlayerPageData` — BuilderCodec for player page events (Button, NavTarget, Target, Page) +- `AdminPageData` — BuilderCodec for admin page events (Button, NavTarget, Target, Value, Filter, Page) +- `PlayerPageOpener` / `AdminPageOpener` — static utilities to resolve and open pages from registries +- `GuiManager.openPlayerPage()` / `openAdminPage()` — convenience methods for page opening +- `NavBarHelper.setupAdminBar()` — admin nav bar variant with "HE Admin" title +- `NavBarHelper.handleNavEvent()` — overload with `GuiType` for admin/player nav routing +- `UIHelper.formatPlaytime()` — human-readable playtime formatting (Xd Xh Xm) +- `hyperessentials.admin.gui` permission node for admin panel access +- `/he` (no args) now opens player dashboard GUI (falls back to help text if no pages registered) +- `/he admin` opens admin dashboard GUI (requires `admin.gui` permission) +- Shared `.ui` templates: `styles.ui` (HyperFactions-quality TextButtonStyle definitions), `empty_state.ui`, `confirm_modal.ui`, `stat_row.ui` + +#### Player GUI Pages — Homes, Warps, Kits +- `HomesPage` — browse homes, teleport (with warmup), delete; shows count/limit header +- `WarpsPage` — browse warps grouped by category with category headers, teleport with warmup +- `KitsPage` — browse available kits, claim with cooldown status display, preview items +- `.ui` templates: `homes_page.ui`, `home_entry.ui`, `warps_page.ui`, `warp_entry.ui`, `warp_category_header.ui`, `kits_page.ui`, `kit_entry.ui` +- `/homes`, `/warps`, `/kits` commands now open GUI pages (text fallback preserved) +- Page registration in `HyperEssentials.registerPages()` with correct display order (Homes=10, Warps=20, Kits=30) + +#### Admin GUI Pages — Dashboard, Warps, Spawns, Kits +- `AdminDashboardPage` — server overview with online count, warp/spawn/kit stats, module status grid (enabled/disabled indicators) +- `AdminWarpsPage` — list all warps sorted by category, create at current location, delete +- `AdminSpawnsPage` — list all spawns with default badge, create at current location, delete +- `AdminKitsPage` — list all kits with item count/cooldown/one-time info, create from inventory, delete +- `.ui` templates: `admin_dashboard.ui`, `admin_module_card.ui`, `admin_warps.ui`, `admin_warp_entry.ui`, `admin_spawns.ui`, `admin_spawn_entry.ui`, `admin_kits.ui`, `admin_kit_entry.ui` +- Admin page registration in `HyperEssentials.registerPages()`: Dashboard(0), Warps(20), Spawns(30), Kits(40) +- `/he admin` now navigable to all 4 admin tabs + +#### Player GUI Pages — Dashboard, TPA, Stats +- `PlayerDashboardPage` — welcome screen with stat cards (homes, online players, TPA requests), quick action buttons, playtime/join info +- `TpaPage` — incoming TPA requests with per-entry accept/deny, toggle TPA acceptance on/off, time remaining display +- `StatsPage` — player stats (first joined, last login, total playtime, current session) and status indicators (AFK, Fly, God, Infinite Stamina) +- `.ui` templates: `dashboard.ui`, `stats.ui`, `tpa_page.ui`, `tpa_entry.ui` +- `UtilityManager.getSessionStart()` — new public accessor for session start time +- All 6 player GUI tabs now functional: Dashboard(0), Homes(10), Warps(20), Kits(30), TPA(40), Stats(50) + +#### Admin GUI Pages — Players, Moderation, Announcements, Settings +- `AdminPlayersPage` — list online players sorted by name with UUID preview +- `AdminModerationPage` — punishment list with active/all filter, revoke functionality; type-colored badges (BAN=red, MUTE=gold, KICK=cyan) +- `AdminAnnouncementsPage` — read-only view of announcement messages, interval, and sequential/random mode +- `AdminSettingsPage` — version info, data directory, config reload button, module status grid with enabled/disabled indicators +- `ModerationManager.getAllPunishments(boolean)` — new method to query all punishments across all players (supports active-only filter) +- `.ui` templates: `admin_players.ui`, `admin_player_entry.ui`, `admin_moderation.ui`, `admin_punishment_entry.ui`, `admin_announcements.ui`, `admin_announcement_entry.ui`, `admin_settings.ui`, `admin_module_toggle.ui` +- Admin page registration: Players(10), Moderation(50), Announcements(60), Settings(70) +- Complete GUI system: 14 pages (6 player + 8 admin) all functional + +#### New Utility Commands +- `/motd` — display configurable message of the day +- `/rules` — display server rules +- `/discord` — display Discord invite link +- `/list` (aliases: `/online`, `/players`) — show sorted online player list with count +- `/playtime` (alias: `/pt`) — show total playtime including current session +- `/joindate` (alias: `/firstjoin`) — show first join timestamp +- `/afk` (alias: `/away`) — toggle AFK status with server-wide broadcast +- `/stamina` (alias: `/stam`) — toggle infinite stamina with periodic enforcement +- `/maxstack` (alias: `/stack`) — set held item quantity to max stack size +- `/sleeppercentage` (aliases: `/sleeppct`, `/spc`) — view/set sleep skip percentage threshold +- `/invsee ` — stub for inventory viewing (GUI coming later) +- `/trash` — stub for disposal inventory (GUI coming later) +- `/repairmax` (alias: `/fixmax`) — fully restore held item including max durability reset +- `/durability` (alias: `/dura`) — set or reset max durability on held items +- `/previewkit` (aliases: `/vkit`, `/viewkit`) — preview kit contents without claiming + +#### IP Ban System +- `/ipban [duration] [reason]` — ban player's IP with smart duration detection +- `/ipunban ` — unban an IP address +- `IpBan` record with expiration support (permanent and temporary) +- IP tracking on player connect via `ModerationManager` +- Connect-time IP ban enforcement in `ModerationListener` +- IP ban persistence in `ModerationStorage` (`"ipBans"` JSON section) +- Same-IP kick on ban (disconnects all players on the banned IP) + +#### Player Stats System +- `PlayerStats` record (uuid, username, firstJoin, totalPlaytimeMs, lastJoin) +- `PlayerStatsStorage` with JSON persistence at `data/playerstats.json` +- Session tracking in `UtilityManager` with playtime accumulation across sessions +- Connect handler infrastructure in `HyperEssentials` core (mirrors disconnect handler) + +#### AFK System +- Manual AFK toggle via `/afk` command with server-wide broadcast +- Auto-AFK detection via configurable idle timeout (`afkTimeoutSeconds`) +- Auto-unset AFK on player activity (chat, interact, mouse motion, movement) +- Activity tracking via `PlayerChatEvent`, `PlayerInteractEvent`, and `PlayerMouseMotionEvent` listeners +- Position-based movement detection via periodic polling (Hytale has no PlayerMoveEvent) + +#### Infinite Stamina System +- Toggle-based infinite stamina via `UtilityManager` state tracking +- Periodic enforcement via `ScheduledExecutorService` (every 1 second) +- Uses `EntityStatsModule` component with `DefaultEntityStatTypes.getStamina()` for targeted stat maximization +- Support for targeting other players with `stamina.others` permission + +#### Spawn Auto-Detection +- Auto-import spawn points from Hytale `WorldConfig` `ISpawnProvider` on fresh install +- `SpawnManager.importWorldSpawns()` for manual import via `/he importspawns` + +#### Expanded Help +- `/he help` subcommand with full command listing grouped by module +- Each module section only shown if the module is enabled + +#### Utility Config Expansion +- 12 new command enable toggles (motd, rules, discord, list, playtime, joindate, afk, invsee, stamina, trash, maxstack, sleepPercentage) +- Content fields: `motdLines`, `ruleLines`, `discordUrl` +- AFK config: `afkTimeoutSeconds` (default 300, 0 = disabled) +- Sleep config: `sleepPercentage` with per-world overrides + +#### New Permission Nodes +- `utility.motd`, `utility.playtime`, `utility.joindate`, `utility.afk` +- `utility.invsee`, `utility.stamina`, `utility.stamina.others` +- `utility.trash`, `utility.maxstack`, `utility.sleeppercentage` +- `moderation.ipban` + +#### Command Aliases +- Homes: `deletehome`/`rmhome`/`removehome`, `listhomes`/`homelist`, `createhome` +- Teleport: `tpy`, `tpc`, `tpn`, `tpt`, `tpr` +- Kits: `ckit`, `dkit` +- Utility: `cc`, `fix` +- Warps: `createwarp` +- Announcements: `bc` +- Moderation: `pun` + +### Changed + +#### Ban/Mute Consolidation +- `/ban [duration] [reason]` — unified syntax with smart duration detection (replaces `/ban` + `/tempban`) +- `/mute [duration] [reason]` — unified syntax with smart duration detection (replaces `/mute` + `/tempmute`) +- `tempban` alias added to `/ban`, `tempmute`/`tmute` aliases added to `/mute` + +#### Kits Overhaul +- Kit items now support 4 inventory sections (hotbar, storage, armor, utility) instead of flat slot numbering +- Inventory space pre-check with `INSUFFICIENT_SPACE` result before claiming +- Displaced armor/utility items automatically moved to hotbar/storage +- Backward-compatible deserialization from old flat-slot format + +#### Fly Command +- Replaced Creative/Adventure gamemode hack with proper `MovementManager.canFly` toggle +- Uses `UpdateMovementSettings` packet for client-side flight mode +- Cross-player fly toggle disabled pending entity ref resolution + +#### Repair Command +- Added "already full durability" check before repairing +- Uses `withDurability(maxDurability)` instead of `withRestoredDurability()` + +#### Warmup/Cooldown System +- Auto-completion via `ScheduledExecutorService` replaces manual polling +- Added `bypass.warmup` and `bypass.cooldown` permission checks +- Cancels existing warmup before starting a new one +- Clean shutdown lifecycle for plugin disable + +#### Teleport Commands +- All teleport commands use `executeTeleport` callback pattern with `onComplete` runnable +- Added `ref.isValid()` safety check before teleporting +- Switched to `Teleport.createForPlayer()` for proper API usage +- Store retrieval deferred to world thread for thread safety + +#### RTP Cave Avoidance +- Added `rtpSafetyAirAboveHead` config (default 10) to prevent underground RTP placement +- RtpManager scans for solid blocks above landing position + +### Removed + +#### Ban/Mute Consolidation +- `TempBanCommand.java` — merged into `BanCommand` +- `TempMuteCommand.java` — merged into `MuteCommand` + +### Fixed +- Infinite stamina enforcement now dispatches to world thread via `world.execute()` (fixes `PlayerRef.getComponent() called async` error spam) +- AFK status now properly clears on player movement (added position polling and mouse motion listener) +- `/heal` only maximizes health stat instead of all stats (avoids unnecessary regen visual effects) +- `/stamina` maximize uses `DefaultEntityStatTypes.getStamina()` instead of iterating all stats +- Manifest `IncludesAssetPack` set to false (plugin has no bundled assets) + +--- + +### Added (prior releases below) + +#### RTP Overhaul +- Chunk-based random location selection with multi-attempt safety verification +- `findSafeY()` heightmap scan with `BlockMaterial` + fluid-aware safety checks (avoidWater, avoidDangerousFluids) +- Faction claim avoidance with configurable chunk buffer radius via `HyperFactionsIntegration` +- `BorderHook` interface for future HyperBorder world border enforcement +- `RtpChunkUtil` utility for 32-block chunk coordinate math +- `RtpResult` sealed interface with `Success(Location)` and `Failure(String)` records +- `playerRelative` config option — center RTP ring on player position (default) or fixed `centerX/Z` +- `rtp.factionAvoidance` config subsection (`enabled`, `bufferRadius`) +- `rtp.safety` config subsection (`avoidWater`, `avoidDangerousFluids`, `minY`, `maxY`) +- `hyperessentials.rtp.bypass.factions` permission node +- "Searching for a safe random location..." feedback message before search begins +- Found coordinates shown in warmup message: "Found location at (X, Y, Z). Teleporting in Ns..." + +#### Documentation +- Updated README with complete module table (status, default state), optional dependencies, command quick reference, and docs index +- Updated architecture docs with full package tree including all data records, modules, managers, commands, listeners, and migration framework +- Updated commands docs with all 46 commands across 9 modules (kits, moderation, utility, announcements) +- Updated integrations docs with HyperFactions territory checker, VaultUnlocked economy, LuckPerms, and current Ecotale/Werchat status +- Updated modules docs with correct config file paths, implementation status, and debug configuration section +- Updated storage docs with kits/punishments storage, complete data directory layout, and migration system +- Updated docs index (readme.md) with completion status for all documentation pages + +### Changed + +#### RTP Overhaul +- RTP search now runs on world thread via `currentWorld.execute()` for chunk/block access +- Player position obtained via `TransformComponent` (matches HyperFactions stuck pattern) +- Replaced `java.util.Random` with `ThreadLocalRandom` +- `maxAttempts` config now actually used (was previously ignored) +- Y coordinate resolved from heightmap scan instead of hardcoded 64 + +### Removed + +#### RTP Overhaul +- `findRandomLocation(String worldName)` method (replaced by `findSafeRandomLocation()`) + +### Added (prior work) + +#### Homes Module +- `Home` immutable record with timestamps, `create()` factory, `withLastUsed()` and `withLocation()` update methods +- `PlayerHomes` collection with case-insensitive lookup (lowercase keys, original casing preserved) +- `HomeManager` with CRUD operations, player cache lifecycle (load on connect, flush on disconnect), permission-based limit resolution (unlimited → limit.N → config default), and bed sync +- `Location.fromHome()` factory method for consistency with existing `fromWarp()` and `fromSpawn()` +- `HomeStorage` interface with `loadPlayerHomes()` and `savePlayerHomes()` methods +- `JsonHomeStorage` implementation with per-player JSON files in `data/players/homes/`, atomic writes +- `/sethome [name]` — set a home with name validation (`[a-zA-Z0-9_-]{1,32}`), faction territory check, and limit enforcement +- `/home [name]` — teleport to a home with faction territory check on destination, warmup/cooldown via WarmupManager +- `/delhome ` — delete a home with available homes listed on error +- `/homes` — list all homes with count/limit display (e.g. "Your homes (2/3)") + +#### HyperFactions Integration +- HyperFactions added to `manifest.json` SoftDependencies for load ordering +- `FactionTerritoryChecker` with `Result` enum (ALLOWED, BLOCKED_OWN/ALLY/ENEMY/NEUTRAL/WILDERNESS) and `canUseHome()` static method +- `HomesConfig` factions subsection with per-relationship toggles: `enabled` (master toggle), `allowInOwnTerritory`, `allowInAllyTerritory`, `allowInNeutralTerritory`, `allowInEnemyTerritory`, `allowInWilderness` +- Faction bypass permissions: `bypass.factions` (wildcard), `bypass.factions.sethome`, `bypass.factions.home` +- `HOME_TELEPORT` and `HOME_LIMIT_PREFIX` permission constants + +#### Infrastructure +- Checkstyle linting (10.26.1) with 2-space indent, 120-char lines, relaxed Javadoc rules +- Category-based debug logging via `Logger.DebugCategory` (12 categories: homes, warps, spawns, teleport, kits, moderation, utility, rtp, announcements, integration, economy, storage) +- `DebugConfig` module config with per-category toggle and `applyToLogger()` +- Migration framework: `Migration` interface, `MigrationType` enum, `MigrationOptions`, `MigrationResult`, `MigrationRegistry`, `MigrationRunner` with ZIP backup and rollback +- VaultUnlocked economy integration via `VaultEconomyProvider` (reflection-based lazy init) +- Standard directory structure: `config/`, `data/`, `data/players/`, `backups/`, `data/.version` marker +- `ConfigFile.hasNewKeys()` auto-detection for missing config keys on load + +### Changed + +#### Infrastructure +- Reformatted all Java files to 2-space indentation (aligned with HyperFactions code style) +- Logger upgraded from `java.util.logging.Logger` to HytaleLogger (Flogger) with `logger.at(Level).log()` pattern +- RTP module merged into Teleport module (RtpManager, RtpCommand moved to `module.teleport` package) +- RTP config merged into `TeleportConfig` under `"rtp"` subsection +- `EcotaleIntegration` now uses reflection detection instead of stub +- `manifest.json` soft dependencies expanded: HyperPerms, VaultUnlocked, LuckPerms + +### Removed +- Standalone RTP module (`module.rtp` package, `RtpConfig`) + +### Added (continued) + +#### Infrastructure (from prior work) +- `DurationParser` utility for parsing human-readable durations ("1h30m", "7d") and formatting +- Categorized `Permissions` constants: KIT, MODERATION, UTILITY, ANNOUNCE, BYPASS, NOTIFY with wildcards +- `CommandUtil.findOnlinePlayer()` for case-insensitive online player lookup +- Disconnect handler system in `HyperEssentials` core for session cleanup callbacks + +#### Kits Module +- `Kit` and `KitItem` data records for kit definitions +- `KitStorage` for JSON persistence of kit definitions (`data/kits.json`) +- `KitManager` with CRUD operations, cooldown tracking, one-time claim tracking, and inventory capture +- `/kit ` — claim a kit with permission and cooldown checks +- `/kits` — list available kits filtered by player permissions +- `/createkit ` — capture current inventory as a new kit definition +- `/deletekit ` — remove a kit definition +- `KitsConfig` with `defaultCooldownSeconds` and `oneTimeDefault` settings + +#### Moderation Module +- `Punishment` record with ban/mute/kick types, expiry, and revocation tracking +- `ModerationStorage` for JSON persistence of punishment history (`data/punishments.json`) +- `ModerationManager` for ban/mute/kick operations with broadcast and staff notifications +- `FreezeManager` with position-locked movement checking via `ScheduledExecutorService` +- `VanishManager` using `HiddenPlayersManager` with fake disconnect/connect messages +- `ModerationListener` for ban enforcement on connect and mute enforcement on chat +- `/ban`, `/tempban`, `/unban` — permanent and temporary bans with reason support +- `/mute`, `/tempmute`, `/unmute` — permanent and temporary mutes +- `/kick` — kick with custom reason, broadcast, and staff notifications +- `/freeze` — toggle position freeze with movement prevention +- `/vanish` — toggle vanish with configurable fake leave/join messages +- `/punishments ` — view punishment history +- `ModerationConfig` with default messages, broadcast toggles, freeze interval, history limits +- `VanishConfig` with fake message toggles and vanish messages + +#### Utility Module +- `UtilityManager` for session-only fly/god state tracking (cleared on disconnect) +- `/heal [player]` — heal via `EntityStatsModule` stat maximization +- `/fly [player]` — toggle Creative/Adventure mode as flight workaround +- `/god [player]` — toggle `Invulnerable` ECS component +- `/clearchat [player]` — clear chat with configurable line count +- `/clearinventory [player]` (alias `/ci`) — clear inventory via `Player` component +- `/repair` — repair held item durability via `withRestoredDurability()` +- `/near [radius]` — list nearby players with configurable default/max radius +- `UtilityConfig` with per-command enable flags, radius limits, and chat line settings + +#### Announcements Module +- `AnnouncementScheduler` with configurable interval, sequential/random rotation +- `/broadcast ` — send formatted announcement to all players +- `/announce list|add|remove|reload` — manage announcement rotation +- `AnnouncementsConfig` with interval, randomize, prefix/message colors, and message list + ## [0.1.0] - 2026-02-18 ### Added diff --git a/README.md b/README.md index 72d7306..c63fe7c 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,20 @@ Modular server essentials for Hytale. Part of the [HyperSystems](https://github. ## Features -HyperEssentials consolidates essential server functionality into a single modular plugin: - -| Module | Features | Status | -|--------|----------|--------| -| **Homes** | Home CRUD, sharing, bed sync | Planned | -| **Warps** | Warp CRUD, categories | Planned | -| **Spawns** | Spawn CRUD, per-world, respawn | Planned | -| **Teleport** | TPA, /back | Planned | -| **Warmup** | Universal warmup/cooldown system | Scaffold | -| **Kits** | Kit system | Planned | -| **Moderation** | Mute, temp ban, IP ban, freeze | Planned | -| **Vanish** | Vanish system | Planned | -| **Utility** | Clear chat, clear inventory, repair, near | Planned | -| **Announcements** | Broadcast/announcement system | Planned | -| **RTP** | Random teleport | Planned | - -Each module can be independently enabled or disabled via configuration. +HyperEssentials consolidates essential server functionality into a single modular plugin with 10 modules, 46 commands, and 50+ permission nodes. Each module can be independently enabled or disabled. + +| Module | Features | Default | Status | +|--------|----------|---------|--------| +| **Warmup** | Universal warmup/cooldown system for all teleport actions | Enabled | Implemented | +| **Homes** | Home CRUD, sharing, bed sync, faction territory restrictions | Enabled | Implemented | +| **Warps** | Warp CRUD, categories, custom permissions per warp | Enabled | Implemented | +| **Spawns** | Spawn CRUD, per-world spawns, group routing, respawn teleport | Enabled | Implemented | +| **Teleport** | TPA requests, /back history, random teleport (RTP) | Enabled | Implemented | +| **Kits** | Kit creation from inventory, cooldowns, one-time claims, per-kit permissions | Disabled | Implemented | +| **Moderation** | Ban, tempban, mute, tempmute, kick, freeze, vanish, punishment history | Disabled | Implemented | +| **Utility** | Heal, fly, god mode, clear chat, clear inventory, repair, near | Disabled | Implemented | +| **Announcements** | Scheduled broadcasts, announcement rotation, manual broadcast | Disabled | Implemented | +| **Vanish** | Standalone vanish module (vanish is also available via Moderation) | Enabled | Stub | ## Requirements @@ -29,7 +26,10 @@ Each module can be independently enabled or disabled via configuration. ## Optional Dependencies -- [HyperPerms](https://github.com/HyperSystems-Development/HyperPerms) - Advanced permission management +- [HyperPerms](https://github.com/HyperSystems-Development/HyperPerms) — Advanced permission management (chain-of-responsibility resolution) +- [HyperFactions](https://github.com/HyperSystems-Development/HyperFactions) — Territory-aware home restrictions +- [VaultUnlocked](https://github.com/TheNewEconomy/VaultUnlockedAPI) — Economy integration +- LuckPerms — Permission provider fallback ## Installation @@ -40,27 +40,56 @@ Each module can be independently enabled or disabled via configuration. ## Configuration -HyperEssentials uses a split configuration system: - -- `config.json` - Core settings (prefix, colors, admin) -- `config/homes.json` - Home module settings -- `config/warps.json` - Warp module settings -- `config/spawns.json` - Spawn module settings -- `config/teleport.json` - TPA module settings -- `config/warmup.json` - Warmup/cooldown settings -- `config/kits.json` - Kit module settings -- `config/moderation.json` - Moderation settings -- `config/vanish.json` - Vanish settings -- `config/utility.json` - Utility settings -- `config/announcements.json` - Announcement settings -- `config/rtp.json` - Random teleport settings +HyperEssentials uses a split configuration system with per-module config files: + +- `config.json` — Core settings (prefix, colors, admin, storage) +- `config/homes.json` — Home module (limits, bed sync, faction restrictions) +- `config/warps.json` — Warp module (default category) +- `config/spawns.json` — Spawn module (default spawn, join/respawn teleport) +- `config/teleport.json` — TPA and RTP settings (timeouts, back history, RTP radius) +- `config/warmup.json` — Per-module warmup/cooldown timers +- `config/kits.json` — Kit module (default cooldown, one-time claims) +- `config/moderation.json` — Moderation (default reasons, broadcast toggles, freeze interval) +- `config/vanish.json` — Vanish (fake join/leave messages) +- `config/utility.json` — Utility commands (per-command toggles, radius limits) +- `config/announcements.json` — Announcement rotation (interval, randomize, messages) +- `config/debug.json` — Debug logging (per-category toggles) ## Commands -| Command | Description | Permission | -|---------|-------------|------------| -| `/hessentials reload` | Reload configuration | `hyperessentials.admin.reload` | -| `/hessentials version` | Show version | - | +See [docs/commands.md](docs/commands.md) for the complete command reference with all 46 commands, permissions, and aliases. + +### Quick Reference + +| Module | Commands | +|--------|----------| +| Admin | `/hessentials reload\|version` | +| Homes | `/sethome`, `/home`, `/delhome`, `/homes` | +| Warps | `/warp`, `/setwarp`, `/delwarp`, `/warps`, `/warpinfo` | +| Spawns | `/spawn`, `/setspawn`, `/delspawn`, `/spawns`, `/spawninfo` | +| Teleport | `/tpa`, `/tpahere`, `/tpaccept`, `/tpdeny`, `/tpcancel`, `/tptoggle`, `/back`, `/rtp` | +| Kits | `/kit`, `/kits`, `/createkit`, `/deletekit` | +| Moderation | `/ban`, `/tempban`, `/unban`, `/mute`, `/tempmute`, `/unmute`, `/kick`, `/freeze`, `/vanish`, `/punishments` | +| Utility | `/heal`, `/fly`, `/god`, `/clearchat`, `/clearinventory`, `/repair`, `/near` | +| Announcements | `/broadcast`, `/announce` | + +## Permissions + +See [docs/permissions.md](docs/permissions.md) for the complete permission reference. All nodes use the `hyperessentials` prefix. + +## Documentation + +Detailed documentation is available in the [docs/](docs/) directory: + +| Document | Description | +|----------|-------------| +| [Architecture](docs/architecture.md) | Package structure, module lifecycle, cross-module communication | +| [Commands](docs/commands.md) | All 46 commands organized by module | +| [Config](docs/config.md) | Configuration reference for core and all modules | +| [Permissions](docs/permissions.md) | Permission nodes organized by module | +| [Modules](docs/modules.md) | Module system overview, enable/disable, lifecycle | +| [Storage](docs/storage.md) | Storage providers, JSON format, data directory layout | +| [Integrations](docs/integrations.md) | HyperPerms, HyperFactions, VaultUnlocked integration | ## Building diff --git a/build.gradle b/build.gradle index de51205..3f43ade 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,26 @@ plugins { id 'java' + id 'checkstyle' id 'com.gradleup.shadow' version '9.3.1' } group = 'com.hyperessentials' version = '0.1.0' +// --------------------------------------------------------------------------- +// Hytale Maven version resolution +// --------------------------------------------------------------------------- +def resolveHytaleVersion(String channel) { + def url = "https://maven.hytale.com/${channel}/com/hypixel/hytale/Server/maven-metadata.xml" + def text = new URL(url).text + def matcher = text =~ /(.+?)<\/release>/ + if (!matcher.find()) throw new GradleException("Could not resolve Hytale server version from ${url}") + return matcher.group(1) +} + +def hytaleChannel = project.findProperty('hytale_channel') ?: 'release' +ext.hytaleVersion = resolveHytaleVersion(hytaleChannel) + java { toolchain { languageVersion = JavaLanguageVersion.of(25) @@ -14,6 +29,10 @@ java { repositories { mavenCentral() + maven { + name = 'hytale' + url = "https://maven.hytale.com/${hytaleChannel}" + } } dependencies { @@ -95,3 +114,16 @@ tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' options.compilerArgs.addAll(['-Xlint:all', '-Xlint:-processing']) } + +checkstyle { + toolVersion = '10.26.1' + configFile = file('config/checkstyle/checkstyle.xml') + ignoreFailures = true +} + +tasks.withType(Checkstyle).configureEach { + reports { + xml.required = true + html.required = true + } +} diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..3b45fed --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,472 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/architecture.md b/docs/architecture.md index a343bf6..06e000f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,7 +1,5 @@ # Architecture -> **Status:** Scaffold only — details will be filled in as modules are implemented. - ## Overview HyperEssentials is a modular server essentials plugin. Each feature area (homes, warps, spawns, etc.) is encapsulated as a **module** that can be independently enabled or disabled. @@ -11,7 +9,7 @@ HyperEssentials is a modular server essentials plugin. Each feature area (homes, ``` com.hyperessentials/ HyperEssentials.java Core singleton — lifecycle, subsystem orchestration - Permissions.java All permission node constants + Permissions.java All permission node constants (organized by module) BuildInfo.java Auto-generated version info platform/ @@ -28,33 +26,124 @@ com.hyperessentials/ ValidationResult.java Config validation collector CoreConfig.java Root plugin settings ConfigManager.java Singleton config orchestrator - modules/ Per-module config classes (11 files) + modules/ Per-module config classes (12 files) + AnnouncementsConfig.java + DebugConfig.java + HomesConfig.java + KitsConfig.java + ModerationConfig.java + SpawnsConfig.java + TeleportConfig.java (includes RTP subsection) + UtilityConfig.java + VanishConfig.java + WarmupConfig.java + WarpsConfig.java + + data/ + Home.java Immutable home record (name, world, xyz, yaw, pitch, timestamps) + Location.java Immutable location record with factory methods + PlayerHomes.java Per-player home collection (case-insensitive lookup) + PlayerTeleportData.java TPA toggle, back history, last TPA timestamp + Spawn.java Immutable spawn record (name, world, xyz, permission, group, default) + TeleportRequest.java TPA/TPAHere request record (expiry tracking) + Warp.java Immutable warp record (name, category, permission, description) module/ Module.java Module interface AbstractModule.java Base implementation ModuleRegistry.java Lifecycle manager - homes/ Home management module - warps/ Warp management module - spawns/ Spawn management module - teleport/ TPA and /back module - warmup/ Universal warmup/cooldown - kits/ Kit system module - moderation/ Moderation tools module - vanish/ Vanish module - utility/ Utility commands module - announcements/ Broadcast module - rtp/ Random teleport module + homes/ + HomesModule.java Module entry point + HomeManager.java CRUD, cache, limit resolution, bed sync + command/ SetHomeCommand, HomeCommand, DelHomeCommand, HomesCommand + warps/ + WarpsModule.java + WarpManager.java CRUD, access control (custom warp permissions) + command/ SetWarpCommand, WarpCommand, DelWarpCommand, WarpsCommand, WarpInfoCommand + spawns/ + SpawnsModule.java + SpawnManager.java CRUD, group routing, default spawn resolution + command/ SetSpawnCommand, SpawnCommand, DelSpawnCommand, SpawnsCommand, SpawnInfoCommand + teleport/ + TeleportModule.java + TpaManager.java TPA request lifecycle, expiry, pending limits + BackManager.java Back location history (per-player ring buffer) + RtpManager.java Random location generation (ring radius) + command/ TpaCommand, TpaHereCommand, TpAcceptCommand, TpDenyCommand, + TpCancelCommand, TpToggleCommand, BackCommand, RtpCommand + warmup/ + WarmupModule.java + WarmupManager.java Per-player warmup tasks + WarmupTask.java Individual warmup tracking + CooldownTracker.java Post-teleport cooldown tracking + kits/ + KitsModule.java + KitManager.java CRUD, cooldown/one-time tracking, inventory capture + data/ + Kit.java Kit definition record + KitItem.java Kit item data + storage/ + KitStorage.java JSON persistence (data/kits.json) + command/ KitCommand, KitsCommand, CreateKitCommand, DeleteKitCommand + moderation/ + ModerationModule.java + ModerationManager.java Ban/mute/kick operations, broadcasts, notifications + FreezeManager.java Position-lock via 100ms check loop + ECS teleport + VanishManager.java HiddenPlayersManager API + fake join/leave messages + data/ + Punishment.java Punishment record (type, expiry, revocation) + PunishmentType.java BAN, MUTE, KICK enum + listener/ + ModerationListener.java Ban enforcement on connect, mute enforcement on chat + storage/ + ModerationStorage.java JSON persistence (data/punishments.json) + command/ BanCommand, TempBanCommand, UnbanCommand, MuteCommand, + TempMuteCommand, UnmuteCommand, KickCommand, FreezeCommand, + VanishCommand, PunishmentsCommand + utility/ + UtilityModule.java + UtilityManager.java Session-only fly/god state tracking + command/ HealCommand, FlyCommand, GodCommand, ClearChatCommand, + ClearInventoryCommand, RepairCommand, NearCommand + announcements/ + AnnouncementsModule.java + AnnouncementScheduler.java Configurable interval, sequential/random rotation + command/ AnnounceCommand, BroadcastCommand + vanish/ + VanishModule.java Standalone vanish module (stub, not yet implemented) gui/ - GuiManager.java Central GUI hub + GuiManager.java Central GUI hub (openPlayerPage, openAdminPage) + GuiColors.java Semantic color constants (brand, text, status, backgrounds) + UIPaths.java Centralized UI template path constants PageRegistry.java Dynamic page registration - NavBarHelper.java Shared navigation bar + NavBarHelper.java Shared navigation bar (player + admin variants) + PlayerPageOpener.java Static utility to open player pages + AdminPageOpener.java Static utility to open admin pages ActivePageTracker.java Player-to-page tracking - UIHelper.java Formatting utilities + UIHelper.java Formatting utilities (coords, duration, playtime) RefreshablePage.java Push-refresh interface PageSupplier.java Page factory interface GuiType.java PLAYER / ADMIN enum + data/ + PlayerPageData.java BuilderCodec for player page events + AdminPageData.java BuilderCodec for admin page events + player/ + HomesPage.java Browse homes, teleport (warmup), delete + WarpsPage.java Browse warps by category, teleport (warmup) + KitsPage.java Browse kits, claim with cooldown, preview + PlayerDashboardPage.java Welcome screen, stat cards, quick actions + TpaPage.java Incoming TPA requests, accept/deny, toggle + StatsPage.java Playtime, join dates, session, status indicators + admin/ + AdminDashboardPage.java Server overview, stats, module grid + AdminWarpsPage.java Warp management (create/delete) + AdminSpawnsPage.java Spawn management (create/delete) + AdminKitsPage.java Kit management (create from inventory/delete) + AdminPlayersPage.java Online players list + AdminModerationPage.java Punishment list, filter, revoke + AdminAnnouncementsPage.java Announcement messages (read-only) + AdminSettingsPage.java Version, reload, module toggles storage/ StorageProvider.java Top-level storage interface @@ -63,26 +152,38 @@ com.hyperessentials/ SpawnStorage.java Spawn CRUD interface PlayerDataStorage.java Per-player data interface json/ - JsonStorageProvider.java JSON file implementation + JsonStorageProvider.java JSON file implementation (atomic writes) integration/ PermissionProvider.java Permission provider interface PermissionManager.java Chain-of-responsibility resolver HyperPermsProviderAdapter.java Reflection-based HyperPerms bridge - HyperFactionsIntegration.java Territory/relation lookups - EcotaleIntegration.java Economy hooks (stub) + HyperFactionsIntegration.java Territory/relation lookups (reflection) + FactionTerritoryChecker.java Home territory restriction logic + EcotaleIntegration.java Economy detection (reflection) WerchatIntegration.java Chat hooks (stub) + economy/ + VaultEconomyProvider.java VaultUnlocked 2 reflection integration + + listener/ + DeathListener.java Saves back-location on death + + migration/ + Migration.java Migration interface + MigrationType.java CONFIG, DATA, SCHEMA enum + MigrationOptions.java Migration parameters + MigrationResult.java Success/failure tracking + MigrationRegistry.java Version chain ordering + MigrationRunner.java Backup (ZIP) + execute + rollback on failure command/ AdminCommand.java /hessentials admin command util/ - CommandUtil.java Messaging and permission helpers - - data/ - Location.java Immutable location record + CommandUtil.java Messaging, permission helpers, player lookup util/ - Logger.java Wrapped logger with prefix + DurationParser.java Human-readable duration parsing ("1h30m", "7d") + Logger.java HytaleLogger wrapper with prefix and debug categories TimeUtil.java Duration formatting ``` @@ -91,8 +192,9 @@ com.hyperessentials/ 1. `HyperEssentialsPlugin.setup()` — creates core singleton 2. `HyperEssentialsPlugin.start()` — calls `HyperEssentials.enable()` 3. `HyperEssentials.enable()`: + - Runs pending migrations (backup + execute) - Loads config - - Initializes integrations + - Initializes integrations (HyperPerms, HyperFactions, VaultUnlocked) - Initializes storage - Registers all modules in `ModuleRegistry` - Enables modules whose config has `enabled = true` @@ -109,3 +211,4 @@ Modules communicate through: - **EventBus** — publish-subscribe for decoupled events - **GuiManager** — shared page registry for navigation - **WarmupManager** — centralized warmup/cooldown tracking +- **PermissionManager** — unified permission resolution across providers diff --git a/docs/commands.md b/docs/commands.md index 9d6aba2..cfac595 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,6 +1,6 @@ # Commands -> **Status:** Only the admin command is implemented. Module commands will be added as modules are built. +HyperEssentials provides 46 commands across 9 modules. All commands require their respective module to be enabled. ## Admin @@ -10,41 +10,109 @@ | `/hessentials reload` | Reload configuration | `hyperessentials.admin.reload` | | `/hessentials version` | Show version | - | -## Homes (Planned) +## Homes | Command | Description | Permission | |---------|-------------|------------| -| `/home [name]` | Teleport to a home | `hyperessentials.home` | -| `/sethome [name]` | Set a home | `hyperessentials.home.set` | +| `/sethome [name]` | Set a home at current location | `hyperessentials.home.set` | +| `/home [name]` | Teleport to a home | `hyperessentials.home.teleport` | | `/delhome ` | Delete a home | `hyperessentials.home.delete` | -| `/homes` | List homes / open GUI | `hyperessentials.home.list` | +| `/homes` | List all homes with count/limit | `hyperessentials.home.list` | -## Warps (Planned) +Home names are validated against `[a-zA-Z0-9_-]{1,32}`. Faction territory restrictions apply if HyperFactions is installed and configured. + +## Warps | Command | Description | Permission | |---------|-------------|------------| | `/warp ` | Teleport to a warp | `hyperessentials.warp` | -| `/setwarp ` | Create a warp | `hyperessentials.warp.set` | +| `/setwarp [category]` | Create or update a warp | `hyperessentials.warp.set` | | `/delwarp ` | Delete a warp | `hyperessentials.warp.delete` | -| `/warps` | List warps / open GUI | `hyperessentials.warp.list` | +| `/warps` | List all warps | `hyperessentials.warp.list` | +| `/warpinfo ` | View detailed warp info | `hyperessentials.warp.info` | + +Aliases: `/delwarp` = `/deletewarp`, `/rmwarp`, `/removewarp` -## Spawns (Planned) +## Spawns | Command | Description | Permission | |---------|-------------|------------| | `/spawn [name]` | Teleport to spawn | `hyperessentials.spawn` | -| `/setspawn [name]` | Set a spawn point | `hyperessentials.spawn.set` | +| `/setspawn [name] [--default]` | Set a spawn point | `hyperessentials.spawn.set` | | `/delspawn ` | Delete a spawn | `hyperessentials.spawn.delete` | -| `/spawns` | List spawns | `hyperessentials.spawn.list` | +| `/spawns` | List all spawns | `hyperessentials.spawn.list` | +| `/spawninfo ` | View detailed spawn info | `hyperessentials.spawn.info` | + +Aliases: `/delspawn` = `/deletespawn`, `/rmspawn`, `/removespawn` + +The `--default` flag on `/setspawn` marks the spawn as the server default. -## Teleport (Planned) +## Teleport | Command | Description | Permission | |---------|-------------|------------| | `/tpa ` | Request teleport to player | `hyperessentials.tpa` | | `/tpahere ` | Request player teleport to you | `hyperessentials.tpahere` | -| `/tpaccept` | Accept teleport request | `hyperessentials.tpaccept` | -| `/tpdeny` | Deny teleport request | `hyperessentials.tpdeny` | +| `/tpaccept [player]` | Accept teleport request | `hyperessentials.tpaccept` | +| `/tpdeny [player]` | Deny teleport request | `hyperessentials.tpdeny` | | `/tpcancel` | Cancel outgoing request | `hyperessentials.tpcancel` | | `/tptoggle` | Toggle TPA requests | `hyperessentials.tptoggle` | | `/back` | Return to previous location | `hyperessentials.back` | +| `/rtp` | Teleport to a random location | `hyperessentials.rtp` | + +Aliases: `/tpaccept` = `/tpyes`, `/tpdeny` = `/tpno`, `/rtp` = `/randomtp`, `/randomteleport` + +RTP generates a random location within a configurable ring radius from a center point. + +## Kits + +| Command | Description | Permission | +|---------|-------------|------------| +| `/kit ` | Claim a kit | `hyperessentials.kit.use.` | +| `/kits` | List available kits | `hyperessentials.kit.list` | +| `/createkit ` | Create a kit from current inventory | `hyperessentials.kit.create` | +| `/deletekit ` | Delete a kit definition | `hyperessentials.kit.delete` | + +Kits support per-kit cooldowns, one-time claims, and custom permission overrides. + +## Moderation + +| Command | Description | Permission | +|---------|-------------|------------| +| `/ban [reason]` | Permanently ban a player | `hyperessentials.moderation.ban` | +| `/tempban [reason]` | Temporarily ban a player | `hyperessentials.moderation.ban` | +| `/unban ` | Unban a player | `hyperessentials.moderation.ban` | +| `/mute [reason]` | Permanently mute a player | `hyperessentials.moderation.mute` | +| `/tempmute [reason]` | Temporarily mute a player | `hyperessentials.moderation.mute` | +| `/unmute ` | Unmute a player | `hyperessentials.moderation.mute` | +| `/kick [reason]` | Kick a player | `hyperessentials.moderation.kick` | +| `/freeze ` | Toggle position freeze | `hyperessentials.moderation.freeze` | +| `/vanish` | Toggle vanish (fake leave/join) | `hyperessentials.moderation.vanish` | +| `/punishments ` | View punishment history | `hyperessentials.moderation.history` | + +Duration format: `1h`, `30m`, `7d`, `1h30m` (parsed by `DurationParser`). + +## Utility + +| Command | Description | Permission | +|---------|-------------|------------| +| `/heal [player]` | Heal self or another player | `hyperessentials.utility.heal` / `.heal.others` | +| `/fly [player]` | Toggle flight for self or another | `hyperessentials.utility.fly` / `.fly.others` | +| `/god [player]` | Toggle god mode (invulnerability) | `hyperessentials.utility.god` / `.god.others` | +| `/clearchat` | Clear chat history | `hyperessentials.utility.clearchat` / `.clearchat.others` | +| `/clearinventory [player]` | Clear inventory | `hyperessentials.utility.clearinventory` / `.clearinventory.others` | +| `/repair` | Repair held item | `hyperessentials.utility.repair` | +| `/near [radius]` | List nearby players | `hyperessentials.utility.near` | + +Aliases: `/clearinventory` = `/ci` + +Each utility command can be individually enabled/disabled in `config/utility.json`. + +## Announcements + +| Command | Description | Permission | +|---------|-------------|------------| +| `/broadcast ` | Send a formatted broadcast | `hyperessentials.announce.broadcast` | +| `/announce list\|add\|remove\|reload` | Manage announcement rotation | `hyperessentials.announce.manage` | + +Announcements can also run automatically on a configurable interval with sequential or random rotation. diff --git a/docs/config.md b/docs/config.md index 53e80f9..894867f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,6 +1,6 @@ # Configuration -> **Status:** Scaffold — config classes exist and will generate default files on first run. +All config files use JSON and are generated with defaults on first run. ## File Structure @@ -11,14 +11,14 @@ mods/com.hyperessentials_HyperEssentials/ homes.json Home module warps.json Warp module spawns.json Spawn module - teleport.json TPA module + teleport.json TPA and RTP settings warmup.json Warmup/cooldown kits.json Kit module moderation.json Moderation module vanish.json Vanish module utility.json Utility module announcements.json Announcement module - rtp.json Random teleport module + debug.json Debug logging categories ``` ## Core Config (`config.json`) @@ -29,15 +29,145 @@ mods/com.hyperessentials_HyperEssentials/ | `prefixColor` | string | `"#FFAA00"` | Prefix text color (hex) | | `prefixBracketColor` | string | `"#AAAAAA"` | Bracket color (hex) | | `primaryColor` | string | `"#55FFFF"` | Primary accent color | -| `secondaryColor` | string | `"#FFAA00"` | Secondary accent color | +| `secondaryColor` | string | `"#55FF55"` | Secondary accent color | | `errorColor` | string | `"#FF5555"` | Error message color | | `adminRequiresOp` | boolean | `true` | Require OP for admin commands | -| `allowWithoutPermissionMod` | boolean | `true` | Allow commands without HyperPerms | -| `storageType` | string | `"json"` | Storage backend | -| `updateCheckEnabled` | boolean | `true` | Check for updates | +| `allowWithoutPermissionMod` | boolean | `true` | Allow commands without a permission plugin | +| `storageType` | string | `"json"` | Storage backend (only `"json"` supported) | +| `updateCheck` | boolean | `true` | Check for updates on startup | +| `configVersion` | int | `1` | Config version for migrations | ## Module Configs Each module config has an `enabled` field. Set to `false` to disable the module entirely. -See individual module documentation for config details (to be added as modules are implemented). +### Homes (`config/homes.json`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `defaultHomeLimit` | int | `3` | Default home limit per player | +| `bedSyncEnabled` | boolean | `true` | Sync bed interaction with a "bed" home | +| `bedHomeName` | string | `"bed"` | Name for the bed-synced home | +| `shareEnabled` | boolean | `true` | Allow players to share homes | +| `maxSharesPerHome` | int | `10` | Maximum shares per home | +| `factions.enabled` | boolean | `true` | Enable faction territory restrictions | +| `factions.allowInOwnTerritory` | boolean | `true` | Allow homes in own faction territory | +| `factions.allowInAllyTerritory` | boolean | `true` | Allow homes in ally territory | +| `factions.allowInNeutralTerritory` | boolean | `true` | Allow homes in neutral territory | +| `factions.allowInEnemyTerritory` | boolean | `false` | Allow homes in enemy territory | +| `factions.allowInWilderness` | boolean | `true` | Allow homes in wilderness | + +### Warps (`config/warps.json`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `defaultCategory` | string | `"general"` | Default category for new warps | + +### Spawns (`config/spawns.json`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `defaultSpawnName` | string | `"spawn"` | Name of the default spawn | +| `teleportOnJoin` | boolean | `false` | Teleport players to spawn on join | +| `teleportOnRespawn` | boolean | `true` | Teleport players to spawn on respawn | +| `perWorldSpawns` | boolean | `false` | Use per-world spawn points | + +### Teleport (`config/teleport.json`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `tpaTimeout` | int | `60` | TPA request expiry in seconds | +| `tpaCooldown` | int | `30` | Cooldown between TPA requests in seconds | +| `maxPendingTpa` | int | `5` | Max pending incoming TPA requests per player | +| `backHistorySize` | int | `5` | Number of back locations to remember | +| `saveBackOnDeath` | boolean | `true` | Save location on death for /back | +| `saveBackOnTeleport` | boolean | `true` | Save location on teleport for /back | +| `rtp.centerX` | int | `0` | RTP center X coordinate (used when `playerRelative` is false) | +| `rtp.centerZ` | int | `0` | RTP center Z coordinate (used when `playerRelative` is false) | +| `rtp.minRadius` | int | `100` | Minimum RTP radius | +| `rtp.maxRadius` | int | `5000` | Maximum RTP radius | +| `rtp.maxAttempts` | int | `10` | Max safe-location search attempts | +| `rtp.playerRelative` | boolean | `true` | Center search ring on player (true) or `centerX/Z` (false) | +| `rtp.blacklistedWorlds` | list | `[]` | Worlds where RTP is disabled | +| `rtp.factionAvoidance.enabled` | boolean | `true` | Skip locations near faction claims | +| `rtp.factionAvoidance.bufferRadius` | int | `2` | Chunk buffer radius around claims (2 = 5x5 check area) | +| `rtp.safety.avoidWater` | boolean | `true` | Skip locations with any fluid at feet/head | +| `rtp.safety.avoidDangerousFluids` | boolean | `true` | Skip locations near lava/tar/poison below ground | +| `rtp.safety.minY` | int | `5` | Minimum Y level (avoid bedrock) | +| `rtp.safety.maxY` | int | `300` | Maximum Y level for heightmap scan | + +### Warmup (`config/warmup.json`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `cancelOnMove` | boolean | `true` | Cancel warmup on player movement | +| `cancelOnDamage` | boolean | `true` | Cancel warmup on damage | +| `safeTeleport` | boolean | `true` | Find safe location on teleport | +| `safeRadius` | int | `3` | Safe teleport search radius | +| `modules.homes` | object | `{warmup: 3, cooldown: 5}` | Homes warmup/cooldown (seconds) | +| `modules.warps` | object | `{warmup: 3, cooldown: 5}` | Warps warmup/cooldown | +| `modules.spawns` | object | `{warmup: 3, cooldown: 5}` | Spawns warmup/cooldown | +| `modules.teleport` | object | `{warmup: 3, cooldown: 5}` | TPA warmup/cooldown | +| `modules.rtp` | object | `{warmup: 5, cooldown: 30}` | RTP warmup/cooldown | + +### Kits (`config/kits.json`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `defaultCooldownSeconds` | int | `300` | Default kit cooldown (5 minutes) | +| `oneTimeDefault` | boolean | `false` | Default one-time claim setting for new kits | + +### Moderation (`config/moderation.json`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `defaultBanReason` | string | (default) | Default ban reason text | +| `defaultMuteReason` | string | (default) | Default mute reason text | +| `defaultKickReason` | string | (default) | Default kick reason text | +| `mutedChatMessage` | string | (default) | Message shown to muted players | +| `freezeMessage` | string | (default) | Message shown to frozen players | +| `freezeCheckIntervalMs` | int | `100` | Freeze position-check interval (ms) | +| `broadcastBans` | boolean | `true` | Broadcast ban messages server-wide | +| `broadcastKicks` | boolean | `true` | Broadcast kick messages server-wide | +| `broadcastMutes` | boolean | `false` | Broadcast mute messages server-wide | +| `maxHistoryPerPlayer` | int | `50` | Maximum punishment records per player | + +### Vanish (`config/vanish.json`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `fakeLeaveMessage` | boolean | `true` | Send fake leave message on vanish | +| `fakeJoinMessage` | boolean | `true` | Send fake join message on unvanish | +| `vanishEnableMessage` | string | (default) | Message shown when vanishing | +| `vanishDisableMessage` | string | (default) | Message shown when unvanishing | +| `silentJoin` | boolean | `false` | Auto-vanish on join | + +### Utility (`config/utility.json`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `healEnabled` | boolean | `true` | Enable /heal | +| `flyEnabled` | boolean | `true` | Enable /fly | +| `godEnabled` | boolean | `true` | Enable /god | +| `clearChatEnabled` | boolean | `true` | Enable /clearchat | +| `clearInventoryEnabled` | boolean | `true` | Enable /clearinventory | +| `repairEnabled` | boolean | `true` | Enable /repair | +| `nearEnabled` | boolean | `true` | Enable /near | +| `defaultNearRadius` | int | `200` | Default /near radius | +| `maxNearRadius` | int | `1000` | Maximum /near radius | +| `clearChatLines` | int | `100` | Number of blank lines for /clearchat | + +### Announcements (`config/announcements.json`) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `intervalSeconds` | int | `300` | Auto-broadcast interval (5 minutes) | +| `randomize` | boolean | `false` | Randomize message order | +| `prefixText` | string | `"Announcement"` | Announcement prefix text | +| `prefixColor` | string | `"#FFAA00"` | Announcement prefix color | +| `messageColor` | string | `"#FFFFFF"` | Announcement message color | +| `messages` | list | `[]` | Announcement message rotation list | + +### Debug (`config/debug.json`) + +Per-category debug logging toggles. Categories: `homes`, `warps`, `spawns`, `teleport`, `kits`, `moderation`, `utility`, `rtp`, `announcements`, `integration`, `economy`, `storage`. diff --git a/docs/gui.md b/docs/gui.md index 34bf77b..7107669 100644 --- a/docs/gui.md +++ b/docs/gui.md @@ -1,15 +1,28 @@ # GUI System -> **Status:** Framework complete, no module pages implemented yet. +> **Status:** Complete — all 14 pages implemented (6 player + 8 admin). ## Overview -HyperEssentials provides a shared GUI framework that modules use to register interactive pages. The system supports a unified navigation bar across all pages. +HyperEssentials provides a dual GUI system (Player + Admin) with shared navigation, following the same patterns as HyperFactions. The system supports 14 total pages across two navigation contexts. + +**Player GUI (6 tabs):** Dashboard, Homes, Warps, Kits, TPA, Stats +**Admin GUI (8 tabs):** Dashboard, Players, Warps, Spawns, Kits, Moderation, Announcements, Settings + +### Opening GUIs + +| Command | Opens | +|---------|-------| +| `/he` (no args) | Player Dashboard (with nav bar to all player tabs) | +| `/homes` | Homes page directly | +| `/warps` | Warps page directly | +| `/kits` | Kits page directly | +| `/he admin` | Admin Dashboard (requires `admin.gui` permission) | ## Components ### GuiManager -Central hub managing two `PageRegistry` instances (player and admin) plus an `ActivePageTracker`. +Central hub managing two `PageRegistry` instances (player and admin) plus an `ActivePageTracker`. Provides convenience methods `openPlayerPage()` and `openAdminPage()` that delegate to `PlayerPageOpener` / `AdminPageOpener`. ### PageRegistry Dynamic registry where modules register GUI page entries. Each entry includes: @@ -19,12 +32,30 @@ Dynamic registry where modules register GUI page entries. Each entry includes: - `permission` — required permission (nullable) - `supplier` — page factory - `showsInNavBar` — whether to show in navigation -- `order` — sort order in nav bar +- `order` — sort order in nav bar (gaps of 10 for future inserts) When a module is disabled, its pages are automatically unregistered. +### Page Registration Order + +**Player pages:** Dashboard(0), Homes(10), Warps(20), Kits(30), TPA(40), Stats(50) +**Admin pages:** Dashboard(0), Players(10), Warps(20), Spawns(30), Kits(40), Moderation(50), Announcements(60), Settings(70) + +### PlayerPageOpener / AdminPageOpener +Static utilities to resolve a page from the registry, check permissions, and open it. `AdminPageOpener` additionally requires `admin.gui` permission. + ### NavBarHelper -Builds the shared navigation bar and handles navigation events. Filters entries by player permissions. +Builds the shared navigation bar and handles navigation events. Provides: +- `setupBar()` — player nav bar with "HyperEssentials" title +- `setupAdminBar()` — admin nav bar with "HE Admin" title +- `handleNavEvent()` — routes navigation with `GuiType` (PLAYER or ADMIN) + +### Event Data Classes + +| Class | Fields | Purpose | +|-------|--------|---------| +| `PlayerPageData` | button, navTarget, target, page | Player page events | +| `AdminPageData` | button, navTarget, target, value, filter, page | Admin page events (extra fields for search/filter) | ### ActivePageTracker Thread-safe tracker mapping players to their currently open page. Used for push-refresh updates. @@ -32,36 +63,235 @@ Thread-safe tracker mapping players to their currently open page. Used for push- ### RefreshablePage Interface for pages that support real-time content refresh without full page rebuild. +### GuiColors +Centralized semantic color constants: brand gold, text (primary/heading/muted/label), status (online/offline/active/inactive), semantic (success/danger/warning/info), backgrounds (dark/panel/card/nav), dividers. Includes helper methods `forModuleEnabled()` and `forOnlineStatus()`. + +### UIHelper +Formatting utilities: `formatCoords()`, `formatDuration()`, `formatRelativeTime()`, `formatLimit()`, `formatPlaytime()`, `truncate()`, `formatWorldName()`, `parseColorCodes()`. + ## UI Resources ``` Common/UI/Custom/HyperEssentials/ shared/ - styles.ui Color constants and reusable styles - nav_bar.ui Navigation bar template - nav_button.ui Inactive nav button - nav_button_active.ui Active nav button (highlighted) + styles.ui TextButtonStyle definitions (Gold, Green, Red, Aqua, Flat variants) + nav_bar.ui Navigation bar template with brand title + nav_button.ui Inactive nav button (MenuItem) + nav_button_active.ui Active nav button (TextButton, highlighted) error_page.ui Generic error display - homes/ (empty — future) - warps/ (empty — future) - spawns/ (empty — future) - teleport/ (empty — future) - admin/ (empty — future) - kits/ (empty — future) + empty_state.ui Empty list state (title + message) + confirm_modal.ui Confirmation modal (message + Confirm/Cancel buttons) + stat_row.ui Reusable label:value row for stats display + homes/ + homes_page.ui Browse homes list with count/limit header + home_entry.ui Home row: name, world, coords, Teleport/Delete buttons + warps/ + warps_page.ui Browse warps with category grouping + warp_entry.ui Warp row: name, category, world, coords, Teleport button + warp_category_header.ui Category section divider + kits/ + kits_page.ui Browse available kits with count header + kit_entry.ui Kit row: name, items, cooldown, Claim/Preview buttons + teleport/ + tpa_page.ui TPA requests list with toggle bar + tpa_entry.ui TPA request row: requester, type, time, Accept/Deny + player/ + dashboard.ui Welcome screen with stat cards and quick actions + stats.ui Player stats and status indicators + admin/ + admin_dashboard.ui Server overview with stats and module grid + admin_module_card.ui Module status card (name + enabled/disabled) + admin_warps.ui Manage warps list with create/delete + admin_warp_entry.ui Warp row: name, category, world, coords, Delete + admin_spawns.ui Manage spawns list with create/delete + admin_spawn_entry.ui Spawn row: name, default badge, world, coords, Delete + admin_kits.ui Manage kits list with create/delete + admin_kit_entry.ui Kit row: name, items, cooldown, one-time, Delete + admin_players.ui Online player list with search + admin_player_entry.ui Player row: name, UUID preview + admin_moderation.ui Punishment list with active/all filter + admin_punishment_entry.ui Punishment row: type, player, reason, expires, Revoke + admin_announcements.ui Announcement messages and settings + admin_announcement_entry.ui Message row: number, text + admin_settings.ui Version info, reload button, module status + admin_module_toggle.ui Module row: name, status dot, enabled/disabled text ``` -## Registering a Page +## Style System + +Styles follow HyperFactions' proven pattern using `TextButtonStyle(...)` tuple syntax with `$C.@DefaultSquareButtonDefaultBackground` native backgrounds: -Modules register pages in their `onEnable()`: +- `@ButtonStyle` — default neutral button +- `@GoldButtonStyle` — brand accent actions +- `@GreenButtonStyle` — success/confirm actions +- `@RedButtonStyle` — danger text actions +- `@AquaButtonStyle` — info actions +- `@InvisibleButtonStyle` — transparent click overlays +- `@DisabledButtonStyle` — grayed out, non-interactive +- `@FlatRedButtonStyle` — solid red background (delete/ban) +- `@FlatGreenButtonStyle` — solid green background (confirm) +- `@FlatGoldButtonStyle` — solid gold background (brand accent) + +## Registering Pages + +Pages are registered centrally in `HyperEssentials.registerPages()` after all module managers are initialized: ```java -guiManager.getPlayerRegistry().registerEntry(new PageRegistry.Entry( - "homes", // id - "Homes", // displayName - "homes", // module - Permissions.HOME_GUI, // permission - this::createHomesPage, // supplier - true, // showsInNavBar - 10 // order +playerReg.registerEntry(new PageRegistry.Entry( + "homes", "Homes", "homes", Permissions.HOME_LIST, + (player, ref, store, playerRef, gm) -> + new HomesPage(player, playerRef, homes.getHomeManager(), warmupManager, gm), + true, 10 )); ``` + +## Command GUI Integration + +Commands try to open their GUI page first, falling back to text output: + +```java +// In execute(): +if (tryOpenGui(store, ref, playerRef)) return; +// ... text fallback ... + +private boolean tryOpenGui(Store store, Ref ref, PlayerRef playerRef) { + if (!HyperEssentialsAPI.isAvailable()) return false; + GuiManager gm = HyperEssentialsAPI.getInstance().getGuiManager(); + if (gm.getPlayerRegistry().getEntry("homes") == null) return false; + Player player = store.getComponent(ref, Player.getComponentType()); + if (player == null) return false; + return gm.openPlayerPage("homes", player, ref, store, playerRef); +} +``` + +## Implemented Player Pages + +### HomesPage +- **File:** `gui/player/HomesPage.java` +- **Features:** Browse homes with count/limit header, teleport with warmup, delete homes +- **Dynamic list:** clears `#HomeList`, appends `home_entry.ui` entries with indexed selectors +- **Teleport:** Uses `WarmupManager.startWarmup()` → `Teleport.createForPlayer()`, closes GUI on teleport +- **Events:** Teleport, Delete buttons per entry; Nav bar navigation + +### WarpsPage +- **File:** `gui/player/WarpsPage.java` +- **Features:** Browse warps grouped by category, teleport with warmup +- **Category grouping:** Inserts `warp_category_header.ui` before each category's warp entries +- **Teleport:** Same warmup pattern as HomesPage, closes GUI on teleport +- **Events:** Teleport button per entry; Nav bar navigation + +### KitsPage +- **File:** `gui/player/KitsPage.java` +- **Features:** Browse available kits, claim with cooldown display, preview +- **Cooldown display:** Shows remaining cooldown time, "Ready", "One-time kit", or nothing +- **Claim:** Calls `kitManager.claimKit()` and rebuilds list to update cooldown status +- **Events:** Claim, Preview buttons per entry; Nav bar navigation + +### PlayerDashboardPage +- **File:** `gui/player/PlayerDashboardPage.java` +- **Features:** Welcome message, 3 stat cards (homes count, online players, TPA requests), quick action buttons (Homes, Warps, Kits), playtime and first join date +- **Dependencies:** Optional HomeManager, TpaManager, UtilityManager (graceful degradation if any are null) +- **Events:** NavDirect buttons for quick page navigation; Nav bar navigation + +### TpaPage +- **File:** `gui/player/TpaPage.java` +- **Features:** Incoming TPA requests with per-entry accept/deny, toggle TPA acceptance, request count, time remaining +- **Dynamic list:** Clears `#RequestList`, appends `tpa_entry.ui` entries with indexed selectors +- **Events:** Accept, Deny buttons per request (pass requester UUID as target); Toggle button; Nav bar navigation +- **Updates:** Rebuilds request list after accept/deny/toggle actions + +### StatsPage +- **File:** `gui/player/StatsPage.java` +- **Features:** Player stats (first joined, last login, total playtime, current session) and status indicators (AFK, Fly, God, Infinite Stamina) +- **Dynamic lists:** Uses `stat_row.ui` for both `#StatsList` and `#StatusList` +- **Status coloring:** Active status indicators colored green (`#4aff7f`) +- **Events:** Nav bar navigation + +## Implemented Admin Pages + +### AdminDashboardPage +- **File:** `gui/admin/AdminDashboardPage.java` +- **Features:** Server overview — online player count, warp/spawn/kit stats cards, module status grid +- **Module grid:** Lists all registered modules with name, enabled/disabled status, color-coded indicator dot +- **Events:** Nav bar navigation only (read-only dashboard) + +### AdminWarpsPage +- **File:** `gui/admin/AdminWarpsPage.java` +- **Features:** List all warps sorted by category then name, create at player location, delete +- **Create:** Generates auto-named warp at admin's current position/world +- **Events:** Create button, Delete button per entry; Nav bar navigation + +### AdminSpawnsPage +- **File:** `gui/admin/AdminSpawnsPage.java` +- **Features:** List all spawns with default badge, create at player location, delete +- **Sorting:** Default spawn listed first, then alphabetical +- **Events:** Create button, Delete button per entry; Nav bar navigation + +### AdminKitsPage +- **File:** `gui/admin/AdminKitsPage.java` +- **Features:** List all kits with item count, cooldown, one-time badge; create from inventory, delete +- **Create:** Captures admin's current inventory as a new kit +- **Events:** Create button, Delete button per entry; Nav bar navigation + +### AdminPlayersPage +- **File:** `gui/admin/AdminPlayersPage.java` +- **Features:** List online players sorted by username (case-insensitive), truncated UUID display +- **Data source:** `HyperEssentialsPlugin.getTrackedPlayers()` for currently online players +- **Events:** Nav bar navigation + +### AdminModerationPage +- **File:** `gui/admin/AdminModerationPage.java` +- **Features:** Punishment list with active/all filter toggle, revoke functionality +- **Type badges:** BAN (red), MUTE (gold), KICK (cyan) with matching colored indicator bars +- **Revoke:** Active bans/mutes can be revoked (not kicks); calls `unban()`/`unmute()` on ModerationManager +- **Filter:** Toggle between "Active Only" and "All History" views +- **Events:** FilterActive, FilterAll, Revoke buttons; Nav bar navigation + +### AdminAnnouncementsPage +- **File:** `gui/admin/AdminAnnouncementsPage.java` +- **Features:** Read-only view of announcement messages from config, interval display, sequential/random mode +- **Note:** Messages are config-driven only — add/remove via `announcements.json` config file +- **Events:** Nav bar navigation + +### AdminSettingsPage +- **File:** `gui/admin/AdminSettingsPage.java` +- **Features:** Plugin version, data directory path, config reload button, module status list (enabled/disabled with color-coded dots) +- **Reload:** Calls `ConfigManager.get().reloadAll()` and rebuilds the settings display +- **Events:** Reload button; Nav bar navigation + +## Page Implementation Pattern + +```java +public class HomesPage extends InteractiveCustomUIPage { + public HomesPage(Player player, PlayerRef playerRef, HomeManager homeManager, + WarmupManager warmupManager, GuiManager guiManager) { + super(playerRef, CustomPageLifetime.CanDismiss, PlayerPageData.CODEC); + } + + @Override + public void build(Ref ref, UICommandBuilder cmd, UIEventBuilder events, Store store) { + cmd.append(UIPaths.HOMES_PAGE); + NavBarHelper.setupBar(playerRef, "homes", guiManager.getPlayerRegistry(), cmd, events); + buildHomeList(cmd, events); // populate dynamic list + } + + @Override + public void handleDataEvent(Ref ref, Store store, PlayerPageData data) { + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent(data.navTarget, player, ref, store, playerRef, guiManager, GuiType.PLAYER); + return; + } + switch (data.button) { + case "Teleport" -> handleTeleport(ref, data.target); + case "Delete" -> handleDelete(ref, store, data.target); + } + } + + private void rebuildList() { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + buildHomeList(cmd, events); + sendUpdate(cmd, events, false); // partial update, not full rebuild + } +} +``` diff --git a/docs/integrations.md b/docs/integrations.md index 37ea539..8f286f7 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -1,40 +1,55 @@ # Integrations -> **Status:** Permission integration complete, others are stubs. +HyperEssentials integrates with several plugins via reflection-based soft dependencies. All integrations fail gracefully if the target plugin is not installed. ## HyperPerms (Soft Dependency) -HyperEssentials uses HyperPerms for permission checks when available. Detection is reflection-based — no hard compile-time dependency at runtime. +HyperEssentials uses HyperPerms for permission checks when available. Detection is reflection-based via `HyperPermsProviderAdapter`. -**Resolution chain:** +**Resolution chain (PermissionManager):** 1. Try HyperPerms via `HyperPermsProviderAdapter` -2. Fall back to OP check -3. If `allowWithoutPermissionMod = true`, allow by default +2. Fall back to OP check (via reflection) +3. For bypass permissions, default to `false` +4. For user permissions, respect `allowWithoutPermissionMod` config flag **Features used:** - `hasPermission(uuid, node)` — standard permission checks -- `getPermissionValue(uuid, prefix, default)` — numeric limits (e.g., home count) -- Wildcard resolution (e.g., `hyperessentials.*`) +- `getPermissionValue(uuid, prefix, default)` — numeric limits (e.g., home count via `home.limit.N`) +- `getPrimaryGroup(uuid)` — group lookup +- Wildcard resolution: exact node → category wildcard (`hyperessentials.home.*`) → root wildcard (`hyperessentials.*`) ## HyperFactions (Soft Dependency) -Reflection-based integration for territory awareness: +Reflection-based integration via `HyperFactionsIntegration` for territory awareness: - `getFactionAtLocation(world, x, z)` — faction name at coordinates -- `getFactionIdAtLocation(world, x, z)` — faction UUID at coordinates - `getRelationAtLocation(playerUuid, world, x, z)` — relation type (OWN, ALLY, NEUTRAL, ENEMY) -- `canSetHomeAtLocation(playerUuid, world, x, z)` — territory restriction check +- `getTerritoryLabel(world, x, z)` — territory display label -Used by the homes module to restrict home placement in enemy territory. +**FactionTerritoryChecker** uses this integration for home placement/teleport restrictions: +- `canUseHome(uuid, world, x, z)` — returns `Result.ALLOWED` or a denial with territory type +- `Result` enum: `ALLOWED`, `BLOCKED_OWN`, `BLOCKED_ALLY`, `BLOCKED_ENEMY`, `BLOCKED_NEUTRAL`, `BLOCKED_WILDERNESS` +- Territory restrictions are configurable per-relationship in `config/homes.json` +- Bypass permissions: `bypass.factions`, `bypass.factions.sethome`, `bypass.factions.home` -## Ecotale (Planned) +## VaultUnlocked (Soft Dependency) -Stub for future economy integration: -- Teleport costs -- Kit purchase prices -- Warp creation fees +Economy integration via `VaultEconomyProvider` using reflection on VaultUnlocked 2: +- `getBalance(uuid)` — get player balance +- `has(uuid, amount)` — check if player has sufficient funds +- `withdraw(uuid, amount)` — withdraw from player account +- `deposit(uuid, amount)` — deposit to player account +- Uses plugin name `"HyperEssentials"` as the account namespace -## Werchat (Planned) +Lazy initialization — only connects to VaultUnlocked on first use. -Stub for future chat integration: -- Announcement delivery through chat channels -- Module-specific chat formatting +## LuckPerms (Soft Dependency) + +Listed as a soft dependency in `manifest.json` for load ordering. Permission resolution falls through the `PermissionManager` chain if LuckPerms is present alongside or instead of HyperPerms. + +## Ecotale (Detection Only) + +Class detection via `Class.forName("com.ecotale.Ecotale")`. No active integration logic — reserved for future economy features (teleport costs, kit prices, warp fees). + +## Werchat (Stub) + +Stub class with no implementation. Reserved for future chat integration (announcement delivery through channels, module-specific formatting). diff --git a/docs/modules.md b/docs/modules.md index 3658088..07a6aed 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -1,26 +1,29 @@ # Module System -> **Status:** Framework complete, all modules are stubs. - ## Overview HyperEssentials uses a modular architecture where each feature area is a self-contained module. Modules can be independently enabled or disabled via their config file. ## Module List -| Module | Config File | Default | Description | -|--------|-------------|---------|-------------| -| **warmup** | `warmup.json` | Enabled | Universal warmup/cooldown system | -| **homes** | `homes.json` | Enabled | Home management and teleportation | -| **warps** | `warps.json` | Enabled | Server warp points | -| **spawns** | `spawns.json` | Enabled | Spawn point management | -| **teleport** | `teleport.json` | Enabled | TPA requests and /back | -| **kits** | `kits.json` | Disabled | Kit system | -| **moderation** | `moderation.json` | Disabled | Mute, ban, freeze | -| **vanish** | `vanish.json` | Disabled | Vanish system | -| **utility** | `utility.json` | Disabled | Clear chat, repair, near | -| **announcements** | `announcements.json` | Disabled | Broadcast system | -| **rtp** | `rtp.json` | Disabled | Random teleport | +| Module | Config File | Default | Status | +|--------|-------------|---------|--------| +| **warmup** | `config/warmup.json` | Enabled | Implemented | +| **homes** | `config/homes.json` | Enabled | Implemented | +| **warps** | `config/warps.json` | Enabled | Implemented | +| **spawns** | `config/spawns.json` | Enabled | Implemented | +| **teleport** | `config/teleport.json` | Enabled | Implemented | +| **kits** | `config/kits.json` | Disabled | Implemented | +| **moderation** | `config/moderation.json` | Disabled | Implemented | +| **vanish** | `config/vanish.json` | Enabled | Stub | +| **utility** | `config/utility.json` | Disabled | Implemented | +| **announcements** | `config/announcements.json` | Disabled | Implemented | + +**Note:** RTP (random teleport) is part of the **teleport** module, not a standalone module. The vanish module is a standalone stub — vanish functionality is fully implemented within the **moderation** module via `VanishManager` and `/vanish`. + +## Debug Configuration + +A separate `config/debug.json` controls per-category debug logging with 12 categories: homes, warps, spawns, teleport, kits, moderation, utility, rtp, announcements, integration, economy, storage. ## Enabling/Disabling @@ -36,9 +39,10 @@ Set to `false` and reload (`/hessentials reload`) to disable a module. Its comma ## Module Lifecycle -1. **Registration** — Module instances are created and registered in `ModuleRegistry` during startup -2. **Enable** — If `enabled = true` in config, `onEnable()` is called (registers commands, listeners, GUI pages) -3. **Disable** — On shutdown or config change, `onDisable()` is called (cleanup) +1. **Registration** - Module instances are created and registered in `ModuleRegistry` during startup +2. **Enable** - If `enabled = true` in config, `onEnable()` is called (registers commands, listeners, GUI pages) +3. **Manager Init** - Modules with storage needs (warps, spawns, teleport, homes, kits, moderation) have their managers initialized post-enable +4. **Disable** - On shutdown or config change, `onDisable()` is called (saves data, cleanup) Modules are enabled in registration order (warmup first) and disabled in reverse order. @@ -46,8 +50,8 @@ Modules are enabled in registration order (warmup first) and disabled in reverse Each module extends `AbstractModule` and implements: -- `getName()` — unique identifier (e.g., `"homes"`) -- `getDisplayName()` — human-readable name (e.g., `"Homes"`) -- `onEnable()` — setup logic -- `onDisable()` — cleanup logic -- `getModuleConfig()` — returns the module's config class +- `getName()` - unique identifier (e.g., `"homes"`) +- `getDisplayName()` - human-readable name (e.g., `"Homes"`) +- `onEnable()` - setup logic +- `onDisable()` - cleanup logic +- `getModuleConfig()` - returns the module's config class diff --git a/docs/permissions.md b/docs/permissions.md index 20de712..452e21d 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -1,18 +1,21 @@ # Permissions -All permission nodes use the `hyperessentials` root prefix. +All permission nodes use the `hyperessentials` root prefix. Wildcard resolution is supported (e.g., `hyperessentials.*` grants all nodes). ## Homes | Permission | Description | |------------|-------------| -| `hyperessentials.home` | Use /home | +| `hyperessentials.home.teleport` | Use /home to teleport | | `hyperessentials.home.set` | Set homes | | `hyperessentials.home.delete` | Delete homes | | `hyperessentials.home.list` | List homes | | `hyperessentials.home.gui` | Open homes GUI | -| `hyperessentials.home.share` | Share homes | -| `hyperessentials.home.unlimited` | Unlimited homes | +| `hyperessentials.home.share` | Share homes with other players | +| `hyperessentials.home.unlimited` | Bypass home count limit | +| `hyperessentials.home.limit.` | Set per-player home limit (e.g., `.limit.10`) | + +Home limit priority: `home.unlimited` > `bypass.limit` > `home.limit.` > config default (3). ## Warps @@ -24,6 +27,8 @@ All permission nodes use the `hyperessentials` root prefix. | `hyperessentials.warp.list` | List warps | | `hyperessentials.warp.info` | View warp info | +Individual warps can also have custom permission nodes set via `/setwarp`. + ## Spawns | Permission | Description | @@ -45,29 +50,90 @@ All permission nodes use the `hyperessentials` root prefix. | `hyperessentials.tpcancel` | Cancel TPA requests | | `hyperessentials.tptoggle` | Toggle TPA on/off | | `hyperessentials.back` | Use /back | +| `hyperessentials.rtp` | Use /rtp (random teleport) | +| `hyperessentials.rtp.bypass.factions` | Skip faction claim avoidance during RTP | + +## Kits + +| Permission | Description | +|------------|-------------| +| `hyperessentials.kit.*` | All kit permissions | +| `hyperessentials.kit.use` | Use kits (base) | +| `hyperessentials.kit.use.` | Use a specific kit (e.g., `.kit.use.starter`) | +| `hyperessentials.kit.list` | List available kits | +| `hyperessentials.kit.create` | Create kit definitions | +| `hyperessentials.kit.delete` | Delete kit definitions | + +## Moderation + +| Permission | Description | +|------------|-------------| +| `hyperessentials.moderation.*` | All moderation permissions | +| `hyperessentials.moderation.ban` | Ban, tempban, and unban players | +| `hyperessentials.moderation.mute` | Mute, tempmute, and unmute players | +| `hyperessentials.moderation.kick` | Kick players | +| `hyperessentials.moderation.freeze` | Freeze/unfreeze players | +| `hyperessentials.moderation.vanish` | Toggle vanish | +| `hyperessentials.moderation.history` | View punishment history | + +## Utility + +| Permission | Description | +|------------|-------------| +| `hyperessentials.utility.*` | All utility permissions | +| `hyperessentials.utility.heal` | Heal self | +| `hyperessentials.utility.heal.others` | Heal other players | +| `hyperessentials.utility.fly` | Toggle flight for self | +| `hyperessentials.utility.fly.others` | Toggle flight for others | +| `hyperessentials.utility.god` | Toggle god mode for self | +| `hyperessentials.utility.god.others` | Toggle god mode for others | +| `hyperessentials.utility.clearchat` | Clear own chat | +| `hyperessentials.utility.clearchat.others` | Clear chat for all players | +| `hyperessentials.utility.clearinventory` | Clear own inventory | +| `hyperessentials.utility.clearinventory.others` | Clear other players' inventory | +| `hyperessentials.utility.repair` | Repair held item | +| `hyperessentials.utility.near` | List nearby players | + +## Announcements + +| Permission | Description | +|------------|-------------| +| `hyperessentials.announce.*` | All announcement permissions | +| `hyperessentials.announce.broadcast` | Send manual broadcasts | +| `hyperessentials.announce.manage` | Manage announcement rotation | ## Bypass | Permission | Description | |------------|-------------| +| `hyperessentials.bypass.*` | All bypass permissions | | `hyperessentials.bypass.warmup` | Skip warmup timers | | `hyperessentials.bypass.cooldown` | Skip cooldowns | | `hyperessentials.bypass.limit` | Bypass home limits | +| `hyperessentials.bypass.toggle` | Bypass TPA toggle (send requests to players with TPA disabled) | +| `hyperessentials.bypass.ban` | Bypass ban enforcement | +| `hyperessentials.bypass.mute` | Bypass mute enforcement | +| `hyperessentials.bypass.freeze` | Bypass freeze | +| `hyperessentials.bypass.kit.cooldown` | Bypass kit cooldowns | +| `hyperessentials.bypass.factions` | Bypass all faction territory restrictions | +| `hyperessentials.bypass.factions.sethome` | Bypass faction restrictions for /sethome | +| `hyperessentials.bypass.factions.home` | Bypass faction restrictions for /home | + +## Notify (Staff Alerts) + +| Permission | Description | +|------------|-------------| +| `hyperessentials.notify.*` | Receive all staff notifications | +| `hyperessentials.notify.ban` | Receive ban notifications | +| `hyperessentials.notify.mute` | Receive mute notifications | +| `hyperessentials.notify.kick` | Receive kick notifications | ## Admin | Permission | Description | |------------|-------------| | `hyperessentials.admin` | Admin root | +| `hyperessentials.admin.*` | All admin permissions | | `hyperessentials.admin.reload` | Reload configuration | | `hyperessentials.admin.settings` | Modify settings | - -## Future Modules - -| Permission | Module | -|------------|--------| -| `hyperessentials.kit` | Kits | -| `hyperessentials.vanish` | Vanish | -| `hyperessentials.freeze` | Moderation (freeze) | -| `hyperessentials.mute` | Moderation (mute) | -| `hyperessentials.rtp` | Random teleport | +| `hyperessentials.admin.gui` | Access admin GUI panel (`/he admin`) | diff --git a/docs/readme.md b/docs/readme.md index ac446eb..16e97b7 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -6,14 +6,14 @@ Comprehensive documentation for HyperEssentials — modular server essentials fo | Document | Description | Status | |----------|-------------|--------| -| [Architecture](architecture.md) | System architecture, module lifecycle, package structure | Planned | -| [Commands](commands.md) | All commands organized by module | Planned | -| [Config](config.md) | Configuration reference for core and all modules | Planned | -| [Permissions](permissions.md) | Permission nodes organized by module | Planned | -| [Modules](modules.md) | Module system overview, enable/disable, dependencies | Planned | -| [GUI](gui.md) | GUI framework, page registry, navigation, UI templates | Planned | -| [Storage](storage.md) | Storage providers, JSON format, data migration | Planned | -| [Integrations](integrations.md) | HyperPerms, HyperFactions, Ecotale, Werchat | Planned | +| [Architecture](architecture.md) | System architecture, module lifecycle, package structure | Complete | +| [Commands](commands.md) | All 46 commands organized by module | Complete | +| [Config](config.md) | Configuration reference for core and all modules | Complete | +| [Permissions](permissions.md) | Permission nodes organized by module | Complete | +| [Modules](modules.md) | Module system overview, enable/disable, dependencies | Complete | +| [Storage](storage.md) | Storage providers, JSON format, data migration | Complete | +| [Integrations](integrations.md) | HyperPerms, HyperFactions, VaultUnlocked, Ecotale | Complete | +| [GUI](gui.md) | GUI framework, page registry, navigation, UI templates | Scaffold | | [API](api.md) | Public API for other plugins | Planned | | [Warmup](warmup.md) | Universal warmup/cooldown system | Planned | | [Migration](migration.md) | Migrating from HyperHomes / HyperWarp | Planned | diff --git a/docs/storage.md b/docs/storage.md index 612d5c0..8b629d0 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -1,7 +1,5 @@ # Storage -> **Status:** Interfaces defined, JSON provider is a stub. - ## Overview HyperEssentials uses a pluggable storage system. The `StorageProvider` interface provides access to domain-specific storage interfaces. @@ -11,25 +9,54 @@ HyperEssentials uses a pluggable storage system. The `StorageProvider` interface | Interface | Domain | Key Operations | |-----------|--------|----------------| | `HomeStorage` | Homes | CRUD for player homes | -| `WarpStorage` | Warps | CRUD for server warps | -| `SpawnStorage` | Spawns | CRUD for spawn points | -| `PlayerDataStorage` | Player data | TPA toggle, back history, preferences | +| `WarpStorage` | Warps | Load/save all warps (bulk) | +| `SpawnStorage` | Spawns | Load/save all spawns (bulk) | +| `PlayerDataStorage` | Player data | Load/save/delete per-player TPA toggle, back history | All operations return `CompletableFuture` for async execution. +Additionally, the kits and moderation modules have their own storage classes: +- `KitStorage` — Kit definitions (`data/kits.json`) +- `ModerationStorage` — Punishment records (`data/punishments.json`) + ## Providers | Provider | Config Value | Status | |----------|-------------|--------| -| `JsonStorageProvider` | `"json"` | Stub | +| `JsonStorageProvider` | `"json"` | Implemented | ## Data Directory ``` mods/com.hyperessentials_HyperEssentials/ data/ - homes/ Player home data (JSON per player) - warps/ Server warps - spawns/ Spawn points - players/ Per-player preferences + .version Version marker file (integer, starts at 1) + warps.json Server warps + spawns.json Spawn points + kits.json Kit definitions + punishments.json Punishment records (keyed by player UUID) + players/ Per-player teleport data + {uuid}.json TPA toggle, back history, last TPA timestamp + players/homes/ Per-player home data + {uuid}.json Home entries (name, world, xyz, yaw, pitch, timestamps) + backups/ + backup_migration_v{from}-to-v{to}_{timestamp}.zip ``` + +## Implementation Details + +The `JsonStorageProvider` uses: +- **Atomic writes** — Data is written to a `.tmp` file first, then atomically moved to the target file using `Files.move` with `ATOMIC_MOVE` and `REPLACE_EXISTING` +- **GSON** — All serialization/deserialization uses GSON 2.11.0 +- **Async** — All operations run on `CompletableFuture.supplyAsync()` / `runAsync()` +- **Bulk storage** — Warps, spawns, kits, and punishments are stored as a single JSON file each +- **Per-player storage** — Player teleport data and homes are stored as individual JSON files keyed by UUID + +## Migration System + +The migration framework supports automatic data upgrades between versions: +- `MigrationRunner` creates a timestamped ZIP backup before executing any migration +- Migrations are chained by version number via `MigrationRegistry` +- Types: `CONFIG` (config files), `DATA` (data files), `SCHEMA` (reserved) +- On failure, the runner rolls back from the backup +- Pending migrations run automatically at startup before config loading diff --git a/src/main/java/com/hyperessentials/HyperEssentials.java b/src/main/java/com/hyperessentials/HyperEssentials.java index 7f01417..5ba083a 100644 --- a/src/main/java/com/hyperessentials/HyperEssentials.java +++ b/src/main/java/com/hyperessentials/HyperEssentials.java @@ -1,160 +1,481 @@ -package com.hyperessentials; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.gui.GuiManager; -import com.hyperessentials.integration.EcotaleIntegration; -import com.hyperessentials.integration.HyperFactionsIntegration; -import com.hyperessentials.integration.PermissionManager; -import com.hyperessentials.integration.WerchatIntegration; -import com.hyperessentials.module.ModuleRegistry; -import com.hyperessentials.module.announcements.AnnouncementsModule; -import com.hyperessentials.module.homes.HomesModule; -import com.hyperessentials.module.kits.KitsModule; -import com.hyperessentials.module.moderation.ModerationModule; -import com.hyperessentials.module.rtp.RtpModule; -import com.hyperessentials.module.spawns.SpawnsModule; -import com.hyperessentials.module.teleport.TeleportModule; -import com.hyperessentials.module.utility.UtilityModule; -import com.hyperessentials.module.vanish.VanishModule; -import com.hyperessentials.module.warmup.WarmupManager; -import com.hyperessentials.module.warmup.WarmupModule; -import com.hyperessentials.module.warps.WarpsModule; -import com.hyperessentials.storage.StorageProvider; -import com.hyperessentials.storage.json.JsonStorageProvider; -import com.hyperessentials.util.Logger; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.nio.file.Path; - -/** - * Core singleton for HyperEssentials. - * Manages all subsystems: config, permissions, modules, GUI, storage. - */ -public class HyperEssentials { - - private final Path dataDir; - private final java.util.logging.Logger javaLogger; - - private ConfigManager configManager; - private ModuleRegistry moduleRegistry; - private GuiManager guiManager; - private WarmupManager warmupManager; - private StorageProvider storageProvider; - - public HyperEssentials(@NotNull Path dataDir, @NotNull java.util.logging.Logger javaLogger) { - this.dataDir = dataDir; - this.javaLogger = javaLogger; - } - - /** - * Enables HyperEssentials - loads config, initializes integrations, registers modules. - */ - public void enable() { - // Initialize logger - Logger.init(javaLogger); - - // Load configuration - configManager = ConfigManager.get(); - configManager.loadAll(dataDir); - - // Initialize integrations - PermissionManager.get().init(); - HyperFactionsIntegration.init(); - EcotaleIntegration.init(); - WerchatIntegration.init(); - - // Initialize storage - storageProvider = new JsonStorageProvider(dataDir); - storageProvider.init().join(); - - // Initialize GUI - guiManager = new GuiManager(); - - // Initialize warmup manager - warmupManager = new WarmupManager(); - - // Register modules (warmup first, then feature modules) - moduleRegistry = new ModuleRegistry(); - moduleRegistry.register(new WarmupModule()); - moduleRegistry.register(new HomesModule()); - moduleRegistry.register(new WarpsModule()); - moduleRegistry.register(new SpawnsModule()); - moduleRegistry.register(new TeleportModule()); - moduleRegistry.register(new KitsModule()); - moduleRegistry.register(new ModerationModule()); - moduleRegistry.register(new VanishModule()); - moduleRegistry.register(new UtilityModule()); - moduleRegistry.register(new AnnouncementsModule()); - moduleRegistry.register(new RtpModule()); - - // Enable modules based on config - moduleRegistry.enableAll(); - - Logger.info("HyperEssentials enabled with %d modules", moduleRegistry.getEnabledModules().size()); - } - - /** - * Disables HyperEssentials - disables modules, shuts down storage, saves config. - */ - public void disable() { - // Disable modules in reverse order - if (moduleRegistry != null) { - moduleRegistry.disableAll(); - } - - // Shutdown storage - if (storageProvider != null) { - storageProvider.shutdown().join(); - } - - // Shutdown GUI - if (guiManager != null) { - guiManager.shutdown(); - } - - // Clear warmup state - if (warmupManager != null) { - warmupManager.clear(); - } - - // Save config - if (configManager != null) { - configManager.saveAll(); - } - - Logger.info("HyperEssentials disabled"); - } - - /** - * Reloads configuration. - */ - public void reloadConfig() { - ConfigManager.get().reloadAll(); - Logger.info("Configuration reloaded"); - } - - // Getters - - @NotNull public Path getDataDir() { return dataDir; } - @NotNull public ConfigManager getConfigManager() { return configManager; } - @NotNull public ModuleRegistry getModuleRegistry() { return moduleRegistry; } - @NotNull public GuiManager getGuiManager() { return guiManager; } - @NotNull public WarmupManager getWarmupManager() { return warmupManager; } - @NotNull public StorageProvider getStorageProvider() { return storageProvider; } - - /** - * Gets a module by class. - */ - @Nullable - public T getModule(@NotNull Class clazz) { - return moduleRegistry.getModule(clazz); - } - - /** - * Checks if a module is enabled by name. - */ - public boolean isModuleEnabled(@NotNull String name) { - return ConfigManager.get().isModuleEnabled(name); - } -} +package com.hyperessentials; + +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.PageRegistry; +import com.hyperessentials.gui.admin.AdminAnnouncementsPage; +import com.hyperessentials.gui.admin.AdminDashboardPage; +import com.hyperessentials.gui.admin.AdminKitsPage; +import com.hyperessentials.gui.admin.AdminModerationPage; +import com.hyperessentials.gui.admin.AdminPlayersPage; +import com.hyperessentials.gui.admin.AdminSettingsPage; +import com.hyperessentials.gui.admin.AdminSpawnsPage; +import com.hyperessentials.gui.admin.AdminWarpsPage; +import com.hyperessentials.gui.player.HomesPage; +import com.hyperessentials.gui.player.KitsPage; +import com.hyperessentials.gui.player.PlayerDashboardPage; +import com.hyperessentials.gui.player.StatsPage; +import com.hyperessentials.gui.player.TpaPage; +import com.hyperessentials.gui.player.WarpsPage; +import com.hyperessentials.integration.EcotaleIntegration; +import com.hyperessentials.integration.HyperFactionsIntegration; +import com.hyperessentials.integration.PermissionManager; +import com.hyperessentials.integration.WerchatIntegration; +import com.hyperessentials.integration.economy.VaultEconomyProvider; +import com.hyperessentials.module.ModuleRegistry; +import com.hyperessentials.module.announcements.AnnouncementsModule; +import com.hyperessentials.module.homes.HomesModule; +import com.hyperessentials.module.kits.KitsModule; +import com.hyperessentials.module.moderation.ModerationModule; +import com.hyperessentials.module.spawns.SpawnsModule; +import com.hyperessentials.module.teleport.TeleportModule; +import com.hyperessentials.module.utility.UtilityManager; +import com.hyperessentials.module.utility.UtilityModule; +import com.hyperessentials.module.warmup.WarmupManager; +import com.hyperessentials.module.warmup.WarmupModule; +import com.hyperessentials.module.warps.WarpsModule; +import com.hyperessentials.storage.StorageProvider; +import com.hyperessentials.storage.json.JsonStorageProvider; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.logger.HytaleLogger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Core singleton for HyperEssentials. + * Manages all subsystems: config, permissions, modules, GUI, storage. + */ +public class HyperEssentials { + + private final Path dataDir; + private final HytaleLogger hytaleLogger; + + private ConfigManager configManager; + private ModuleRegistry moduleRegistry; + private GuiManager guiManager; + private WarmupManager warmupManager; + private StorageProvider storageProvider; + private VaultEconomyProvider vaultEconomy; + private final CopyOnWriteArrayList> disconnectHandlers = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList> connectHandlers = new CopyOnWriteArrayList<>(); + + public HyperEssentials(@NotNull Path dataDir, @NotNull HytaleLogger hytaleLogger) { + this.dataDir = dataDir; + this.hytaleLogger = hytaleLogger; + } + + /** + * Enables HyperEssentials - loads config, initializes integrations, registers modules. + */ + public void enable() { + // Initialize logger + Logger.init(hytaleLogger); + + // Load configuration + configManager = ConfigManager.get(); + configManager.loadAll(dataDir); + + // Apply debug config to logger + configManager.debug().applyToLogger(); + + // Ensure standard directory structure + ensureDirectories(); + + // Initialize integrations + PermissionManager.get().init(); + HyperFactionsIntegration.init(); + EcotaleIntegration.init(); + WerchatIntegration.init(); + + // Initialize VaultUnlocked economy + vaultEconomy = new VaultEconomyProvider(); + vaultEconomy.init(); + + // Initialize storage + storageProvider = new JsonStorageProvider(dataDir); + storageProvider.init().join(); + + // Initialize GUI + guiManager = new GuiManager(); + + // Initialize warmup manager + warmupManager = new WarmupManager(); + + // Register modules (warmup first, then feature modules) + moduleRegistry = new ModuleRegistry(); + moduleRegistry.register(new WarmupModule()); + moduleRegistry.register(new HomesModule()); + moduleRegistry.register(new WarpsModule()); + moduleRegistry.register(new SpawnsModule()); + moduleRegistry.register(new TeleportModule()); + moduleRegistry.register(new KitsModule()); + moduleRegistry.register(new ModerationModule()); + moduleRegistry.register(new UtilityModule()); + moduleRegistry.register(new AnnouncementsModule()); + + // Enable modules based on config + moduleRegistry.enableAll(); + + // Initialize module managers with storage (post-enable) + initModuleManagers(); + + // Register GUI pages (post-init, modules + managers must be ready) + registerPages(); + + Logger.info("HyperEssentials enabled with %d modules", moduleRegistry.getEnabledModules().size()); + } + + /** + * Disables HyperEssentials - disables modules, shuts down storage, saves config. + */ + public void disable() { + // Disable modules in reverse order + if (moduleRegistry != null) { + moduleRegistry.disableAll(); + } + + // Shutdown storage + if (storageProvider != null) { + storageProvider.shutdown().join(); + } + + // Shutdown GUI + if (guiManager != null) { + guiManager.shutdown(); + } + + // Shutdown warmup scheduler + if (warmupManager != null) { + warmupManager.shutdown(); + } + + // Save config + if (configManager != null) { + configManager.saveAll(); + } + + Logger.info("HyperEssentials disabled"); + } + + /** + * Reloads configuration. + */ + public void reloadConfig() { + ConfigManager.get().reloadAll(); + Logger.info("Configuration reloaded"); + } + + /** + * Initializes module managers with storage after modules are enabled. + */ + private void initModuleManagers() { + HomesModule homes = getHomesModule(); + if (homes != null && homes.isEnabled()) { + homes.initManager(storageProvider.getHomeStorage()); + } + + WarpsModule warps = getWarpsModule(); + if (warps != null && warps.isEnabled()) { + warps.initManager(storageProvider.getWarpStorage()); + } + + SpawnsModule spawns = getSpawnsModule(); + if (spawns != null && spawns.isEnabled()) { + spawns.initManager(storageProvider.getSpawnStorage()); + } + + TeleportModule teleport = getTeleportModule(); + if (teleport != null && teleport.isEnabled()) { + teleport.initManagers(storageProvider.getPlayerDataStorage()); + } + } + + /** + * Registers GUI pages for enabled modules. + */ + private void registerPages() { + PageRegistry playerReg = guiManager.getPlayerRegistry(); + + // Homes page + HomesModule homes = getHomesModule(); + if (homes != null && homes.isEnabled() && homes.getHomeManager() != null) { + playerReg.registerEntry(new PageRegistry.Entry( + "homes", "Homes", "homes", Permissions.HOME_LIST, + (player, ref, store, playerRef, gm) -> + new HomesPage(player, playerRef, homes.getHomeManager(), warmupManager, gm), + true, 10 + )); + } + + // Warps page + WarpsModule warps = getWarpsModule(); + if (warps != null && warps.isEnabled() && warps.getWarpManager() != null) { + playerReg.registerEntry(new PageRegistry.Entry( + "warps", "Warps", "warps", Permissions.WARP_LIST, + (player, ref, store, playerRef, gm) -> + new WarpsPage(player, playerRef, warps.getWarpManager(), warmupManager, gm), + true, 20 + )); + } + + // Kits page + KitsModule kitsModule = moduleRegistry.getModule(KitsModule.class); + if (kitsModule != null && kitsModule.isEnabled()) { + playerReg.registerEntry(new PageRegistry.Entry( + "kits", "Kits", "kits", Permissions.KIT_LIST, + (player, ref, store, playerRef, gm) -> + new KitsPage(player, playerRef, kitsModule.getKitManager(), gm), + true, 30 + )); + } + + // TPA page + TeleportModule teleport = getTeleportModule(); + if (teleport != null && teleport.isEnabled() && teleport.getTpaManager() != null) { + playerReg.registerEntry(new PageRegistry.Entry( + "tpa", "TPA", "teleport", Permissions.TPA, + (player, ref, store, playerRef, gm) -> + new TpaPage(player, playerRef, teleport.getTpaManager(), gm), + true, 40 + )); + } + + // Stats page + UtilityModule utilityModule = moduleRegistry.getModule(UtilityModule.class); + UtilityManager utilMgr = utilityModule != null && utilityModule.isEnabled() + ? utilityModule.getUtilityManager() : null; + + playerReg.registerEntry(new PageRegistry.Entry( + "stats", "Stats", "core", null, + (player, ref, store, playerRef, gm) -> + new StatsPage(player, playerRef, gm, utilMgr), + true, 50 + )); + + // Dashboard (registered last since it references other module managers) + HomesModule homesForDash = getHomesModule(); + playerReg.registerEntry(new PageRegistry.Entry( + "dashboard", "Dashboard", "core", null, + (player, ref, store, playerRef, gm) -> + new PlayerDashboardPage(player, playerRef, gm, + homesForDash != null && homesForDash.isEnabled() ? homesForDash.getHomeManager() : null, + teleport != null && teleport.isEnabled() ? teleport.getTpaManager() : null, + utilMgr), + true, 0 + )); + + int pageCount = playerReg.getEntries().size(); + if (pageCount > 0) { + Logger.info("[GUI] Registered %d player page(s)", pageCount); + } + + // === Admin Pages === + PageRegistry adminReg = guiManager.getAdminRegistry(); + + // Admin Dashboard + adminReg.registerEntry(new PageRegistry.Entry( + "dashboard", "Dashboard", "core", null, + (player, ref, store, playerRef, gm) -> + new AdminDashboardPage(player, playerRef, gm, moduleRegistry), + true, 0 + )); + + // Admin Warps + if (warps != null && warps.isEnabled() && warps.getWarpManager() != null) { + adminReg.registerEntry(new PageRegistry.Entry( + "warps", "Warps", "warps", Permissions.WARP_SET, + (player, ref, store, playerRef, gm) -> + new AdminWarpsPage(player, playerRef, warps.getWarpManager(), gm), + true, 20 + )); + } + + // Admin Spawns + SpawnsModule spawnsForAdmin = getSpawnsModule(); + if (spawnsForAdmin != null && spawnsForAdmin.isEnabled() && spawnsForAdmin.getSpawnManager() != null) { + adminReg.registerEntry(new PageRegistry.Entry( + "spawns", "Spawns", "spawns", Permissions.SPAWN_SET, + (player, ref, store, playerRef, gm) -> + new AdminSpawnsPage(player, playerRef, spawnsForAdmin.getSpawnManager(), gm), + true, 30 + )); + } + + // Admin Kits + if (kitsModule != null && kitsModule.isEnabled() && kitsModule.getKitManager() != null) { + adminReg.registerEntry(new PageRegistry.Entry( + "kits", "Kits", "kits", Permissions.KIT_CREATE, + (player, ref, store, playerRef, gm) -> + new AdminKitsPage(player, playerRef, ref, store, kitsModule.getKitManager(), gm), + true, 40 + )); + } + + // Admin Players + adminReg.registerEntry(new PageRegistry.Entry( + "players", "Players", "core", Permissions.ADMIN_GUI, + (player, ref, store, playerRef, gm) -> + new AdminPlayersPage(player, playerRef, gm), + true, 10 + )); + + // Admin Moderation + ModerationModule modModule = moduleRegistry.getModule(ModerationModule.class); + if (modModule != null && modModule.isEnabled() && modModule.getModerationManager() != null) { + adminReg.registerEntry(new PageRegistry.Entry( + "moderation", "Moderation", "moderation", Permissions.ADMIN_GUI, + (player, ref, store, playerRef, gm) -> + new AdminModerationPage(player, playerRef, modModule.getModerationManager(), gm), + true, 50 + )); + } + + // Admin Announcements + AnnouncementsModule annModule = moduleRegistry.getModule(AnnouncementsModule.class); + if (annModule != null && annModule.isEnabled()) { + adminReg.registerEntry(new PageRegistry.Entry( + "announcements", "Announcements", "announcements", Permissions.ADMIN_GUI, + (player, ref, store, playerRef, gm) -> + new AdminAnnouncementsPage(player, playerRef, gm), + true, 60 + )); + } + + // Admin Settings + adminReg.registerEntry(new PageRegistry.Entry( + "settings", "Settings", "core", Permissions.ADMIN_SETTINGS, + (player, ref, store, playerRef, gm) -> + new AdminSettingsPage(player, playerRef, gm, moduleRegistry, dataDir), + true, 70 + )); + + int adminPageCount = adminReg.getEntries().size(); + if (adminPageCount > 0) { + Logger.info("[GUI] Registered %d admin page(s)", adminPageCount); + } + } + + /** + * Ensures the standard directory structure exists. + */ + private void ensureDirectories() { + try { + Files.createDirectories(dataDir.resolve("config")); + Files.createDirectories(dataDir.resolve("data")); + Files.createDirectories(dataDir.resolve("data/players")); + Files.createDirectories(dataDir.resolve("backups")); + + // Write .version marker if it doesn't exist + Path versionFile = dataDir.resolve("data/.version"); + if (!Files.exists(versionFile)) { + int configVersion = 1; + Files.writeString(versionFile, String.valueOf(configVersion)); + Logger.debug("[Startup] Created .version marker: %d", configVersion); + } + } catch (IOException e) { + Logger.severe("[Startup] Failed to create directories: %s", e.getMessage()); + } + } + + // Module getters + + @Nullable + public WarpsModule getWarpsModule() { return moduleRegistry.getModule(WarpsModule.class); } + @Nullable + public SpawnsModule getSpawnsModule() { return moduleRegistry.getModule(SpawnsModule.class); } + @Nullable + public HomesModule getHomesModule() { return moduleRegistry.getModule(HomesModule.class); } + @Nullable + public TeleportModule getTeleportModule() { return moduleRegistry.getModule(TeleportModule.class); } + + // Getters + + @NotNull public Path getDataDir() { return dataDir; } + @NotNull public ConfigManager getConfigManager() { return configManager; } + @NotNull public ModuleRegistry getModuleRegistry() { return moduleRegistry; } + @NotNull public GuiManager getGuiManager() { return guiManager; } + @NotNull public WarmupManager getWarmupManager() { return warmupManager; } + @NotNull public StorageProvider getStorageProvider() { return storageProvider; } + @NotNull public VaultEconomyProvider getVaultEconomy() { return vaultEconomy; } + + /** + * Gets a module by class. + */ + @Nullable + public T getModule(@NotNull Class clazz) { + return moduleRegistry.getModule(clazz); + } + + /** + * Checks if a module is enabled by name. + */ + public boolean isModuleEnabled(@NotNull String name) { + return ConfigManager.get().isModuleEnabled(name); + } + + /** + * Registers a handler that is called when a player connects. + * Handler receives (UUID, username). + */ + public void registerConnectHandler(@NotNull BiConsumer handler) { + connectHandlers.add(handler); + } + + /** + * Unregisters a connect handler. + */ + public void unregisterConnectHandler(@NotNull BiConsumer handler) { + connectHandlers.remove(handler); + } + + /** + * Called by the plugin when a player connects. + * Notifies all registered connect handlers. + */ + public void onPlayerConnect(@NotNull UUID uuid, @NotNull String username) { + for (BiConsumer handler : connectHandlers) { + try { + handler.accept(uuid, username); + } catch (Exception e) { + Logger.severe("Error in connect handler: %s", e.getMessage()); + } + } + } + + /** + * Registers a handler that is called when a player disconnects. + * Used by modules to clean up session state. + */ + public void registerDisconnectHandler(@NotNull Consumer handler) { + disconnectHandlers.add(handler); + } + + /** + * Unregisters a disconnect handler. + */ + public void unregisterDisconnectHandler(@NotNull Consumer handler) { + disconnectHandlers.remove(handler); + } + + /** + * Called by the plugin when a player disconnects. + * Notifies all registered disconnect handlers. + */ + public void onPlayerDisconnect(@NotNull UUID uuid) { + for (Consumer handler : disconnectHandlers) { + try { + handler.accept(uuid); + } catch (Exception e) { + Logger.severe("Error in disconnect handler: %s", e.getMessage()); + } + } + } +} diff --git a/src/main/java/com/hyperessentials/Permissions.java b/src/main/java/com/hyperessentials/Permissions.java index 1864059..e1b258e 100644 --- a/src/main/java/com/hyperessentials/Permissions.java +++ b/src/main/java/com/hyperessentials/Permissions.java @@ -1,61 +1,128 @@ -package com.hyperessentials; - -/** - * Centralized permission node definitions for HyperEssentials. - */ -public final class Permissions { - - private Permissions() {} - - public static final String ROOT = "hyperessentials"; - - // Homes - public static final String HOME = ROOT + ".home"; - public static final String HOME_SET = HOME + ".set"; - public static final String HOME_DELETE = HOME + ".delete"; - public static final String HOME_LIST = HOME + ".list"; - public static final String HOME_GUI = HOME + ".gui"; - public static final String HOME_SHARE = HOME + ".share"; - public static final String HOME_UNLIMITED = HOME + ".unlimited"; - - // Warps - public static final String WARP = ROOT + ".warp"; - public static final String WARP_SET = WARP + ".set"; - public static final String WARP_DELETE = WARP + ".delete"; - public static final String WARP_LIST = WARP + ".list"; - public static final String WARP_INFO = WARP + ".info"; - - // Spawns - public static final String SPAWN = ROOT + ".spawn"; - public static final String SPAWN_SET = SPAWN + ".set"; - public static final String SPAWN_DELETE = SPAWN + ".delete"; - public static final String SPAWN_LIST = SPAWN + ".list"; - public static final String SPAWN_INFO = SPAWN + ".info"; - - // Teleport - public static final String TPA = ROOT + ".tpa"; - public static final String TPAHERE = ROOT + ".tpahere"; - public static final String TPACCEPT = ROOT + ".tpaccept"; - public static final String TPDENY = ROOT + ".tpdeny"; - public static final String TPCANCEL = ROOT + ".tpcancel"; - public static final String TPTOGGLE = ROOT + ".tptoggle"; - public static final String BACK = ROOT + ".back"; - - // Bypass - public static final String BYPASS = ROOT + ".bypass"; - public static final String BYPASS_WARMUP = BYPASS + ".warmup"; - public static final String BYPASS_COOLDOWN = BYPASS + ".cooldown"; - public static final String BYPASS_LIMIT = BYPASS + ".limit"; - - // Admin - public static final String ADMIN = ROOT + ".admin"; - public static final String ADMIN_RELOAD = ADMIN + ".reload"; - public static final String ADMIN_SETTINGS = ADMIN + ".settings"; - - // Future module permissions - public static final String KIT = ROOT + ".kit"; - public static final String VANISH = ROOT + ".vanish"; - public static final String FREEZE = ROOT + ".freeze"; - public static final String MUTE = ROOT + ".mute"; - public static final String RTP = ROOT + ".rtp"; -} +package com.hyperessentials; + +/** + * Centralized permission node definitions for HyperEssentials. + * Follows namespace.category.action hierarchy with wildcards at every level. + */ +public final class Permissions { + + private Permissions() {} + + public static final String ROOT = "hyperessentials"; + public static final String WILDCARD = ROOT + ".*"; + + // === Homes === + public static final String HOME = ROOT + ".home"; + public static final String HOME_SET = HOME + ".set"; + public static final String HOME_DELETE = HOME + ".delete"; + public static final String HOME_LIST = HOME + ".list"; + public static final String HOME_GUI = HOME + ".gui"; + public static final String HOME_SHARE = HOME + ".share"; + public static final String HOME_UNLIMITED = HOME + ".unlimited"; + public static final String HOME_TELEPORT = HOME + ".teleport"; + public static final String HOME_LIMIT_PREFIX = HOME + ".limit."; + + // === Warps === + public static final String WARP = ROOT + ".warp"; + public static final String WARP_SET = WARP + ".set"; + public static final String WARP_DELETE = WARP + ".delete"; + public static final String WARP_LIST = WARP + ".list"; + public static final String WARP_INFO = WARP + ".info"; + + // === Spawns === + public static final String SPAWN = ROOT + ".spawn"; + public static final String SPAWN_SET = SPAWN + ".set"; + public static final String SPAWN_DELETE = SPAWN + ".delete"; + public static final String SPAWN_LIST = SPAWN + ".list"; + public static final String SPAWN_INFO = SPAWN + ".info"; + + // === Teleport === + public static final String TPA = ROOT + ".tpa"; + public static final String TPAHERE = ROOT + ".tpahere"; + public static final String TPACCEPT = ROOT + ".tpaccept"; + public static final String TPDENY = ROOT + ".tpdeny"; + public static final String TPCANCEL = ROOT + ".tpcancel"; + public static final String TPTOGGLE = ROOT + ".tptoggle"; + public static final String BACK = ROOT + ".back"; + + // === Kits === + public static final String KIT_WILDCARD = ROOT + ".kit.*"; + public static final String KIT_USE = ROOT + ".kit.use"; + public static final String KIT_USE_PREFIX = ROOT + ".kit.use."; + public static final String KIT_LIST = ROOT + ".kit.list"; + public static final String KIT_CREATE = ROOT + ".kit.create"; + public static final String KIT_DELETE = ROOT + ".kit.delete"; + + // === Moderation === + public static final String MODERATION_WILDCARD = ROOT + ".moderation.*"; + public static final String MODERATION_BAN = ROOT + ".moderation.ban"; + public static final String MODERATION_MUTE = ROOT + ".moderation.mute"; + public static final String MODERATION_KICK = ROOT + ".moderation.kick"; + public static final String MODERATION_FREEZE = ROOT + ".moderation.freeze"; + public static final String MODERATION_VANISH = ROOT + ".moderation.vanish"; + public static final String MODERATION_HISTORY = ROOT + ".moderation.history"; + public static final String MODERATION_IPBAN = ROOT + ".moderation.ipban"; + + // === Utility === + public static final String UTILITY_WILDCARD = ROOT + ".utility.*"; + public static final String UTILITY_HEAL = ROOT + ".utility.heal"; + public static final String UTILITY_HEAL_OTHERS = ROOT + ".utility.heal.others"; + public static final String UTILITY_FLY = ROOT + ".utility.fly"; + public static final String UTILITY_FLY_OTHERS = ROOT + ".utility.fly.others"; + public static final String UTILITY_GOD = ROOT + ".utility.god"; + public static final String UTILITY_GOD_OTHERS = ROOT + ".utility.god.others"; + public static final String UTILITY_CLEARCHAT = ROOT + ".utility.clearchat"; + public static final String UTILITY_CLEARCHAT_OTHERS = ROOT + ".utility.clearchat.others"; + public static final String UTILITY_CLEARINVENTORY = ROOT + ".utility.clearinventory"; + public static final String UTILITY_CLEARINVENTORY_OTHERS = ROOT + ".utility.clearinventory.others"; + public static final String UTILITY_REPAIR = ROOT + ".utility.repair"; + public static final String UTILITY_DURABILITY = ROOT + ".utility.durability"; + public static final String UTILITY_NEAR = ROOT + ".utility.near"; + public static final String UTILITY_MOTD = ROOT + ".utility.motd"; + public static final String UTILITY_PLAYTIME = ROOT + ".utility.playtime"; + public static final String UTILITY_JOINDATE = ROOT + ".utility.joindate"; + public static final String UTILITY_AFK = ROOT + ".utility.afk"; + public static final String UTILITY_INVSEE = ROOT + ".utility.invsee"; + public static final String UTILITY_STAMINA = ROOT + ".utility.stamina"; + public static final String UTILITY_STAMINA_OTHERS = ROOT + ".utility.stamina.others"; + public static final String UTILITY_TRASH = ROOT + ".utility.trash"; + public static final String UTILITY_MAXSTACK = ROOT + ".utility.maxstack"; + public static final String UTILITY_SLEEPPERCENTAGE = ROOT + ".utility.sleeppercentage"; + + // === Announcements === + public static final String ANNOUNCE_WILDCARD = ROOT + ".announce.*"; + public static final String ANNOUNCE_BROADCAST = ROOT + ".announce.broadcast"; + public static final String ANNOUNCE_MANAGE = ROOT + ".announce.manage"; + + // === Bypass === + public static final String BYPASS = ROOT + ".bypass"; + public static final String BYPASS_WILDCARD = BYPASS + ".*"; + public static final String BYPASS_WARMUP = BYPASS + ".warmup"; + public static final String BYPASS_COOLDOWN = BYPASS + ".cooldown"; + public static final String BYPASS_LIMIT = BYPASS + ".limit"; + public static final String BYPASS_TOGGLE = BYPASS + ".toggle"; + public static final String BYPASS_BAN = BYPASS + ".ban"; + public static final String BYPASS_MUTE = BYPASS + ".mute"; + public static final String BYPASS_FREEZE = BYPASS + ".freeze"; + public static final String BYPASS_KIT_COOLDOWN = BYPASS + ".kit.cooldown"; + public static final String BYPASS_FACTIONS = BYPASS + ".factions"; + public static final String BYPASS_FACTIONS_SETHOME = BYPASS_FACTIONS + ".sethome"; + public static final String BYPASS_FACTIONS_HOME = BYPASS_FACTIONS + ".home"; + + // === Notify === + public static final String NOTIFY_WILDCARD = ROOT + ".notify.*"; + public static final String NOTIFY_BAN = ROOT + ".notify.ban"; + public static final String NOTIFY_MUTE = ROOT + ".notify.mute"; + public static final String NOTIFY_KICK = ROOT + ".notify.kick"; + + // === Admin === + public static final String ADMIN = ROOT + ".admin"; + public static final String ADMIN_WILDCARD = ADMIN + ".*"; + public static final String ADMIN_RELOAD = ADMIN + ".reload"; + public static final String ADMIN_SETTINGS = ADMIN + ".settings"; + public static final String ADMIN_GUI = ADMIN + ".gui"; + + // === RTP === + public static final String RTP = ROOT + ".rtp"; + public static final String RTP_BYPASS_FACTIONS = RTP + ".bypass.factions"; +} diff --git a/src/main/java/com/hyperessentials/api/HyperEssentialsAPI.java b/src/main/java/com/hyperessentials/api/HyperEssentialsAPI.java index af898a0..0ea9e2f 100644 --- a/src/main/java/com/hyperessentials/api/HyperEssentialsAPI.java +++ b/src/main/java/com/hyperessentials/api/HyperEssentialsAPI.java @@ -1,38 +1,135 @@ -package com.hyperessentials.api; - -import com.hyperessentials.HyperEssentials; -import org.jetbrains.annotations.Nullable; - -/** - * Public API for HyperEssentials. - * Other plugins can use this to interact with HyperEssentials modules. - */ -public final class HyperEssentialsAPI { - - private static HyperEssentials instance; - - private HyperEssentialsAPI() {} - - public static void setInstance(@Nullable HyperEssentials instance) { - HyperEssentialsAPI.instance = instance; - } - - /** - * Gets the HyperEssentials instance. - * - * @return the instance, or null if not initialized - */ - @Nullable - public static HyperEssentials getInstance() { - return instance; - } - - /** - * Checks if HyperEssentials is available and initialized. - * - * @return true if available - */ - public static boolean isAvailable() { - return instance != null; - } -} +package com.hyperessentials.api; + +import com.hyperessentials.HyperEssentials; +import com.hyperessentials.data.Location; +import com.hyperessentials.data.Spawn; +import com.hyperessentials.data.Warp; +import com.hyperessentials.module.spawns.SpawnManager; +import com.hyperessentials.module.spawns.SpawnsModule; +import com.hyperessentials.module.teleport.BackManager; +import com.hyperessentials.module.teleport.TeleportModule; +import com.hyperessentials.module.teleport.TpaManager; +import com.hyperessentials.module.warps.WarpManager; +import com.hyperessentials.module.warps.WarpsModule; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +/** + * Public API for HyperEssentials. + * Other plugins can use this to interact with HyperEssentials modules. + */ +public final class HyperEssentialsAPI { + + private static HyperEssentials instance; + + private HyperEssentialsAPI() {} + + public static void setInstance(@Nullable HyperEssentials instance) { + HyperEssentialsAPI.instance = instance; + } + + @Nullable + public static HyperEssentials getInstance() { + return instance; + } + + public static boolean isAvailable() { + return instance != null; + } + + // ========== Warp API ========== + + @Nullable + public static Warp getWarp(@NotNull String name) { + WarpManager wm = getWarpManager(); + return wm != null ? wm.getWarp(name) : null; + } + + @NotNull + public static Collection getAllWarps() { + WarpManager wm = getWarpManager(); + return wm != null ? wm.getAllWarps() : Collections.emptyList(); + } + + @NotNull + public static List getAccessibleWarps(@NotNull UUID uuid) { + WarpManager wm = getWarpManager(); + return wm != null ? wm.getAccessibleWarps(uuid) : Collections.emptyList(); + } + + // ========== Spawn API ========== + + @Nullable + public static Spawn getSpawn(@NotNull String name) { + SpawnManager sm = getSpawnManager(); + return sm != null ? sm.getSpawn(name) : null; + } + + @Nullable + public static Spawn getDefaultSpawn() { + SpawnManager sm = getSpawnManager(); + return sm != null ? sm.getDefaultSpawn() : null; + } + + @Nullable + public static Spawn getSpawnForPlayer(@NotNull UUID uuid) { + SpawnManager sm = getSpawnManager(); + return sm != null ? sm.getSpawnForPlayer(uuid) : null; + } + + // ========== Back API ========== + + public static void saveBackLocation(@NotNull UUID uuid, @NotNull Location location) { + BackManager bm = getBackManager(); + if (bm != null) { + bm.saveBackLocation(uuid, location); + } + } + + public static boolean hasBackHistory(@NotNull UUID uuid) { + BackManager bm = getBackManager(); + return bm != null && bm.hasBackHistory(uuid); + } + + // ========== TPA API ========== + + public static boolean isAcceptingTpa(@NotNull UUID uuid) { + TpaManager tm = getTpaManager(); + return tm == null || tm.isAcceptingRequests(uuid); + } + + // ========== Internal helpers ========== + + @Nullable + private static WarpManager getWarpManager() { + if (!isAvailable()) return null; + WarpsModule module = instance.getWarpsModule(); + return (module != null && module.isEnabled()) ? module.getWarpManager() : null; + } + + @Nullable + private static SpawnManager getSpawnManager() { + if (!isAvailable()) return null; + SpawnsModule module = instance.getSpawnsModule(); + return (module != null && module.isEnabled()) ? module.getSpawnManager() : null; + } + + @Nullable + private static BackManager getBackManager() { + if (!isAvailable()) return null; + TeleportModule module = instance.getTeleportModule(); + return (module != null && module.isEnabled()) ? module.getBackManager() : null; + } + + @Nullable + private static TpaManager getTpaManager() { + if (!isAvailable()) return null; + TeleportModule module = instance.getTeleportModule(); + return (module != null && module.isEnabled()) ? module.getTpaManager() : null; + } +} diff --git a/src/main/java/com/hyperessentials/api/events/EventBus.java b/src/main/java/com/hyperessentials/api/events/EventBus.java index f43ea19..98b7d16 100644 --- a/src/main/java/com/hyperessentials/api/events/EventBus.java +++ b/src/main/java/com/hyperessentials/api/events/EventBus.java @@ -1,48 +1,48 @@ -package com.hyperessentials.api.events; - -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; - -/** - * Simple event bus for HyperEssentials events. - */ -public final class EventBus { - - private static final Map, List>> handlers = new ConcurrentHashMap<>(); - - private EventBus() {} - - public static void register(@NotNull Class eventClass, @NotNull Consumer handler) { - handlers.computeIfAbsent(eventClass, k -> new CopyOnWriteArrayList<>()).add(handler); - } - - public static void unregister(@NotNull Class eventClass, @NotNull Consumer handler) { - List> list = handlers.get(eventClass); - if (list != null) { - list.remove(handler); - } - } - - @SuppressWarnings("unchecked") - public static void fire(@NotNull T event) { - List> list = handlers.get(event.getClass()); - if (list != null) { - for (Consumer handler : list) { - try { - ((Consumer) handler).accept(event); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - } - - public static void clear() { - handlers.clear(); - } -} +package com.hyperessentials.api.events; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +/** + * Simple event bus for HyperEssentials events. + */ +public final class EventBus { + + private static final Map, List>> handlers = new ConcurrentHashMap<>(); + + private EventBus() {} + + public static void register(@NotNull Class eventClass, @NotNull Consumer handler) { + handlers.computeIfAbsent(eventClass, k -> new CopyOnWriteArrayList<>()).add(handler); + } + + public static void unregister(@NotNull Class eventClass, @NotNull Consumer handler) { + List> list = handlers.get(eventClass); + if (list != null) { + list.remove(handler); + } + } + + @SuppressWarnings("unchecked") + public static void fire(@NotNull T event) { + List> list = handlers.get(event.getClass()); + if (list != null) { + for (Consumer handler : list) { + try { + ((Consumer) handler).accept(event); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + + public static void clear() { + handlers.clear(); + } +} diff --git a/src/main/java/com/hyperessentials/command/AdminCommand.java b/src/main/java/com/hyperessentials/command/AdminCommand.java index a093393..808854a 100644 --- a/src/main/java/com/hyperessentials/command/AdminCommand.java +++ b/src/main/java/com/hyperessentials/command/AdminCommand.java @@ -1,68 +1,199 @@ -package com.hyperessentials.command; - -import com.hyperessentials.BuildInfo; -import com.hyperessentials.Permissions; -import com.hyperessentials.command.util.CommandUtil; -import com.hyperessentials.config.ConfigManager; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.command.system.CommandContext; -import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; -import com.hypixel.hytale.server.core.universe.PlayerRef; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import org.jetbrains.annotations.NotNull; - -/** - * Main admin command for HyperEssentials (/hessentials). - */ -public class AdminCommand extends AbstractPlayerCommand { - - public AdminCommand() { - super("hessentials", "HyperEssentials admin command"); - setAllowsExtraArguments(true); - } - - @Override - protected void execute(@NotNull CommandContext ctx, - @NotNull Store store, - @NotNull Ref ref, - @NotNull PlayerRef playerRef, - @NotNull World world) { - - String input = ctx.getInputString(); - String subcommand = ""; - if (input != null && !input.isEmpty()) { - String[] parts = input.trim().split("\\s+"); - if (parts.length > 1) { - subcommand = parts[1].toLowerCase(); - } - } - - switch (subcommand) { - case "reload" -> handleReload(ctx, playerRef); - case "version", "ver" -> showVersion(ctx); - default -> showHelp(ctx); - } - } - - private void handleReload(@NotNull CommandContext ctx, @NotNull PlayerRef playerRef) { - if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.ADMIN_RELOAD)) { - ctx.sendMessage(CommandUtil.error("You don't have permission to reload.")); - return; - } - - ConfigManager.get().reloadAll(); - ctx.sendMessage(CommandUtil.success("Configuration reloaded.")); - } - - private void showVersion(@NotNull CommandContext ctx) { - ctx.sendMessage(CommandUtil.info("HyperEssentials v" + BuildInfo.VERSION)); - } - - private void showHelp(@NotNull CommandContext ctx) { - ctx.sendMessage(CommandUtil.info("HyperEssentials v" + BuildInfo.VERSION)); - ctx.sendMessage(CommandUtil.msg("/hessentials reload - Reload configuration", CommandUtil.COLOR_GRAY)); - ctx.sendMessage(CommandUtil.msg("/hessentials version - Show version", CommandUtil.COLOR_GRAY)); - } -} +package com.hyperessentials.command; + +import com.hyperessentials.BuildInfo; +import com.hyperessentials.Permissions; +import com.hyperessentials.api.HyperEssentialsAPI; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.module.spawns.SpawnManager; +import com.hyperessentials.module.spawns.SpawnsModule; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * Main admin command for HyperEssentials (/hessentials). + */ +public class AdminCommand extends AbstractPlayerCommand { + + public AdminCommand() { + super("hessentials", "HyperEssentials admin command"); + addAliases("he", "hyperessentials"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + + String input = ctx.getInputString(); + String subcommand = ""; + if (input != null && !input.isEmpty()) { + String[] parts = input.trim().split("\\s+"); + if (parts.length > 1) { + subcommand = parts[1].toLowerCase(); + } + } + + switch (subcommand) { + case "reload" -> handleReload(ctx, playerRef); + case "importspawns" -> handleImportSpawns(ctx, playerRef); + case "version", "ver" -> showVersion(ctx); + case "help" -> showFullHelp(ctx); + case "admin" -> openAdminGui(ctx, store, ref, playerRef); + default -> openPlayerGui(ctx, store, ref, playerRef); + } + } + + private void handleReload(@NotNull CommandContext ctx, @NotNull PlayerRef playerRef) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.ADMIN_RELOAD)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to reload.")); + return; + } + + ConfigManager.get().reloadAll(); + ctx.sendMessage(CommandUtil.success("Configuration reloaded.")); + } + + private void handleImportSpawns(@NotNull CommandContext ctx, @NotNull PlayerRef playerRef) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.ADMIN)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to import spawns.")); + return; + } + + if (!HyperEssentialsAPI.isAvailable()) { + ctx.sendMessage(CommandUtil.error("HyperEssentials is not initialized.")); + return; + } + + SpawnsModule spawnsModule = HyperEssentialsAPI.getInstance().getSpawnsModule(); + if (spawnsModule == null || spawnsModule.getSpawnManager() == null) { + ctx.sendMessage(CommandUtil.error("Spawns module is not enabled.")); + return; + } + + SpawnManager spawnManager = spawnsModule.getSpawnManager(); + int imported = spawnManager.importWorldSpawns(); + + if (imported > 0) { + ctx.sendMessage(CommandUtil.success("Imported " + imported + " world spawn(s) from server config.")); + } else { + ctx.sendMessage(CommandUtil.error("No world spawns could be imported.")); + } + } + + private void showVersion(@NotNull CommandContext ctx) { + ctx.sendMessage(CommandUtil.info("HyperEssentials v" + BuildInfo.VERSION)); + } + + private void showHelp(@NotNull CommandContext ctx) { + ctx.sendMessage(CommandUtil.info("HyperEssentials v" + BuildInfo.VERSION)); + ctx.sendMessage(CommandUtil.msg("/he reload - Reload configuration", CommandUtil.COLOR_GRAY)); + ctx.sendMessage(CommandUtil.msg("/he importspawns - Import world spawns", CommandUtil.COLOR_GRAY)); + ctx.sendMessage(CommandUtil.msg("/he version - Show version", CommandUtil.COLOR_GRAY)); + ctx.sendMessage(CommandUtil.msg("/he help - Full command listing", CommandUtil.COLOR_GRAY)); + } + + private void showFullHelp(@NotNull CommandContext ctx) { + ctx.sendMessage(CommandUtil.msg("=== HyperEssentials v" + BuildInfo.VERSION + " ===", CommandUtil.COLOR_GOLD)); + + if (isModuleEnabled("homes")) { + ctx.sendMessage(CommandUtil.msg("[Homes] /sethome, /home, /delhome, /homes", CommandUtil.COLOR_AQUA)); + } + if (isModuleEnabled("warps")) { + ctx.sendMessage(CommandUtil.msg("[Warps] /warp, /setwarp, /delwarp, /warps, /warpinfo", CommandUtil.COLOR_AQUA)); + } + if (isModuleEnabled("spawns")) { + ctx.sendMessage(CommandUtil.msg("[Spawns] /spawn, /setspawn, /delspawn, /spawns, /spawninfo", CommandUtil.COLOR_AQUA)); + } + if (isModuleEnabled("teleport")) { + ctx.sendMessage(CommandUtil.msg("[Teleport] /tpa, /tpahere, /tpaccept, /tpdeny, /tpcancel, /tptoggle, /back, /rtp", CommandUtil.COLOR_AQUA)); + } + if (isModuleEnabled("kits")) { + ctx.sendMessage(CommandUtil.msg("[Kits] /kit, /kits, /createkit, /deletekit, /previewkit", CommandUtil.COLOR_AQUA)); + } + if (isModuleEnabled("moderation")) { + ctx.sendMessage(CommandUtil.msg("[Moderation] /ban, /unban, /mute, /unmute, /kick, /freeze, /vanish, /punishments, /ipban, /ipunban", CommandUtil.COLOR_AQUA)); + } + if (isModuleEnabled("utility")) { + ctx.sendMessage(CommandUtil.msg("[Utility] /heal, /fly, /god, /stamina, /repair, /repairmax, /durability, /maxstack,", CommandUtil.COLOR_AQUA)); + ctx.sendMessage(CommandUtil.msg(" /near, /clearchat, /clearinventory, /motd, /rules, /discord, /list,", CommandUtil.COLOR_AQUA)); + ctx.sendMessage(CommandUtil.msg(" /playtime, /joindate, /afk, /invsee, /trash, /sleeppercentage", CommandUtil.COLOR_AQUA)); + } + if (isModuleEnabled("announcements")) { + ctx.sendMessage(CommandUtil.msg("[Announcements] /broadcast, /announce", CommandUtil.COLOR_AQUA)); + } + ctx.sendMessage(CommandUtil.msg("[Admin] /he reload | version | help | importspawns", CommandUtil.COLOR_AQUA)); + } + + private void openPlayerGui(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef) { + if (!HyperEssentialsAPI.isAvailable()) { + showHelp(ctx); + return; + } + + GuiManager guiManager = HyperEssentialsAPI.getInstance().getGuiManager(); + if (guiManager.getPlayerRegistry().getEntries().isEmpty()) { + showHelp(ctx); + return; + } + + Player player = store.getComponent(ref, Player.getComponentType()); + if (player == null) { + showHelp(ctx); + return; + } + + if (!guiManager.openPlayerPage("dashboard", player, ref, store, playerRef)) { + showHelp(ctx); + } + } + + private void openAdminGui(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.ADMIN_GUI)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to access the admin panel.")); + return; + } + + if (!HyperEssentialsAPI.isAvailable()) { + ctx.sendMessage(CommandUtil.error("HyperEssentials is not initialized.")); + return; + } + + GuiManager guiManager = HyperEssentialsAPI.getInstance().getGuiManager(); + if (guiManager.getAdminRegistry().getEntries().isEmpty()) { + ctx.sendMessage(CommandUtil.error("No admin pages are registered.")); + return; + } + + Player player = store.getComponent(ref, Player.getComponentType()); + if (player == null) { + ctx.sendMessage(CommandUtil.error("Could not resolve player.")); + return; + } + + if (!guiManager.openAdminPage("admin_dashboard", player, ref, store, playerRef)) { + ctx.sendMessage(CommandUtil.error("Could not open admin dashboard.")); + } + } + + private boolean isModuleEnabled(@NotNull String name) { + return HyperEssentialsAPI.isAvailable() && HyperEssentialsAPI.getInstance().isModuleEnabled(name); + } +} diff --git a/src/main/java/com/hyperessentials/command/util/CommandUtil.java b/src/main/java/com/hyperessentials/command/util/CommandUtil.java index 1fae340..e6bbb3e 100644 --- a/src/main/java/com/hyperessentials/command/util/CommandUtil.java +++ b/src/main/java/com/hyperessentials/command/util/CommandUtil.java @@ -1,79 +1,94 @@ -package com.hyperessentials.command.util; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.integration.PermissionManager; -import com.hypixel.hytale.server.core.Message; -import org.jetbrains.annotations.NotNull; - -import java.util.UUID; - -/** - * Unified messaging utilities for HyperEssentials commands. - */ -public final class CommandUtil { - - public static final String COLOR_GOLD = "#FFAA00"; - public static final String COLOR_GREEN = "#55FF55"; - public static final String COLOR_RED = "#FF5555"; - public static final String COLOR_YELLOW = "#FFFF55"; - public static final String COLOR_GRAY = "#AAAAAA"; - public static final String COLOR_WHITE = "#FFFFFF"; - public static final String COLOR_AQUA = "#55FFFF"; - public static final String COLOR_DARK_GRAY = "#555555"; - - private CommandUtil() {} - - @NotNull - public static Message prefix() { - ConfigManager config = ConfigManager.get(); - String bracketColor = config.core().getPrefixBracketColor(); - String textColor = config.core().getPrefixColor(); - String text = config.core().getPrefixText(); - - return Message.raw("[").color(bracketColor) - .insert(Message.raw(text).color(textColor)) - .insert(Message.raw("] ").color(bracketColor)); - } - - @NotNull - public static Message msg(@NotNull String text, @NotNull String color) { - return prefix().insert(Message.raw(text).color(color)); - } - - @NotNull - public static Message error(@NotNull String text) { - return msg(text, COLOR_RED); - } - - @NotNull - public static Message success(@NotNull String text) { - return msg(text, COLOR_GREEN); - } - - @NotNull - public static Message info(@NotNull String text) { - return msg(text, COLOR_YELLOW); - } - - public static boolean hasPermission(@NotNull UUID playerUuid, @NotNull String permission) { - return PermissionManager.get().hasPermission(playerUuid, permission); - } - - public static int getPermissionValue(@NotNull UUID playerUuid, @NotNull String prefix, int defaultValue) { - return PermissionManager.get().getPermissionValue(playerUuid, prefix, defaultValue); - } - - @NotNull - public static String formatTime(long ms) { - long seconds = (ms + 999) / 1000; - if (seconds < 60) { - return seconds + " second" + (seconds == 1 ? "" : "s"); - } - long minutes = seconds / 60; - seconds = seconds % 60; - if (seconds == 0) { - return minutes + " minute" + (minutes == 1 ? "" : "s"); - } - return minutes + "m " + seconds + "s"; - } -} +package com.hyperessentials.command.util; + +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.integration.PermissionManager; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * Unified messaging utilities for HyperEssentials commands. + */ +public final class CommandUtil { + + public static final String COLOR_GOLD = "#FFAA00"; + public static final String COLOR_GREEN = "#55FF55"; + public static final String COLOR_RED = "#FF5555"; + public static final String COLOR_YELLOW = "#FFFF55"; + public static final String COLOR_GRAY = "#AAAAAA"; + public static final String COLOR_WHITE = "#FFFFFF"; + public static final String COLOR_AQUA = "#55FFFF"; + public static final String COLOR_DARK_GRAY = "#555555"; + + private CommandUtil() {} + + @NotNull + public static Message prefix() { + ConfigManager config = ConfigManager.get(); + String bracketColor = config.core().getPrefixBracketColor(); + String textColor = config.core().getPrefixColor(); + String text = config.core().getPrefixText(); + + return Message.raw("[").color(bracketColor) + .insert(Message.raw(text).color(textColor)) + .insert(Message.raw("] ").color(bracketColor)); + } + + @NotNull + public static Message msg(@NotNull String text, @NotNull String color) { + return prefix().insert(Message.raw(text).color(color)); + } + + @NotNull + public static Message error(@NotNull String text) { + return msg(text, COLOR_RED); + } + + @NotNull + public static Message success(@NotNull String text) { + return msg(text, COLOR_GREEN); + } + + @NotNull + public static Message info(@NotNull String text) { + return msg(text, COLOR_YELLOW); + } + + public static boolean hasPermission(@NotNull UUID playerUuid, @NotNull String permission) { + return PermissionManager.get().hasPermission(playerUuid, permission); + } + + public static int getPermissionValue(@NotNull UUID playerUuid, @NotNull String prefix, int defaultValue) { + return PermissionManager.get().getPermissionValue(playerUuid, prefix, defaultValue); + } + + @NotNull + public static String formatTime(long ms) { + long seconds = (ms + 999) / 1000; + if (seconds < 60) { + return seconds + " second" + (seconds == 1 ? "" : "s"); + } + long minutes = seconds / 60; + seconds = seconds % 60; + if (seconds == 0) { + return minutes + " minute" + (minutes == 1 ? "" : "s"); + } + return minutes + "m " + seconds + "s"; + } + + /** + * Finds an online player by name (case-insensitive). + * + * @param name the player name + * @return the PlayerRef if found online, null otherwise + */ + @Nullable + public static PlayerRef findOnlinePlayer(@NotNull String name) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + return plugin != null ? plugin.findOnlinePlayer(name) : null; + } +} diff --git a/src/main/java/com/hyperessentials/config/ConfigFile.java b/src/main/java/com/hyperessentials/config/ConfigFile.java index f8eb684..05fb8de 100644 --- a/src/main/java/com/hyperessentials/config/ConfigFile.java +++ b/src/main/java/com/hyperessentials/config/ConfigFile.java @@ -1,257 +1,289 @@ -package com.hyperessentials.config; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.hyperessentials.util.Logger; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; - -/** - * Abstract base class for configuration files. - */ -public abstract class ConfigFile { - - protected static final Gson GSON = new GsonBuilder() - .setPrettyPrinting() - .disableHtmlEscaping() - .create(); - - protected final Path filePath; - protected boolean needsSave = false; - protected ValidationResult lastValidationResult = null; - - protected ConfigFile(@NotNull Path filePath) { - this.filePath = filePath; - } - - @NotNull - public Path getFilePath() { - return filePath; - } - - public void load() { - if (!Files.exists(filePath)) { - Logger.info("[Config] Creating new config file: %s", filePath.getFileName()); - createDefaults(); - save(); - return; - } - - needsSave = false; - - try { - String json = Files.readString(filePath); - JsonObject root = JsonParser.parseString(json).getAsJsonObject(); - loadFromJson(root); - - if (needsSave) { - Logger.info("[Config] Adding missing keys to: %s", filePath.getFileName()); - save(); - } - } catch (Exception e) { - Logger.severe("[Config] Failed to load %s: %s", filePath.getFileName(), e.getMessage()); - createDefaults(); - } - } - - public void save() { - try { - Files.createDirectories(filePath.getParent()); - JsonObject json = toJson(); - Files.writeString(filePath, GSON.toJson(json)); - needsSave = false; - Logger.debug("[Config] Saved: %s", filePath.getFileName()); - } catch (IOException e) { - Logger.severe("[Config] Failed to save %s: %s", filePath.getFileName(), e.getMessage()); - } - } - - public void reload() { - load(); - } - - protected abstract void loadFromJson(@NotNull JsonObject root); - - @NotNull - protected abstract JsonObject toJson(); - - protected abstract void createDefaults(); - - @NotNull - public String getConfigName() { - return filePath.getFileName().toString(); - } - - @NotNull - public ValidationResult validate() { - return new ValidationResult(); - } - - public ValidationResult getLastValidationResult() { - return lastValidationResult; - } - - public void validateAndLog() { - lastValidationResult = validate(); - - if (lastValidationResult.hasIssues()) { - for (ValidationResult.Issue issue : lastValidationResult.getIssues()) { - Logger.warn("[Config] %s", issue); - } - - if (lastValidationResult.needsSave()) { - Logger.info("[Config] Saving corrected values to: %s", getConfigName()); - save(); - } - } - } - - protected int validateRange(@NotNull ValidationResult result, @NotNull String field, - int value, int min, int max, int defaultVal) { - if (value < min || value > max) { - result.addWarning(getConfigName(), field, - String.format("Value must be between %d and %d", min, max), - value, defaultVal); - needsSave = true; - return defaultVal; - } - return value; - } - - protected int validateMin(@NotNull ValidationResult result, @NotNull String field, - int value, int min, int defaultVal) { - if (value < min) { - result.addWarning(getConfigName(), field, - String.format("Value must be at least %d", min), - value, defaultVal); - needsSave = true; - return defaultVal; - } - return value; - } - - protected double validateRange(@NotNull ValidationResult result, @NotNull String field, - double value, double min, double max, double defaultVal) { - if (value < min || value > max) { - result.addWarning(getConfigName(), field, - String.format("Value must be between %.2f and %.2f", min, max), - value, defaultVal); - needsSave = true; - return defaultVal; - } - return value; - } - - protected double validateMin(@NotNull ValidationResult result, @NotNull String field, - double value, double min, double defaultVal) { - if (value < min) { - result.addWarning(getConfigName(), field, - String.format("Value must be at least %.2f", min), - value, defaultVal); - needsSave = true; - return defaultVal; - } - return value; - } - - @NotNull - protected String validateEnum(@NotNull ValidationResult result, @NotNull String field, - @NotNull String value, @NotNull String[] allowed, @NotNull String defaultVal) { - for (String a : allowed) { - if (a.equalsIgnoreCase(value)) { - return value; - } - } - result.addWarning(getConfigName(), field, - String.format("Value must be one of: %s", String.join(", ", allowed)), - value, defaultVal); - needsSave = true; - return defaultVal; - } - - protected boolean validateHexColor(@NotNull ValidationResult result, @NotNull String field, - @NotNull String value) { - if (!value.matches("^#[0-9A-Fa-f]{6}$")) { - result.addWarning(getConfigName(), field, - "Should be a hex color in format #RRGGBB", value); - return false; - } - return true; - } - - protected int getInt(@NotNull JsonObject obj, @NotNull String key, int defaultVal) { - if (obj.has(key)) { - return obj.get(key).getAsInt(); - } - needsSave = true; - return defaultVal; - } - - protected double getDouble(@NotNull JsonObject obj, @NotNull String key, double defaultVal) { - if (obj.has(key)) { - return obj.get(key).getAsDouble(); - } - needsSave = true; - return defaultVal; - } - - protected boolean getBool(@NotNull JsonObject obj, @NotNull String key, boolean defaultVal) { - if (obj.has(key)) { - return obj.get(key).getAsBoolean(); - } - needsSave = true; - return defaultVal; - } - - @NotNull - protected String getString(@NotNull JsonObject obj, @NotNull String key, @NotNull String defaultVal) { - if (obj.has(key)) { - return obj.get(key).getAsString(); - } - needsSave = true; - return defaultVal; - } - - @NotNull - protected List getStringList(@NotNull JsonObject obj, @NotNull String key) { - List list = new ArrayList<>(); - if (obj.has(key) && obj.get(key).isJsonArray()) { - obj.getAsJsonArray(key).forEach(el -> list.add(el.getAsString())); - } else if (!obj.has(key)) { - needsSave = true; - } - return list; - } - - protected boolean hasSection(@NotNull JsonObject obj, @NotNull String sectionName) { - if (obj.has(sectionName) && obj.get(sectionName).isJsonObject()) { - return true; - } - needsSave = true; - return false; - } - - @NotNull - protected JsonObject getSection(@NotNull JsonObject obj, @NotNull String sectionName) { - if (obj.has(sectionName) && obj.get(sectionName).isJsonObject()) { - return obj.getAsJsonObject(sectionName); - } - needsSave = true; - return new JsonObject(); - } - - @NotNull - protected JsonArray toJsonArray(@NotNull List list) { - JsonArray array = new JsonArray(); - list.forEach(array::add); - return array; - } -} +package com.hyperessentials.config; + +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 com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * Abstract base class for configuration files. + */ +public abstract class ConfigFile { + + protected static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .disableHtmlEscaping() + .create(); + + protected final Path filePath; + protected boolean needsSave = false; + protected ValidationResult lastValidationResult = null; + + protected ConfigFile(@NotNull Path filePath) { + this.filePath = filePath; + } + + @NotNull + public Path getFilePath() { + return filePath; + } + + public void load() { + if (!Files.exists(filePath)) { + Logger.info("[Config] Creating new config file: %s", filePath.getFileName()); + createDefaults(); + save(); + return; + } + + needsSave = false; + + try { + String json = Files.readString(filePath); + JsonObject root = JsonParser.parseString(json).getAsJsonObject(); + loadFromJson(root); + + // Auto-detect new keys even if loadFromJson didn't set needsSave + if (!needsSave) { + JsonObject fresh = toJson(); + if (hasNewKeys(root, fresh)) { + needsSave = true; + } + } + + if (needsSave) { + Logger.info("[Config] Adding missing keys to: %s", filePath.getFileName()); + save(); + } + } catch (Exception e) { + Logger.severe("[Config] Failed to load %s: %s", filePath.getFileName(), e.getMessage()); + createDefaults(); + } + } + + public void save() { + try { + Files.createDirectories(filePath.getParent()); + JsonObject json = toJson(); + Files.writeString(filePath, GSON.toJson(json)); + needsSave = false; + Logger.debug("[Config] Saved: %s", filePath.getFileName()); + } catch (IOException e) { + Logger.severe("[Config] Failed to save %s: %s", filePath.getFileName(), e.getMessage()); + } + } + + public void reload() { + load(); + } + + protected abstract void loadFromJson(@NotNull JsonObject root); + + @NotNull + protected abstract JsonObject toJson(); + + protected abstract void createDefaults(); + + @NotNull + public String getConfigName() { + return filePath.getFileName().toString(); + } + + @NotNull + public ValidationResult validate() { + return new ValidationResult(); + } + + public ValidationResult getLastValidationResult() { + return lastValidationResult; + } + + public void validateAndLog() { + lastValidationResult = validate(); + + if (lastValidationResult.hasIssues()) { + for (ValidationResult.Issue issue : lastValidationResult.getIssues()) { + Logger.warn("[Config] %s", issue); + } + + if (lastValidationResult.needsSave()) { + Logger.info("[Config] Saving corrected values to: %s", getConfigName()); + save(); + } + } + } + + protected int validateRange(@NotNull ValidationResult result, @NotNull String field, + int value, int min, int max, int defaultVal) { + if (value < min || value > max) { + result.addWarning(getConfigName(), field, + String.format("Value must be between %d and %d", min, max), + value, defaultVal); + needsSave = true; + return defaultVal; + } + return value; + } + + protected int validateMin(@NotNull ValidationResult result, @NotNull String field, + int value, int min, int defaultVal) { + if (value < min) { + result.addWarning(getConfigName(), field, + String.format("Value must be at least %d", min), + value, defaultVal); + needsSave = true; + return defaultVal; + } + return value; + } + + protected double validateRange(@NotNull ValidationResult result, @NotNull String field, + double value, double min, double max, double defaultVal) { + if (value < min || value > max) { + result.addWarning(getConfigName(), field, + String.format("Value must be between %.2f and %.2f", min, max), + value, defaultVal); + needsSave = true; + return defaultVal; + } + return value; + } + + protected double validateMin(@NotNull ValidationResult result, @NotNull String field, + double value, double min, double defaultVal) { + if (value < min) { + result.addWarning(getConfigName(), field, + String.format("Value must be at least %.2f", min), + value, defaultVal); + needsSave = true; + return defaultVal; + } + return value; + } + + @NotNull + protected String validateEnum(@NotNull ValidationResult result, @NotNull String field, + @NotNull String value, @NotNull String[] allowed, @NotNull String defaultVal) { + for (String a : allowed) { + if (a.equalsIgnoreCase(value)) { + return value; + } + } + result.addWarning(getConfigName(), field, + String.format("Value must be one of: %s", String.join(", ", allowed)), + value, defaultVal); + needsSave = true; + return defaultVal; + } + + protected boolean validateHexColor(@NotNull ValidationResult result, @NotNull String field, + @NotNull String value) { + if (!value.matches("^#[0-9A-Fa-f]{6}$")) { + result.addWarning(getConfigName(), field, + "Should be a hex color in format #RRGGBB", value); + return false; + } + return true; + } + + protected int getInt(@NotNull JsonObject obj, @NotNull String key, int defaultVal) { + if (obj.has(key)) { + return obj.get(key).getAsInt(); + } + needsSave = true; + return defaultVal; + } + + protected double getDouble(@NotNull JsonObject obj, @NotNull String key, double defaultVal) { + if (obj.has(key)) { + return obj.get(key).getAsDouble(); + } + needsSave = true; + return defaultVal; + } + + protected boolean getBool(@NotNull JsonObject obj, @NotNull String key, boolean defaultVal) { + if (obj.has(key)) { + return obj.get(key).getAsBoolean(); + } + needsSave = true; + return defaultVal; + } + + @NotNull + protected String getString(@NotNull JsonObject obj, @NotNull String key, @NotNull String defaultVal) { + if (obj.has(key)) { + return obj.get(key).getAsString(); + } + needsSave = true; + return defaultVal; + } + + @NotNull + protected List getStringList(@NotNull JsonObject obj, @NotNull String key) { + List list = new ArrayList<>(); + if (obj.has(key) && obj.get(key).isJsonArray()) { + obj.getAsJsonArray(key).forEach(el -> list.add(el.getAsString())); + } else if (!obj.has(key)) { + needsSave = true; + } + return list; + } + + protected boolean hasSection(@NotNull JsonObject obj, @NotNull String sectionName) { + if (obj.has(sectionName) && obj.get(sectionName).isJsonObject()) { + return true; + } + needsSave = true; + return false; + } + + @NotNull + protected JsonObject getSection(@NotNull JsonObject obj, @NotNull String sectionName) { + if (obj.has(sectionName) && obj.get(sectionName).isJsonObject()) { + return obj.getAsJsonObject(sectionName); + } + needsSave = true; + return new JsonObject(); + } + + @NotNull + protected JsonArray toJsonArray(@NotNull List list) { + JsonArray array = new JsonArray(); + list.forEach(array::add); + return array; + } + + /** + * Recursively checks whether the fresh JSON has keys not present in the original. + * + * @param original the JSON loaded from disk + * @param fresh the JSON produced by serializing the current in-memory state + * @return true if fresh has at least one key (at any depth) not in original + */ + private static boolean hasNewKeys(@NotNull JsonObject original, @NotNull JsonObject fresh) { + for (String key : fresh.keySet()) { + if (!original.has(key)) { + return true; + } + JsonElement origVal = original.get(key); + JsonElement freshVal = fresh.get(key); + if (freshVal.isJsonObject() && origVal.isJsonObject()) { + if (hasNewKeys(origVal.getAsJsonObject(), freshVal.getAsJsonObject())) { + return true; + } + } + } + return false; + } +} diff --git a/src/main/java/com/hyperessentials/config/ConfigManager.java b/src/main/java/com/hyperessentials/config/ConfigManager.java index c2a4e9c..0232e22 100644 --- a/src/main/java/com/hyperessentials/config/ConfigManager.java +++ b/src/main/java/com/hyperessentials/config/ConfigManager.java @@ -1,186 +1,189 @@ -package com.hyperessentials.config; - -import com.hyperessentials.config.modules.*; -import com.hyperessentials.util.Logger; -import org.jetbrains.annotations.NotNull; - -import java.nio.file.Path; - -/** - * Central manager for all HyperEssentials configuration. - */ -public class ConfigManager { - - private static ConfigManager instance; - - private Path dataDir; - private CoreConfig coreConfig; - private HomesConfig homesConfig; - private WarpsConfig warpsConfig; - private SpawnsConfig spawnsConfig; - private TeleportConfig teleportConfig; - private WarmupConfig warmupConfig; - private KitsConfig kitsConfig; - private ModerationConfig moderationConfig; - private VanishConfig vanishConfig; - private UtilityConfig utilityConfig; - private AnnouncementsConfig announcementsConfig; - private RtpConfig rtpConfig; - - private ConfigManager() {} - - @NotNull - public static ConfigManager get() { - if (instance == null) { - instance = new ConfigManager(); - } - return instance; - } - - public void loadAll(@NotNull Path dataDir) { - this.dataDir = dataDir; - Logger.info("[Config] Loading configuration from: %s", dataDir.toAbsolutePath()); - - coreConfig = new CoreConfig(dataDir.resolve("config.json")); - coreConfig.load(); - - Path configDir = dataDir.resolve("config"); - - homesConfig = new HomesConfig(configDir.resolve("homes.json")); - homesConfig.load(); - - warpsConfig = new WarpsConfig(configDir.resolve("warps.json")); - warpsConfig.load(); - - spawnsConfig = new SpawnsConfig(configDir.resolve("spawns.json")); - spawnsConfig.load(); - - teleportConfig = new TeleportConfig(configDir.resolve("teleport.json")); - teleportConfig.load(); - - warmupConfig = new WarmupConfig(configDir.resolve("warmup.json")); - warmupConfig.load(); - - kitsConfig = new KitsConfig(configDir.resolve("kits.json")); - kitsConfig.load(); - - moderationConfig = new ModerationConfig(configDir.resolve("moderation.json")); - moderationConfig.load(); - - vanishConfig = new VanishConfig(configDir.resolve("vanish.json")); - vanishConfig.load(); - - utilityConfig = new UtilityConfig(configDir.resolve("utility.json")); - utilityConfig.load(); - - announcementsConfig = new AnnouncementsConfig(configDir.resolve("announcements.json")); - announcementsConfig.load(); - - rtpConfig = new RtpConfig(configDir.resolve("rtp.json")); - rtpConfig.load(); - - validateAll(); - Logger.info("[Config] Configuration loaded successfully"); - } - - private void validateAll() { - ValidationResult combined = new ValidationResult(); - validateAndMerge(combined, coreConfig); - validateAndMerge(combined, homesConfig); - validateAndMerge(combined, warpsConfig); - validateAndMerge(combined, spawnsConfig); - validateAndMerge(combined, teleportConfig); - validateAndMerge(combined, warmupConfig); - - if (combined.hasIssues()) { - Logger.info("[Config] Validation complete: %d warning(s), %d error(s)", - combined.getWarnings().size(), combined.getErrors().size()); - } - } - - private void validateAndMerge(@NotNull ValidationResult combined, @NotNull ConfigFile config) { - config.validateAndLog(); - if (config.getLastValidationResult() != null) { - combined.merge(config.getLastValidationResult()); - } - } - - public void reloadAll() { - Logger.info("[Config] Reloading configuration..."); - coreConfig.reload(); - homesConfig.reload(); - warpsConfig.reload(); - spawnsConfig.reload(); - teleportConfig.reload(); - warmupConfig.reload(); - kitsConfig.reload(); - moderationConfig.reload(); - vanishConfig.reload(); - utilityConfig.reload(); - announcementsConfig.reload(); - rtpConfig.reload(); - validateAll(); - Logger.info("[Config] Configuration reloaded"); - } - - public void saveAll() { - coreConfig.save(); - homesConfig.save(); - warpsConfig.save(); - spawnsConfig.save(); - teleportConfig.save(); - warmupConfig.save(); - kitsConfig.save(); - moderationConfig.save(); - vanishConfig.save(); - utilityConfig.save(); - announcementsConfig.save(); - rtpConfig.save(); - } - - @NotNull public CoreConfig core() { return coreConfig; } - @NotNull public HomesConfig homes() { return homesConfig; } - @NotNull public WarpsConfig warps() { return warpsConfig; } - @NotNull public SpawnsConfig spawns() { return spawnsConfig; } - @NotNull public TeleportConfig teleport() { return teleportConfig; } - @NotNull public WarmupConfig warmup() { return warmupConfig; } - @NotNull public KitsConfig kits() { return kitsConfig; } - @NotNull public ModerationConfig moderation() { return moderationConfig; } - @NotNull public VanishConfig vanish() { return vanishConfig; } - @NotNull public UtilityConfig utility() { return utilityConfig; } - @NotNull public AnnouncementsConfig announcements() { return announcementsConfig; } - @NotNull public RtpConfig rtp() { return rtpConfig; } - - @SuppressWarnings("unchecked") - public T getModule(@NotNull Class clazz) { - if (clazz == HomesConfig.class) return (T) homesConfig; - if (clazz == WarpsConfig.class) return (T) warpsConfig; - if (clazz == SpawnsConfig.class) return (T) spawnsConfig; - if (clazz == TeleportConfig.class) return (T) teleportConfig; - if (clazz == WarmupConfig.class) return (T) warmupConfig; - if (clazz == KitsConfig.class) return (T) kitsConfig; - if (clazz == ModerationConfig.class) return (T) moderationConfig; - if (clazz == VanishConfig.class) return (T) vanishConfig; - if (clazz == UtilityConfig.class) return (T) utilityConfig; - if (clazz == AnnouncementsConfig.class) return (T) announcementsConfig; - if (clazz == RtpConfig.class) return (T) rtpConfig; - throw new IllegalArgumentException("Unknown module config: " + clazz.getName()); - } - - public boolean isModuleEnabled(@NotNull String moduleName) { - return switch (moduleName) { - case "homes" -> homesConfig.isEnabled(); - case "warps" -> warpsConfig.isEnabled(); - case "spawns" -> spawnsConfig.isEnabled(); - case "teleport" -> teleportConfig.isEnabled(); - case "warmup" -> warmupConfig.isEnabled(); - case "kits" -> kitsConfig.isEnabled(); - case "moderation" -> moderationConfig.isEnabled(); - case "vanish" -> vanishConfig.isEnabled(); - case "utility" -> utilityConfig.isEnabled(); - case "announcements" -> announcementsConfig.isEnabled(); - case "rtp" -> rtpConfig.isEnabled(); - default -> false; - }; - } -} +package com.hyperessentials.config; + +import com.hyperessentials.config.modules.*; +import com.hyperessentials.migration.MigrationRunner; +import com.hyperessentials.migration.MigrationType; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +/** + * Central manager for all HyperEssentials configuration. + */ +public class ConfigManager { + + private static ConfigManager instance; + + private Path dataDir; + private CoreConfig coreConfig; + private HomesConfig homesConfig; + private WarpsConfig warpsConfig; + private SpawnsConfig spawnsConfig; + private TeleportConfig teleportConfig; + private WarmupConfig warmupConfig; + private KitsConfig kitsConfig; + private ModerationConfig moderationConfig; + private VanishConfig vanishConfig; + private UtilityConfig utilityConfig; + private AnnouncementsConfig announcementsConfig; + private DebugConfig debugConfig; + + private ConfigManager() {} + + @NotNull + public static ConfigManager get() { + if (instance == null) { + instance = new ConfigManager(); + } + return instance; + } + + public void loadAll(@NotNull Path dataDir) { + this.dataDir = dataDir; + Logger.info("[Config] Loading configuration from: %s", dataDir.toAbsolutePath()); + + // Run pending config migrations before loading + MigrationRunner.runPendingMigrations(dataDir, MigrationType.CONFIG); + + coreConfig = new CoreConfig(dataDir.resolve("config.json")); + coreConfig.load(); + + Path configDir = dataDir.resolve("config"); + + homesConfig = new HomesConfig(configDir.resolve("homes.json")); + homesConfig.load(); + + warpsConfig = new WarpsConfig(configDir.resolve("warps.json")); + warpsConfig.load(); + + spawnsConfig = new SpawnsConfig(configDir.resolve("spawns.json")); + spawnsConfig.load(); + + teleportConfig = new TeleportConfig(configDir.resolve("teleport.json")); + teleportConfig.load(); + + warmupConfig = new WarmupConfig(configDir.resolve("warmup.json")); + warmupConfig.load(); + + kitsConfig = new KitsConfig(configDir.resolve("kits.json")); + kitsConfig.load(); + + moderationConfig = new ModerationConfig(configDir.resolve("moderation.json")); + moderationConfig.load(); + + vanishConfig = new VanishConfig(configDir.resolve("vanish.json")); + vanishConfig.load(); + + utilityConfig = new UtilityConfig(configDir.resolve("utility.json")); + utilityConfig.load(); + + announcementsConfig = new AnnouncementsConfig(configDir.resolve("announcements.json")); + announcementsConfig.load(); + + debugConfig = new DebugConfig(configDir.resolve("debug.json")); + debugConfig.load(); + + validateAll(); + Logger.info("[Config] Configuration loaded successfully"); + } + + private void validateAll() { + ValidationResult combined = new ValidationResult(); + validateAndMerge(combined, coreConfig); + validateAndMerge(combined, homesConfig); + validateAndMerge(combined, warpsConfig); + validateAndMerge(combined, spawnsConfig); + validateAndMerge(combined, teleportConfig); + validateAndMerge(combined, warmupConfig); + + if (combined.hasIssues()) { + Logger.info("[Config] Validation complete: %d warning(s), %d error(s)", + combined.getWarnings().size(), combined.getErrors().size()); + } + } + + private void validateAndMerge(@NotNull ValidationResult combined, @NotNull ConfigFile config) { + config.validateAndLog(); + if (config.getLastValidationResult() != null) { + combined.merge(config.getLastValidationResult()); + } + } + + public void reloadAll() { + Logger.info("[Config] Reloading configuration..."); + coreConfig.reload(); + homesConfig.reload(); + warpsConfig.reload(); + spawnsConfig.reload(); + teleportConfig.reload(); + warmupConfig.reload(); + kitsConfig.reload(); + moderationConfig.reload(); + vanishConfig.reload(); + utilityConfig.reload(); + announcementsConfig.reload(); + debugConfig.reload(); + validateAll(); + Logger.info("[Config] Configuration reloaded"); + } + + public void saveAll() { + coreConfig.save(); + homesConfig.save(); + warpsConfig.save(); + spawnsConfig.save(); + teleportConfig.save(); + warmupConfig.save(); + kitsConfig.save(); + moderationConfig.save(); + vanishConfig.save(); + utilityConfig.save(); + announcementsConfig.save(); + debugConfig.save(); + } + + @NotNull public CoreConfig core() { return coreConfig; } + @NotNull public HomesConfig homes() { return homesConfig; } + @NotNull public WarpsConfig warps() { return warpsConfig; } + @NotNull public SpawnsConfig spawns() { return spawnsConfig; } + @NotNull public TeleportConfig teleport() { return teleportConfig; } + @NotNull public WarmupConfig warmup() { return warmupConfig; } + @NotNull public KitsConfig kits() { return kitsConfig; } + @NotNull public ModerationConfig moderation() { return moderationConfig; } + @NotNull public VanishConfig vanish() { return vanishConfig; } + @NotNull public UtilityConfig utility() { return utilityConfig; } + @NotNull public AnnouncementsConfig announcements() { return announcementsConfig; } + @NotNull public DebugConfig debug() { return debugConfig; } + + @SuppressWarnings("unchecked") + public T getModule(@NotNull Class clazz) { + if (clazz == HomesConfig.class) return (T) homesConfig; + if (clazz == WarpsConfig.class) return (T) warpsConfig; + if (clazz == SpawnsConfig.class) return (T) spawnsConfig; + if (clazz == TeleportConfig.class) return (T) teleportConfig; + if (clazz == WarmupConfig.class) return (T) warmupConfig; + if (clazz == KitsConfig.class) return (T) kitsConfig; + if (clazz == ModerationConfig.class) return (T) moderationConfig; + if (clazz == VanishConfig.class) return (T) vanishConfig; + if (clazz == UtilityConfig.class) return (T) utilityConfig; + if (clazz == AnnouncementsConfig.class) return (T) announcementsConfig; + throw new IllegalArgumentException("Unknown module config: " + clazz.getName()); + } + + public boolean isModuleEnabled(@NotNull String moduleName) { + return switch (moduleName) { + case "homes" -> homesConfig.isEnabled(); + case "warps" -> warpsConfig.isEnabled(); + case "spawns" -> spawnsConfig.isEnabled(); + case "teleport" -> teleportConfig.isEnabled(); + case "warmup" -> warmupConfig.isEnabled(); + case "kits" -> kitsConfig.isEnabled(); + case "moderation" -> moderationConfig.isEnabled(); + case "vanish" -> vanishConfig.isEnabled(); + case "utility" -> utilityConfig.isEnabled(); + case "announcements" -> announcementsConfig.isEnabled(); + default -> false; + }; + } +} diff --git a/src/main/java/com/hyperessentials/config/CoreConfig.java b/src/main/java/com/hyperessentials/config/CoreConfig.java index 3bbb228..480f415 100644 --- a/src/main/java/com/hyperessentials/config/CoreConfig.java +++ b/src/main/java/com/hyperessentials/config/CoreConfig.java @@ -1,95 +1,95 @@ -package com.hyperessentials.config; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; - -import java.nio.file.Path; - -/** - * Core configuration for HyperEssentials. - */ -public class CoreConfig extends ConfigFile { - - private String prefixText = "HyperEssentials"; - private String prefixColor = "#FFAA00"; - private String prefixBracketColor = "#AAAAAA"; - private String primaryColor = "#55FFFF"; - private String secondaryColor = "#55FF55"; - private String errorColor = "#FF5555"; - - private boolean adminRequiresOp = true; - private boolean allowWithoutPermissionMod = true; - private String storageType = "json"; - private boolean updateCheck = true; - - private int configVersion = 1; - - public CoreConfig(@NotNull Path filePath) { - super(filePath); - } - - @Override - protected void createDefaults() { - // All defaults are set via field initializers - } - - @Override - protected void loadFromJson(@NotNull JsonObject root) { - prefixText = getString(root, "prefixText", prefixText); - prefixColor = getString(root, "prefixColor", prefixColor); - prefixBracketColor = getString(root, "prefixBracketColor", prefixBracketColor); - primaryColor = getString(root, "primaryColor", primaryColor); - secondaryColor = getString(root, "secondaryColor", secondaryColor); - errorColor = getString(root, "errorColor", errorColor); - adminRequiresOp = getBool(root, "adminRequiresOp", adminRequiresOp); - allowWithoutPermissionMod = getBool(root, "allowWithoutPermissionMod", allowWithoutPermissionMod); - storageType = getString(root, "storageType", storageType); - updateCheck = getBool(root, "updateCheck", updateCheck); - configVersion = getInt(root, "configVersion", configVersion); - } - - @Override - @NotNull - protected JsonObject toJson() { - JsonObject root = new JsonObject(); - root.addProperty("prefixText", prefixText); - root.addProperty("prefixColor", prefixColor); - root.addProperty("prefixBracketColor", prefixBracketColor); - root.addProperty("primaryColor", primaryColor); - root.addProperty("secondaryColor", secondaryColor); - root.addProperty("errorColor", errorColor); - root.addProperty("adminRequiresOp", adminRequiresOp); - root.addProperty("allowWithoutPermissionMod", allowWithoutPermissionMod); - root.addProperty("storageType", storageType); - root.addProperty("updateCheck", updateCheck); - root.addProperty("configVersion", configVersion); - return root; - } - - @Override - @NotNull - public ValidationResult validate() { - ValidationResult result = new ValidationResult(); - validateHexColor(result, "prefixColor", prefixColor); - validateHexColor(result, "prefixBracketColor", prefixBracketColor); - validateHexColor(result, "primaryColor", primaryColor); - validateHexColor(result, "secondaryColor", secondaryColor); - validateHexColor(result, "errorColor", errorColor); - storageType = validateEnum(result, "storageType", storageType, - new String[]{"json"}, "json"); - return result; - } - - // Getters - public String getPrefixText() { return prefixText; } - public String getPrefixColor() { return prefixColor; } - public String getPrefixBracketColor() { return prefixBracketColor; } - public String getPrimaryColor() { return primaryColor; } - public String getSecondaryColor() { return secondaryColor; } - public String getErrorColor() { return errorColor; } - public boolean isAdminRequiresOp() { return adminRequiresOp; } - public boolean isAllowWithoutPermissionMod() { return allowWithoutPermissionMod; } - public String getStorageType() { return storageType; } - public boolean isUpdateCheck() { return updateCheck; } - public int getConfigVersion() { return configVersion; } -} +package com.hyperessentials.config; + +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +/** + * Core configuration for HyperEssentials. + */ +public class CoreConfig extends ConfigFile { + + private String prefixText = "HyperEssentials"; + private String prefixColor = "#FFAA00"; + private String prefixBracketColor = "#AAAAAA"; + private String primaryColor = "#55FFFF"; + private String secondaryColor = "#55FF55"; + private String errorColor = "#FF5555"; + + private boolean adminRequiresOp = true; + private boolean allowWithoutPermissionMod = true; + private String storageType = "json"; + private boolean updateCheck = true; + + private int configVersion = 1; + + public CoreConfig(@NotNull Path filePath) { + super(filePath); + } + + @Override + protected void createDefaults() { + // All defaults are set via field initializers + } + + @Override + protected void loadFromJson(@NotNull JsonObject root) { + prefixText = getString(root, "prefixText", prefixText); + prefixColor = getString(root, "prefixColor", prefixColor); + prefixBracketColor = getString(root, "prefixBracketColor", prefixBracketColor); + primaryColor = getString(root, "primaryColor", primaryColor); + secondaryColor = getString(root, "secondaryColor", secondaryColor); + errorColor = getString(root, "errorColor", errorColor); + adminRequiresOp = getBool(root, "adminRequiresOp", adminRequiresOp); + allowWithoutPermissionMod = getBool(root, "allowWithoutPermissionMod", allowWithoutPermissionMod); + storageType = getString(root, "storageType", storageType); + updateCheck = getBool(root, "updateCheck", updateCheck); + configVersion = getInt(root, "configVersion", configVersion); + } + + @Override + @NotNull + protected JsonObject toJson() { + JsonObject root = new JsonObject(); + root.addProperty("prefixText", prefixText); + root.addProperty("prefixColor", prefixColor); + root.addProperty("prefixBracketColor", prefixBracketColor); + root.addProperty("primaryColor", primaryColor); + root.addProperty("secondaryColor", secondaryColor); + root.addProperty("errorColor", errorColor); + root.addProperty("adminRequiresOp", adminRequiresOp); + root.addProperty("allowWithoutPermissionMod", allowWithoutPermissionMod); + root.addProperty("storageType", storageType); + root.addProperty("updateCheck", updateCheck); + root.addProperty("configVersion", configVersion); + return root; + } + + @Override + @NotNull + public ValidationResult validate() { + ValidationResult result = new ValidationResult(); + validateHexColor(result, "prefixColor", prefixColor); + validateHexColor(result, "prefixBracketColor", prefixBracketColor); + validateHexColor(result, "primaryColor", primaryColor); + validateHexColor(result, "secondaryColor", secondaryColor); + validateHexColor(result, "errorColor", errorColor); + storageType = validateEnum(result, "storageType", storageType, + new String[]{"json"}, "json"); + return result; + } + + // Getters + public String getPrefixText() { return prefixText; } + public String getPrefixColor() { return prefixColor; } + public String getPrefixBracketColor() { return prefixBracketColor; } + public String getPrimaryColor() { return primaryColor; } + public String getSecondaryColor() { return secondaryColor; } + public String getErrorColor() { return errorColor; } + public boolean isAdminRequiresOp() { return adminRequiresOp; } + public boolean isAllowWithoutPermissionMod() { return allowWithoutPermissionMod; } + public String getStorageType() { return storageType; } + public boolean isUpdateCheck() { return updateCheck; } + public int getConfigVersion() { return configVersion; } +} diff --git a/src/main/java/com/hyperessentials/config/ModuleConfig.java b/src/main/java/com/hyperessentials/config/ModuleConfig.java index e7fe15b..6d1a80b 100644 --- a/src/main/java/com/hyperessentials/config/ModuleConfig.java +++ b/src/main/java/com/hyperessentials/config/ModuleConfig.java @@ -1,52 +1,52 @@ -package com.hyperessentials.config; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; - -import java.nio.file.Path; - -/** - * Abstract base class for module configuration files. - */ -public abstract class ModuleConfig extends ConfigFile { - - protected boolean enabled = true; - - protected ModuleConfig(@NotNull Path filePath) { - super(filePath); - } - - @NotNull - public abstract String getModuleName(); - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - protected boolean getDefaultEnabled() { - return true; - } - - @Override - protected void loadFromJson(@NotNull JsonObject root) { - this.enabled = getBool(root, "enabled", getDefaultEnabled()); - loadModuleSettings(root); - } - - @Override - @NotNull - protected JsonObject toJson() { - JsonObject root = new JsonObject(); - root.addProperty("enabled", enabled); - writeModuleSettings(root); - return root; - } - - protected abstract void loadModuleSettings(@NotNull JsonObject root); - - protected abstract void writeModuleSettings(@NotNull JsonObject root); -} +package com.hyperessentials.config; + +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +/** + * Abstract base class for module configuration files. + */ +public abstract class ModuleConfig extends ConfigFile { + + protected boolean enabled = true; + + protected ModuleConfig(@NotNull Path filePath) { + super(filePath); + } + + @NotNull + public abstract String getModuleName(); + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + protected boolean getDefaultEnabled() { + return true; + } + + @Override + protected void loadFromJson(@NotNull JsonObject root) { + this.enabled = getBool(root, "enabled", getDefaultEnabled()); + loadModuleSettings(root); + } + + @Override + @NotNull + protected JsonObject toJson() { + JsonObject root = new JsonObject(); + root.addProperty("enabled", enabled); + writeModuleSettings(root); + return root; + } + + protected abstract void loadModuleSettings(@NotNull JsonObject root); + + protected abstract void writeModuleSettings(@NotNull JsonObject root); +} diff --git a/src/main/java/com/hyperessentials/config/ValidationResult.java b/src/main/java/com/hyperessentials/config/ValidationResult.java index 794dd70..54ec932 100644 --- a/src/main/java/com/hyperessentials/config/ValidationResult.java +++ b/src/main/java/com/hyperessentials/config/ValidationResult.java @@ -1,125 +1,125 @@ -package com.hyperessentials.config; - -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; - -/** - * Result of configuration validation. - */ -public class ValidationResult { - - public enum Severity { - WARNING, - ERROR - } - - public record Issue( - @NotNull Severity severity, - @NotNull String configFile, - @NotNull String field, - @NotNull String message, - @NotNull String originalValue, - String correctedValue - ) { - public static Issue warning(@NotNull String configFile, @NotNull String field, - @NotNull String message, Object original, Object corrected) { - return new Issue(Severity.WARNING, configFile, field, message, - String.valueOf(original), String.valueOf(corrected)); - } - - public static Issue warning(@NotNull String configFile, @NotNull String field, - @NotNull String message, Object original) { - return new Issue(Severity.WARNING, configFile, field, message, - String.valueOf(original), null); - } - - public static Issue error(@NotNull String configFile, @NotNull String field, - @NotNull String message, Object original) { - return new Issue(Severity.ERROR, configFile, field, message, - String.valueOf(original), null); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("[").append(severity).append("] "); - sb.append(configFile).append(" -> ").append(field).append(": "); - sb.append(message); - sb.append(" (was: ").append(originalValue); - if (correctedValue != null) { - sb.append(", corrected to: ").append(correctedValue); - } - sb.append(")"); - return sb.toString(); - } - } - - private final List issues = new ArrayList<>(); - private boolean needsSave = false; - - public void addIssue(@NotNull Issue issue) { - issues.add(issue); - if (issue.correctedValue() != null) { - needsSave = true; - } - } - - public void addWarning(@NotNull String configFile, @NotNull String field, - @NotNull String message, Object original, Object corrected) { - addIssue(Issue.warning(configFile, field, message, original, corrected)); - } - - public void addWarning(@NotNull String configFile, @NotNull String field, - @NotNull String message, Object original) { - addIssue(Issue.warning(configFile, field, message, original)); - } - - public void addError(@NotNull String configFile, @NotNull String field, - @NotNull String message, Object original) { - addIssue(Issue.error(configFile, field, message, original)); - } - - @NotNull - public List getIssues() { - return issues; - } - - @NotNull - public List getWarnings() { - return issues.stream() - .filter(i -> i.severity() == Severity.WARNING) - .toList(); - } - - @NotNull - public List getErrors() { - return issues.stream() - .filter(i -> i.severity() == Severity.ERROR) - .toList(); - } - - public boolean hasIssues() { - return !issues.isEmpty(); - } - - public boolean hasErrors() { - return issues.stream().anyMatch(i -> i.severity() == Severity.ERROR); - } - - public boolean needsSave() { - return needsSave; - } - - public int size() { - return issues.size(); - } - - public void merge(@NotNull ValidationResult other) { - issues.addAll(other.issues); - if (other.needsSave) { - needsSave = true; - } - } -} +package com.hyperessentials.config; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * Result of configuration validation. + */ +public class ValidationResult { + + public enum Severity { + WARNING, + ERROR + } + + public record Issue( + @NotNull Severity severity, + @NotNull String configFile, + @NotNull String field, + @NotNull String message, + @NotNull String originalValue, + String correctedValue + ) { + public static Issue warning(@NotNull String configFile, @NotNull String field, + @NotNull String message, Object original, Object corrected) { + return new Issue(Severity.WARNING, configFile, field, message, + String.valueOf(original), String.valueOf(corrected)); + } + + public static Issue warning(@NotNull String configFile, @NotNull String field, + @NotNull String message, Object original) { + return new Issue(Severity.WARNING, configFile, field, message, + String.valueOf(original), null); + } + + public static Issue error(@NotNull String configFile, @NotNull String field, + @NotNull String message, Object original) { + return new Issue(Severity.ERROR, configFile, field, message, + String.valueOf(original), null); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("[").append(severity).append("] "); + sb.append(configFile).append(" -> ").append(field).append(": "); + sb.append(message); + sb.append(" (was: ").append(originalValue); + if (correctedValue != null) { + sb.append(", corrected to: ").append(correctedValue); + } + sb.append(")"); + return sb.toString(); + } + } + + private final List issues = new ArrayList<>(); + private boolean needsSave = false; + + public void addIssue(@NotNull Issue issue) { + issues.add(issue); + if (issue.correctedValue() != null) { + needsSave = true; + } + } + + public void addWarning(@NotNull String configFile, @NotNull String field, + @NotNull String message, Object original, Object corrected) { + addIssue(Issue.warning(configFile, field, message, original, corrected)); + } + + public void addWarning(@NotNull String configFile, @NotNull String field, + @NotNull String message, Object original) { + addIssue(Issue.warning(configFile, field, message, original)); + } + + public void addError(@NotNull String configFile, @NotNull String field, + @NotNull String message, Object original) { + addIssue(Issue.error(configFile, field, message, original)); + } + + @NotNull + public List getIssues() { + return issues; + } + + @NotNull + public List getWarnings() { + return issues.stream() + .filter(i -> i.severity() == Severity.WARNING) + .toList(); + } + + @NotNull + public List getErrors() { + return issues.stream() + .filter(i -> i.severity() == Severity.ERROR) + .toList(); + } + + public boolean hasIssues() { + return !issues.isEmpty(); + } + + public boolean hasErrors() { + return issues.stream().anyMatch(i -> i.severity() == Severity.ERROR); + } + + public boolean needsSave() { + return needsSave; + } + + public int size() { + return issues.size(); + } + + public void merge(@NotNull ValidationResult other) { + issues.addAll(other.issues); + if (other.needsSave) { + needsSave = true; + } + } +} diff --git a/src/main/java/com/hyperessentials/config/modules/AnnouncementsConfig.java b/src/main/java/com/hyperessentials/config/modules/AnnouncementsConfig.java index 6267c68..1635495 100644 --- a/src/main/java/com/hyperessentials/config/modules/AnnouncementsConfig.java +++ b/src/main/java/com/hyperessentials/config/modules/AnnouncementsConfig.java @@ -1,15 +1,66 @@ -package com.hyperessentials.config.modules; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; -import java.nio.file.Path; -import com.hyperessentials.config.ModuleConfig; - -public class AnnouncementsConfig extends ModuleConfig { - public AnnouncementsConfig(@NotNull Path filePath) { super(filePath); } - @Override @NotNull public String getModuleName() { return "announcements"; } - @Override protected boolean getDefaultEnabled() { return false; } - @Override protected void createDefaults() {} - @Override protected void loadModuleSettings(@NotNull JsonObject root) {} - @Override protected void writeModuleSettings(@NotNull JsonObject root) {} -} +package com.hyperessentials.config.modules; + +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNull; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import com.hyperessentials.config.ModuleConfig; + +public class AnnouncementsConfig extends ModuleConfig { + + private int intervalSeconds = 300; + private boolean randomize = false; + private String prefixText = "Announcement"; + private String prefixColor = "#FFAA00"; + private String messageColor = "#FFFFFF"; + private List messages = new ArrayList<>(); + + public AnnouncementsConfig(@NotNull Path filePath) { super(filePath); } + + @Override @NotNull public String getModuleName() { return "announcements"; } + @Override protected boolean getDefaultEnabled() { return false; } + + @Override + protected void createDefaults() { + intervalSeconds = 300; + randomize = false; + prefixText = "Announcement"; + prefixColor = "#FFAA00"; + messageColor = "#FFFFFF"; + messages = new ArrayList<>(); + messages.add("Welcome to the server! Type /help for commands."); + messages.add("Join our Discord at discord.gg/example"); + } + + @Override + protected void loadModuleSettings(@NotNull JsonObject root) { + intervalSeconds = getInt(root, "intervalSeconds", 300); + randomize = getBool(root, "randomize", false); + prefixText = getString(root, "prefixText", "Announcement"); + prefixColor = getString(root, "prefixColor", "#FFAA00"); + messageColor = getString(root, "messageColor", "#FFFFFF"); + messages = new ArrayList<>(getStringList(root, "messages")); + if (messages.isEmpty() && !root.has("messages")) { + messages.add("Welcome to the server! Type /help for commands."); + messages.add("Join our Discord at discord.gg/example"); + } + } + + @Override + protected void writeModuleSettings(@NotNull JsonObject root) { + root.addProperty("intervalSeconds", intervalSeconds); + root.addProperty("randomize", randomize); + root.addProperty("prefixText", prefixText); + root.addProperty("prefixColor", prefixColor); + root.addProperty("messageColor", messageColor); + root.add("messages", toJsonArray(messages)); + } + + public int getIntervalSeconds() { return intervalSeconds; } + public boolean isRandomize() { return randomize; } + public String getPrefixText() { return prefixText; } + public String getPrefixColor() { return prefixColor; } + public String getMessageColor() { return messageColor; } + @NotNull public List getMessages() { return messages; } +} diff --git a/src/main/java/com/hyperessentials/config/modules/DebugConfig.java b/src/main/java/com/hyperessentials/config/modules/DebugConfig.java new file mode 100644 index 0000000..dedccd3 --- /dev/null +++ b/src/main/java/com/hyperessentials/config/modules/DebugConfig.java @@ -0,0 +1,147 @@ +package com.hyperessentials.config.modules; + +import com.google.gson.JsonObject; +import com.hyperessentials.config.ModuleConfig; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +/** + * Configuration for the debug logging system. + * Controls debug output by category, with integration into the Logger utility. + */ +public class DebugConfig extends ModuleConfig { + + // Global debug settings + private boolean enabledByDefault = false; + private boolean logToConsole = true; + + // Per-category settings + private boolean homes = false; + private boolean warps = false; + private boolean spawns = false; + private boolean teleport = false; + private boolean kits = false; + private boolean moderation = false; + private boolean utility = false; + private boolean rtp = false; + private boolean announcements = false; + private boolean integration = false; + private boolean economy = false; + private boolean storage = false; + + public DebugConfig(@NotNull Path filePath) { + super(filePath); + } + + @Override + @NotNull + public String getModuleName() { + return "debug"; + } + + @Override + protected boolean getDefaultEnabled() { + return false; + } + + @Override + protected void createDefaults() { + enabled = false; + enabledByDefault = false; + logToConsole = true; + homes = false; + warps = false; + spawns = false; + teleport = false; + kits = false; + moderation = false; + utility = false; + rtp = false; + announcements = false; + integration = false; + economy = false; + storage = false; + } + + @Override + protected void loadModuleSettings(@NotNull JsonObject root) { + enabledByDefault = getBool(root, "enabledByDefault", enabledByDefault); + logToConsole = getBool(root, "logToConsole", logToConsole); + + if (hasSection(root, "categories")) { + JsonObject categories = root.getAsJsonObject("categories"); + homes = getBool(categories, "homes", false); + warps = getBool(categories, "warps", false); + spawns = getBool(categories, "spawns", false); + teleport = getBool(categories, "teleport", false); + kits = getBool(categories, "kits", false); + moderation = getBool(categories, "moderation", false); + utility = getBool(categories, "utility", false); + rtp = getBool(categories, "rtp", false); + announcements = getBool(categories, "announcements", false); + integration = getBool(categories, "integration", false); + economy = getBool(categories, "economy", false); + storage = getBool(categories, "storage", false); + } + + applyToLogger(); + } + + @Override + protected void writeModuleSettings(@NotNull JsonObject root) { + root.addProperty("enabledByDefault", enabledByDefault); + root.addProperty("logToConsole", logToConsole); + + JsonObject categories = new JsonObject(); + categories.addProperty("homes", homes); + categories.addProperty("warps", warps); + categories.addProperty("spawns", spawns); + categories.addProperty("teleport", teleport); + categories.addProperty("kits", kits); + categories.addProperty("moderation", moderation); + categories.addProperty("utility", utility); + categories.addProperty("rtp", rtp); + categories.addProperty("announcements", announcements); + categories.addProperty("integration", integration); + categories.addProperty("economy", economy); + categories.addProperty("storage", storage); + root.add("categories", categories); + } + + /** + * Applies the debug settings to the Logger utility. + */ + public void applyToLogger() { + Logger.setLogToConsole(logToConsole); + Logger.setDebugEnabled(Logger.DebugCategory.HOMES, homes); + Logger.setDebugEnabled(Logger.DebugCategory.WARPS, warps); + Logger.setDebugEnabled(Logger.DebugCategory.SPAWNS, spawns); + Logger.setDebugEnabled(Logger.DebugCategory.TELEPORT, teleport); + Logger.setDebugEnabled(Logger.DebugCategory.KITS, kits); + Logger.setDebugEnabled(Logger.DebugCategory.MODERATION, moderation); + Logger.setDebugEnabled(Logger.DebugCategory.UTILITY, utility); + Logger.setDebugEnabled(Logger.DebugCategory.RTP, rtp); + Logger.setDebugEnabled(Logger.DebugCategory.ANNOUNCEMENTS, announcements); + Logger.setDebugEnabled(Logger.DebugCategory.INTEGRATION, integration); + Logger.setDebugEnabled(Logger.DebugCategory.ECONOMY, economy); + Logger.setDebugEnabled(Logger.DebugCategory.STORAGE, storage); + } + + // Getters + public boolean isEnabledByDefault() { return enabledByDefault; } + public boolean isLogToConsole() { return logToConsole; } + public boolean isHomes() { return homes; } + public boolean isWarps() { return warps; } + public boolean isSpawns() { return spawns; } + public boolean isTeleport() { return teleport; } + public boolean isKits() { return kits; } + public boolean isModeration() { return moderation; } + public boolean isUtility() { return utility; } + public boolean isRtp() { return rtp; } + public boolean isAnnouncements() { return announcements; } + public boolean isIntegration() { return integration; } + public boolean isEconomy() { return economy; } + public boolean isStorage() { return storage; } +} diff --git a/src/main/java/com/hyperessentials/config/modules/HomesConfig.java b/src/main/java/com/hyperessentials/config/modules/HomesConfig.java index 88f92d6..4b14027 100644 --- a/src/main/java/com/hyperessentials/config/modules/HomesConfig.java +++ b/src/main/java/com/hyperessentials/config/modules/HomesConfig.java @@ -1,51 +1,87 @@ -package com.hyperessentials.config.modules; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; - -import java.nio.file.Path; - -import com.hyperessentials.config.ModuleConfig; - -public class HomesConfig extends ModuleConfig { - - private int defaultHomeLimit = 3; - private boolean restrictInEnemyTerritory = false; - private boolean bedSyncEnabled = true; - private String bedHomeName = "bed"; - private boolean shareEnabled = true; - private int maxSharesPerHome = 10; - - public HomesConfig(@NotNull Path filePath) { super(filePath); } - - @Override @NotNull public String getModuleName() { return "homes"; } - - @Override protected void createDefaults() {} - - @Override - protected void loadModuleSettings(@NotNull JsonObject root) { - defaultHomeLimit = getInt(root, "defaultHomeLimit", defaultHomeLimit); - restrictInEnemyTerritory = getBool(root, "restrictInEnemyTerritory", restrictInEnemyTerritory); - bedSyncEnabled = getBool(root, "bedSyncEnabled", bedSyncEnabled); - bedHomeName = getString(root, "bedHomeName", bedHomeName); - shareEnabled = getBool(root, "shareEnabled", shareEnabled); - maxSharesPerHome = getInt(root, "maxSharesPerHome", maxSharesPerHome); - } - - @Override - protected void writeModuleSettings(@NotNull JsonObject root) { - root.addProperty("defaultHomeLimit", defaultHomeLimit); - root.addProperty("restrictInEnemyTerritory", restrictInEnemyTerritory); - root.addProperty("bedSyncEnabled", bedSyncEnabled); - root.addProperty("bedHomeName", bedHomeName); - root.addProperty("shareEnabled", shareEnabled); - root.addProperty("maxSharesPerHome", maxSharesPerHome); - } - - public int getDefaultHomeLimit() { return defaultHomeLimit; } - public boolean isRestrictInEnemyTerritory() { return restrictInEnemyTerritory; } - public boolean isBedSyncEnabled() { return bedSyncEnabled; } - public String getBedHomeName() { return bedHomeName; } - public boolean isShareEnabled() { return shareEnabled; } - public int getMaxSharesPerHome() { return maxSharesPerHome; } -} +package com.hyperessentials.config.modules; + +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +import com.hyperessentials.config.ModuleConfig; + +public class HomesConfig extends ModuleConfig { + + private int defaultHomeLimit = 3; + + // Factions integration + private boolean factionsEnabled = true; + private boolean allowInOwnTerritory = true; + private boolean allowInAllyTerritory = true; + private boolean allowInNeutralTerritory = true; + private boolean allowInEnemyTerritory = false; + private boolean allowInWilderness = true; + + // Bed sync + private boolean bedSyncEnabled = true; + private String bedHomeName = "bed"; + + // Sharing (deferred - config only) + private boolean shareEnabled = true; + private int maxSharesPerHome = 10; + + public HomesConfig(@NotNull Path filePath) { super(filePath); } + + @Override @NotNull public String getModuleName() { return "homes"; } + + @Override protected void createDefaults() {} + + @Override + protected void loadModuleSettings(@NotNull JsonObject root) { + defaultHomeLimit = getInt(root, "defaultHomeLimit", defaultHomeLimit); + + if (hasSection(root, "factions")) { + JsonObject factions = root.getAsJsonObject("factions"); + factionsEnabled = getBool(factions, "enabled", factionsEnabled); + allowInOwnTerritory = getBool(factions, "allowInOwnTerritory", allowInOwnTerritory); + allowInAllyTerritory = getBool(factions, "allowInAllyTerritory", allowInAllyTerritory); + allowInNeutralTerritory = getBool(factions, "allowInNeutralTerritory", allowInNeutralTerritory); + allowInEnemyTerritory = getBool(factions, "allowInEnemyTerritory", allowInEnemyTerritory); + allowInWilderness = getBool(factions, "allowInWilderness", allowInWilderness); + } + + bedSyncEnabled = getBool(root, "bedSyncEnabled", bedSyncEnabled); + bedHomeName = getString(root, "bedHomeName", bedHomeName); + shareEnabled = getBool(root, "shareEnabled", shareEnabled); + maxSharesPerHome = getInt(root, "maxSharesPerHome", maxSharesPerHome); + } + + @Override + protected void writeModuleSettings(@NotNull JsonObject root) { + root.addProperty("defaultHomeLimit", defaultHomeLimit); + + JsonObject factions = new JsonObject(); + factions.addProperty("enabled", factionsEnabled); + factions.addProperty("allowInOwnTerritory", allowInOwnTerritory); + factions.addProperty("allowInAllyTerritory", allowInAllyTerritory); + factions.addProperty("allowInNeutralTerritory", allowInNeutralTerritory); + factions.addProperty("allowInEnemyTerritory", allowInEnemyTerritory); + factions.addProperty("allowInWilderness", allowInWilderness); + root.add("factions", factions); + + root.addProperty("bedSyncEnabled", bedSyncEnabled); + root.addProperty("bedHomeName", bedHomeName); + root.addProperty("shareEnabled", shareEnabled); + root.addProperty("maxSharesPerHome", maxSharesPerHome); + } + + // Getters + public int getDefaultHomeLimit() { return defaultHomeLimit; } + public boolean isFactionsEnabled() { return factionsEnabled; } + public boolean isAllowInOwnTerritory() { return allowInOwnTerritory; } + public boolean isAllowInAllyTerritory() { return allowInAllyTerritory; } + public boolean isAllowInNeutralTerritory() { return allowInNeutralTerritory; } + public boolean isAllowInEnemyTerritory() { return allowInEnemyTerritory; } + public boolean isAllowInWilderness() { return allowInWilderness; } + public boolean isBedSyncEnabled() { return bedSyncEnabled; } + public String getBedHomeName() { return bedHomeName; } + public boolean isShareEnabled() { return shareEnabled; } + public int getMaxSharesPerHome() { return maxSharesPerHome; } +} diff --git a/src/main/java/com/hyperessentials/config/modules/KitsConfig.java b/src/main/java/com/hyperessentials/config/modules/KitsConfig.java index 4f1211a..1832b79 100644 --- a/src/main/java/com/hyperessentials/config/modules/KitsConfig.java +++ b/src/main/java/com/hyperessentials/config/modules/KitsConfig.java @@ -1,15 +1,38 @@ -package com.hyperessentials.config.modules; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; -import java.nio.file.Path; -import com.hyperessentials.config.ModuleConfig; - -public class KitsConfig extends ModuleConfig { - public KitsConfig(@NotNull Path filePath) { super(filePath); } - @Override @NotNull public String getModuleName() { return "kits"; } - @Override protected boolean getDefaultEnabled() { return false; } - @Override protected void createDefaults() {} - @Override protected void loadModuleSettings(@NotNull JsonObject root) {} - @Override protected void writeModuleSettings(@NotNull JsonObject root) {} -} +package com.hyperessentials.config.modules; + +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNull; +import java.nio.file.Path; +import com.hyperessentials.config.ModuleConfig; + +public class KitsConfig extends ModuleConfig { + + private int defaultCooldownSeconds = 300; + private boolean oneTimeDefault = false; + + public KitsConfig(@NotNull Path filePath) { super(filePath); } + + @Override @NotNull public String getModuleName() { return "kits"; } + @Override protected boolean getDefaultEnabled() { return false; } + + @Override + protected void createDefaults() { + defaultCooldownSeconds = 300; + oneTimeDefault = false; + } + + @Override + protected void loadModuleSettings(@NotNull JsonObject root) { + defaultCooldownSeconds = getInt(root, "defaultCooldownSeconds", 300); + oneTimeDefault = getBool(root, "oneTimeDefault", false); + } + + @Override + protected void writeModuleSettings(@NotNull JsonObject root) { + root.addProperty("defaultCooldownSeconds", defaultCooldownSeconds); + root.addProperty("oneTimeDefault", oneTimeDefault); + } + + public int getDefaultCooldownSeconds() { return defaultCooldownSeconds; } + public boolean isOneTimeDefault() { return oneTimeDefault; } +} diff --git a/src/main/java/com/hyperessentials/config/modules/ModerationConfig.java b/src/main/java/com/hyperessentials/config/modules/ModerationConfig.java index a998e1a..dcbf466 100644 --- a/src/main/java/com/hyperessentials/config/modules/ModerationConfig.java +++ b/src/main/java/com/hyperessentials/config/modules/ModerationConfig.java @@ -1,15 +1,78 @@ -package com.hyperessentials.config.modules; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; -import java.nio.file.Path; -import com.hyperessentials.config.ModuleConfig; - -public class ModerationConfig extends ModuleConfig { - public ModerationConfig(@NotNull Path filePath) { super(filePath); } - @Override @NotNull public String getModuleName() { return "moderation"; } - @Override protected boolean getDefaultEnabled() { return false; } - @Override protected void createDefaults() {} - @Override protected void loadModuleSettings(@NotNull JsonObject root) {} - @Override protected void writeModuleSettings(@NotNull JsonObject root) {} -} +package com.hyperessentials.config.modules; + +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNull; +import java.nio.file.Path; +import com.hyperessentials.config.ModuleConfig; + +public class ModerationConfig extends ModuleConfig { + + private String defaultBanReason = "You have been banned from this server."; + private String defaultMuteReason = "You have been muted."; + private String defaultKickReason = "You have been kicked from this server."; + private String mutedChatMessage = "You are muted. Your message was not sent."; + private String freezeMessage = "You have been frozen by a moderator."; + private int freezeCheckIntervalMs = 100; + private boolean broadcastBans = true; + private boolean broadcastKicks = true; + private boolean broadcastMutes = false; + private int maxHistoryPerPlayer = 50; + + public ModerationConfig(@NotNull Path filePath) { super(filePath); } + + @Override @NotNull public String getModuleName() { return "moderation"; } + @Override protected boolean getDefaultEnabled() { return false; } + + @Override + protected void createDefaults() { + defaultBanReason = "You have been banned from this server."; + defaultMuteReason = "You have been muted."; + defaultKickReason = "You have been kicked from this server."; + mutedChatMessage = "You are muted. Your message was not sent."; + freezeMessage = "You have been frozen by a moderator."; + freezeCheckIntervalMs = 100; + broadcastBans = true; + broadcastKicks = true; + broadcastMutes = false; + maxHistoryPerPlayer = 50; + } + + @Override + protected void loadModuleSettings(@NotNull JsonObject root) { + defaultBanReason = getString(root, "defaultBanReason", "You have been banned from this server."); + defaultMuteReason = getString(root, "defaultMuteReason", "You have been muted."); + defaultKickReason = getString(root, "defaultKickReason", "You have been kicked from this server."); + mutedChatMessage = getString(root, "mutedChatMessage", "You are muted. Your message was not sent."); + freezeMessage = getString(root, "freezeMessage", "You have been frozen by a moderator."); + freezeCheckIntervalMs = getInt(root, "freezeCheckIntervalMs", 100); + broadcastBans = getBool(root, "broadcastBans", true); + broadcastKicks = getBool(root, "broadcastKicks", true); + broadcastMutes = getBool(root, "broadcastMutes", false); + maxHistoryPerPlayer = getInt(root, "maxHistoryPerPlayer", 50); + } + + @Override + protected void writeModuleSettings(@NotNull JsonObject root) { + root.addProperty("defaultBanReason", defaultBanReason); + root.addProperty("defaultMuteReason", defaultMuteReason); + root.addProperty("defaultKickReason", defaultKickReason); + root.addProperty("mutedChatMessage", mutedChatMessage); + root.addProperty("freezeMessage", freezeMessage); + root.addProperty("freezeCheckIntervalMs", freezeCheckIntervalMs); + root.addProperty("broadcastBans", broadcastBans); + root.addProperty("broadcastKicks", broadcastKicks); + root.addProperty("broadcastMutes", broadcastMutes); + root.addProperty("maxHistoryPerPlayer", maxHistoryPerPlayer); + } + + public String getDefaultBanReason() { return defaultBanReason; } + public String getDefaultMuteReason() { return defaultMuteReason; } + public String getDefaultKickReason() { return defaultKickReason; } + public String getMutedChatMessage() { return mutedChatMessage; } + public String getFreezeMessage() { return freezeMessage; } + public int getFreezeCheckIntervalMs() { return freezeCheckIntervalMs; } + public boolean isBroadcastBans() { return broadcastBans; } + public boolean isBroadcastKicks() { return broadcastKicks; } + public boolean isBroadcastMutes() { return broadcastMutes; } + public int getMaxHistoryPerPlayer() { return maxHistoryPerPlayer; } +} diff --git a/src/main/java/com/hyperessentials/config/modules/RtpConfig.java b/src/main/java/com/hyperessentials/config/modules/RtpConfig.java deleted file mode 100644 index 868f6f3..0000000 --- a/src/main/java/com/hyperessentials/config/modules/RtpConfig.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.hyperessentials.config.modules; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; -import java.nio.file.Path; -import com.hyperessentials.config.ModuleConfig; - -public class RtpConfig extends ModuleConfig { - public RtpConfig(@NotNull Path filePath) { super(filePath); } - @Override @NotNull public String getModuleName() { return "rtp"; } - @Override protected boolean getDefaultEnabled() { return false; } - @Override protected void createDefaults() {} - @Override protected void loadModuleSettings(@NotNull JsonObject root) {} - @Override protected void writeModuleSettings(@NotNull JsonObject root) {} -} diff --git a/src/main/java/com/hyperessentials/config/modules/SpawnsConfig.java b/src/main/java/com/hyperessentials/config/modules/SpawnsConfig.java index 9395b3e..f426510 100644 --- a/src/main/java/com/hyperessentials/config/modules/SpawnsConfig.java +++ b/src/main/java/com/hyperessentials/config/modules/SpawnsConfig.java @@ -1,36 +1,36 @@ -package com.hyperessentials.config.modules; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; -import java.nio.file.Path; -import com.hyperessentials.config.ModuleConfig; - -public class SpawnsConfig extends ModuleConfig { - private String defaultSpawnName = "spawn"; - private boolean teleportOnJoin = false; - private boolean teleportOnRespawn = true; - private boolean perWorldSpawns = false; - - public SpawnsConfig(@NotNull Path filePath) { super(filePath); } - @Override @NotNull public String getModuleName() { return "spawns"; } - @Override protected void createDefaults() {} - - @Override protected void loadModuleSettings(@NotNull JsonObject root) { - defaultSpawnName = getString(root, "defaultSpawnName", defaultSpawnName); - teleportOnJoin = getBool(root, "teleportOnJoin", teleportOnJoin); - teleportOnRespawn = getBool(root, "teleportOnRespawn", teleportOnRespawn); - perWorldSpawns = getBool(root, "perWorldSpawns", perWorldSpawns); - } - - @Override protected void writeModuleSettings(@NotNull JsonObject root) { - root.addProperty("defaultSpawnName", defaultSpawnName); - root.addProperty("teleportOnJoin", teleportOnJoin); - root.addProperty("teleportOnRespawn", teleportOnRespawn); - root.addProperty("perWorldSpawns", perWorldSpawns); - } - - public String getDefaultSpawnName() { return defaultSpawnName; } - public boolean isTeleportOnJoin() { return teleportOnJoin; } - public boolean isTeleportOnRespawn() { return teleportOnRespawn; } - public boolean isPerWorldSpawns() { return perWorldSpawns; } -} +package com.hyperessentials.config.modules; + +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNull; +import java.nio.file.Path; +import com.hyperessentials.config.ModuleConfig; + +public class SpawnsConfig extends ModuleConfig { + private String defaultSpawnName = "spawn"; + private boolean teleportOnJoin = false; + private boolean teleportOnRespawn = true; + private boolean perWorldSpawns = false; + + public SpawnsConfig(@NotNull Path filePath) { super(filePath); } + @Override @NotNull public String getModuleName() { return "spawns"; } + @Override protected void createDefaults() {} + + @Override protected void loadModuleSettings(@NotNull JsonObject root) { + defaultSpawnName = getString(root, "defaultSpawnName", defaultSpawnName); + teleportOnJoin = getBool(root, "teleportOnJoin", teleportOnJoin); + teleportOnRespawn = getBool(root, "teleportOnRespawn", teleportOnRespawn); + perWorldSpawns = getBool(root, "perWorldSpawns", perWorldSpawns); + } + + @Override protected void writeModuleSettings(@NotNull JsonObject root) { + root.addProperty("defaultSpawnName", defaultSpawnName); + root.addProperty("teleportOnJoin", teleportOnJoin); + root.addProperty("teleportOnRespawn", teleportOnRespawn); + root.addProperty("perWorldSpawns", perWorldSpawns); + } + + public String getDefaultSpawnName() { return defaultSpawnName; } + public boolean isTeleportOnJoin() { return teleportOnJoin; } + public boolean isTeleportOnRespawn() { return teleportOnRespawn; } + public boolean isPerWorldSpawns() { return perWorldSpawns; } +} diff --git a/src/main/java/com/hyperessentials/config/modules/TeleportConfig.java b/src/main/java/com/hyperessentials/config/modules/TeleportConfig.java index 6f5ee65..e83a54a 100644 --- a/src/main/java/com/hyperessentials/config/modules/TeleportConfig.java +++ b/src/main/java/com/hyperessentials/config/modules/TeleportConfig.java @@ -1,44 +1,171 @@ -package com.hyperessentials.config.modules; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; -import java.nio.file.Path; -import com.hyperessentials.config.ModuleConfig; - -public class TeleportConfig extends ModuleConfig { - private int tpaTimeout = 60; - private int tpaCooldown = 30; - private int maxPendingTpa = 5; - private int backHistorySize = 5; - private boolean saveBackOnDeath = true; - private boolean saveBackOnTeleport = true; - - public TeleportConfig(@NotNull Path filePath) { super(filePath); } - @Override @NotNull public String getModuleName() { return "teleport"; } - @Override protected void createDefaults() {} - - @Override protected void loadModuleSettings(@NotNull JsonObject root) { - tpaTimeout = getInt(root, "tpaTimeout", tpaTimeout); - tpaCooldown = getInt(root, "tpaCooldown", tpaCooldown); - maxPendingTpa = getInt(root, "maxPendingTpa", maxPendingTpa); - backHistorySize = getInt(root, "backHistorySize", backHistorySize); - saveBackOnDeath = getBool(root, "saveBackOnDeath", saveBackOnDeath); - saveBackOnTeleport = getBool(root, "saveBackOnTeleport", saveBackOnTeleport); - } - - @Override protected void writeModuleSettings(@NotNull JsonObject root) { - root.addProperty("tpaTimeout", tpaTimeout); - root.addProperty("tpaCooldown", tpaCooldown); - root.addProperty("maxPendingTpa", maxPendingTpa); - root.addProperty("backHistorySize", backHistorySize); - root.addProperty("saveBackOnDeath", saveBackOnDeath); - root.addProperty("saveBackOnTeleport", saveBackOnTeleport); - } - - public int getTpaTimeout() { return tpaTimeout; } - public int getTpaCooldown() { return tpaCooldown; } - public int getMaxPendingTpa() { return maxPendingTpa; } - public int getBackHistorySize() { return backHistorySize; } - public boolean isSaveBackOnDeath() { return saveBackOnDeath; } - public boolean isSaveBackOnTeleport() { return saveBackOnTeleport; } -} +package com.hyperessentials.config.modules; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.hyperessentials.config.ModuleConfig; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * Configuration for the Teleport module (TPA, back, and RTP). + */ +public class TeleportConfig extends ModuleConfig { + + private int tpaTimeout = 60; + private int tpaCooldown = 30; + private int maxPendingTpa = 5; + private int backHistorySize = 5; + private boolean saveBackOnDeath = true; + private boolean saveBackOnTeleport = true; + + // RTP settings + private int rtpCenterX = 0; + private int rtpCenterZ = 0; + private int rtpMinRadius = 100; + private int rtpMaxRadius = 5000; + private int rtpMaxAttempts = 10; + private boolean rtpPlayerRelative = true; + private List rtpBlacklistedWorlds = new ArrayList<>(); + + // RTP faction avoidance + private boolean rtpFactionAvoidanceEnabled = true; + private int rtpFactionAvoidanceBufferRadius = 2; + + // RTP safety + private boolean rtpSafetyAvoidWater = true; + private boolean rtpSafetyAvoidDangerousFluids = true; + private int rtpSafetyMinY = 5; + private int rtpSafetyMaxY = 300; + private int rtpSafetyAirAboveHead = 10; + + public TeleportConfig(@NotNull Path filePath) { + super(filePath); + } + + @Override + @NotNull + public String getModuleName() { + return "teleport"; + } + + @Override + protected void createDefaults() {} + + @Override + protected void loadModuleSettings(@NotNull JsonObject root) { + tpaTimeout = getInt(root, "tpaTimeout", tpaTimeout); + tpaCooldown = getInt(root, "tpaCooldown", tpaCooldown); + maxPendingTpa = getInt(root, "maxPendingTpa", maxPendingTpa); + backHistorySize = getInt(root, "backHistorySize", backHistorySize); + saveBackOnDeath = getBool(root, "saveBackOnDeath", saveBackOnDeath); + saveBackOnTeleport = getBool(root, "saveBackOnTeleport", saveBackOnTeleport); + + // RTP subsection + if (hasSection(root, "rtp")) { + JsonObject rtp = root.getAsJsonObject("rtp"); + rtpCenterX = getInt(rtp, "centerX", rtpCenterX); + rtpCenterZ = getInt(rtp, "centerZ", rtpCenterZ); + rtpMinRadius = getInt(rtp, "minRadius", rtpMinRadius); + rtpMaxRadius = getInt(rtp, "maxRadius", rtpMaxRadius); + rtpMaxAttempts = getInt(rtp, "maxAttempts", rtpMaxAttempts); + rtpPlayerRelative = getBool(rtp, "playerRelative", rtpPlayerRelative); + + rtpBlacklistedWorlds = new ArrayList<>(); + if (rtp.has("blacklistedWorlds") && rtp.get("blacklistedWorlds").isJsonArray()) { + JsonArray arr = rtp.getAsJsonArray("blacklistedWorlds"); + for (int i = 0; i < arr.size(); i++) { + rtpBlacklistedWorlds.add(arr.get(i).getAsString().toLowerCase()); + } + } + + // Faction avoidance subsection + if (hasSection(rtp, "factionAvoidance")) { + JsonObject fa = rtp.getAsJsonObject("factionAvoidance"); + rtpFactionAvoidanceEnabled = getBool(fa, "enabled", rtpFactionAvoidanceEnabled); + rtpFactionAvoidanceBufferRadius = getInt(fa, "bufferRadius", rtpFactionAvoidanceBufferRadius); + } + + // Safety subsection + if (hasSection(rtp, "safety")) { + JsonObject safety = rtp.getAsJsonObject("safety"); + rtpSafetyAvoidWater = getBool(safety, "avoidWater", rtpSafetyAvoidWater); + rtpSafetyAvoidDangerousFluids = getBool(safety, "avoidDangerousFluids", rtpSafetyAvoidDangerousFluids); + rtpSafetyMinY = getInt(safety, "minY", rtpSafetyMinY); + rtpSafetyMaxY = getInt(safety, "maxY", rtpSafetyMaxY); + rtpSafetyAirAboveHead = getInt(safety, "airAboveHead", rtpSafetyAirAboveHead); + } + } + } + + @Override + protected void writeModuleSettings(@NotNull JsonObject root) { + root.addProperty("tpaTimeout", tpaTimeout); + root.addProperty("tpaCooldown", tpaCooldown); + root.addProperty("maxPendingTpa", maxPendingTpa); + root.addProperty("backHistorySize", backHistorySize); + root.addProperty("saveBackOnDeath", saveBackOnDeath); + root.addProperty("saveBackOnTeleport", saveBackOnTeleport); + + // RTP subsection + JsonObject rtp = new JsonObject(); + rtp.addProperty("centerX", rtpCenterX); + rtp.addProperty("centerZ", rtpCenterZ); + rtp.addProperty("minRadius", rtpMinRadius); + rtp.addProperty("maxRadius", rtpMaxRadius); + rtp.addProperty("maxAttempts", rtpMaxAttempts); + rtp.addProperty("playerRelative", rtpPlayerRelative); + JsonArray arr = new JsonArray(); + for (String world : rtpBlacklistedWorlds) { + arr.add(world); + } + rtp.add("blacklistedWorlds", arr); + + // Faction avoidance + JsonObject fa = new JsonObject(); + fa.addProperty("enabled", rtpFactionAvoidanceEnabled); + fa.addProperty("bufferRadius", rtpFactionAvoidanceBufferRadius); + rtp.add("factionAvoidance", fa); + + // Safety + JsonObject safety = new JsonObject(); + safety.addProperty("avoidWater", rtpSafetyAvoidWater); + safety.addProperty("avoidDangerousFluids", rtpSafetyAvoidDangerousFluids); + safety.addProperty("minY", rtpSafetyMinY); + safety.addProperty("maxY", rtpSafetyMaxY); + safety.addProperty("airAboveHead", rtpSafetyAirAboveHead); + rtp.add("safety", safety); + + root.add("rtp", rtp); + } + + // TPA getters + public int getTpaTimeout() { return tpaTimeout; } + public int getTpaCooldown() { return tpaCooldown; } + public int getMaxPendingTpa() { return maxPendingTpa; } + public int getBackHistorySize() { return backHistorySize; } + public boolean isSaveBackOnDeath() { return saveBackOnDeath; } + public boolean isSaveBackOnTeleport() { return saveBackOnTeleport; } + + // RTP getters + public int getRtpCenterX() { return rtpCenterX; } + public int getRtpCenterZ() { return rtpCenterZ; } + public int getRtpMinRadius() { return rtpMinRadius; } + public int getRtpMaxRadius() { return rtpMaxRadius; } + public int getRtpMaxAttempts() { return rtpMaxAttempts; } + public boolean isRtpPlayerRelative() { return rtpPlayerRelative; } + public List getRtpBlacklistedWorlds() { return rtpBlacklistedWorlds; } + + // RTP faction avoidance getters + public boolean isRtpFactionAvoidanceEnabled() { return rtpFactionAvoidanceEnabled; } + public int getRtpFactionAvoidanceBufferRadius() { return rtpFactionAvoidanceBufferRadius; } + + // RTP safety getters + public boolean isRtpSafetyAvoidWater() { return rtpSafetyAvoidWater; } + public boolean isRtpSafetyAvoidDangerousFluids() { return rtpSafetyAvoidDangerousFluids; } + public int getRtpSafetyMinY() { return rtpSafetyMinY; } + public int getRtpSafetyMaxY() { return rtpSafetyMaxY; } + public int getRtpSafetyAirAboveHead() { return rtpSafetyAirAboveHead; } +} diff --git a/src/main/java/com/hyperessentials/config/modules/UtilityConfig.java b/src/main/java/com/hyperessentials/config/modules/UtilityConfig.java index 53efd3b..ea16fde 100644 --- a/src/main/java/com/hyperessentials/config/modules/UtilityConfig.java +++ b/src/main/java/com/hyperessentials/config/modules/UtilityConfig.java @@ -1,15 +1,221 @@ -package com.hyperessentials.config.modules; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; -import java.nio.file.Path; -import com.hyperessentials.config.ModuleConfig; - -public class UtilityConfig extends ModuleConfig { - public UtilityConfig(@NotNull Path filePath) { super(filePath); } - @Override @NotNull public String getModuleName() { return "utility"; } - @Override protected boolean getDefaultEnabled() { return false; } - @Override protected void createDefaults() {} - @Override protected void loadModuleSettings(@NotNull JsonObject root) {} - @Override protected void writeModuleSettings(@NotNull JsonObject root) {} -} +package com.hyperessentials.config.modules; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNull; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.hyperessentials.config.ModuleConfig; + +public class UtilityConfig extends ModuleConfig { + + private boolean clearChatEnabled = true; + private boolean clearInventoryEnabled = true; + private boolean repairEnabled = true; + private boolean nearEnabled = true; + private boolean healEnabled = true; + private boolean flyEnabled = true; + private boolean godEnabled = true; + private int defaultNearRadius = 200; + private int maxNearRadius = 1000; + private int clearChatLines = 100; + private boolean durabilityEnabled = true; + + // New command toggles + private boolean motdEnabled = true; + private boolean rulesEnabled = true; + private boolean discordEnabled = true; + private boolean listEnabled = true; + private boolean playtimeEnabled = true; + private boolean joindateEnabled = true; + private boolean afkEnabled = true; + private boolean invseeEnabled = true; + private boolean staminaEnabled = true; + private boolean trashEnabled = true; + private boolean maxstackEnabled = true; + private boolean sleepPercentageEnabled = true; + + // MOTD/Rules/Discord content + private List motdLines = List.of("Welcome to the server!"); + private List ruleLines = List.of("1. Be respectful", "2. No griefing", "3. Have fun!"); + private String discordUrl = ""; + + // AFK + private int afkTimeoutSeconds = 300; + + // Sleep percentage + private int sleepPercentage = 50; + private Map worldSleepPercentages = new HashMap<>(); + + public UtilityConfig(@NotNull Path filePath) { super(filePath); } + + @Override @NotNull public String getModuleName() { return "utility"; } + @Override protected boolean getDefaultEnabled() { return false; } + + @Override + protected void createDefaults() { + clearChatEnabled = true; + clearInventoryEnabled = true; + repairEnabled = true; + nearEnabled = true; + healEnabled = true; + flyEnabled = true; + godEnabled = true; + defaultNearRadius = 200; + maxNearRadius = 1000; + clearChatLines = 100; + durabilityEnabled = true; + motdEnabled = true; + rulesEnabled = true; + discordEnabled = true; + listEnabled = true; + playtimeEnabled = true; + joindateEnabled = true; + afkEnabled = true; + invseeEnabled = true; + staminaEnabled = true; + trashEnabled = true; + maxstackEnabled = true; + sleepPercentageEnabled = true; + motdLines = List.of("Welcome to the server!"); + ruleLines = List.of("1. Be respectful", "2. No griefing", "3. Have fun!"); + discordUrl = ""; + afkTimeoutSeconds = 300; + sleepPercentage = 50; + worldSleepPercentages = new HashMap<>(); + } + + @Override + protected void loadModuleSettings(@NotNull JsonObject root) { + clearChatEnabled = getBool(root, "clearChatEnabled", true); + clearInventoryEnabled = getBool(root, "clearInventoryEnabled", true); + repairEnabled = getBool(root, "repairEnabled", true); + nearEnabled = getBool(root, "nearEnabled", true); + healEnabled = getBool(root, "healEnabled", true); + flyEnabled = getBool(root, "flyEnabled", true); + godEnabled = getBool(root, "godEnabled", true); + defaultNearRadius = getInt(root, "defaultNearRadius", 200); + maxNearRadius = getInt(root, "maxNearRadius", 1000); + clearChatLines = getInt(root, "clearChatLines", 100); + durabilityEnabled = getBool(root, "durabilityEnabled", true); + + motdEnabled = getBool(root, "motdEnabled", true); + rulesEnabled = getBool(root, "rulesEnabled", true); + discordEnabled = getBool(root, "discordEnabled", true); + listEnabled = getBool(root, "listEnabled", true); + playtimeEnabled = getBool(root, "playtimeEnabled", true); + joindateEnabled = getBool(root, "joindateEnabled", true); + afkEnabled = getBool(root, "afkEnabled", true); + invseeEnabled = getBool(root, "invseeEnabled", true); + staminaEnabled = getBool(root, "staminaEnabled", true); + trashEnabled = getBool(root, "trashEnabled", true); + maxstackEnabled = getBool(root, "maxstackEnabled", true); + sleepPercentageEnabled = getBool(root, "sleepPercentageEnabled", true); + + discordUrl = getString(root, "discordUrl", ""); + afkTimeoutSeconds = getInt(root, "afkTimeoutSeconds", 300); + sleepPercentage = getInt(root, "sleepPercentage", 50); + + // Load motdLines + if (root.has("motdLines") && root.get("motdLines").isJsonArray()) { + motdLines = new ArrayList<>(); + root.getAsJsonArray("motdLines").forEach(e -> motdLines.add(e.getAsString())); + } + + // Load ruleLines + if (root.has("ruleLines") && root.get("ruleLines").isJsonArray()) { + ruleLines = new ArrayList<>(); + root.getAsJsonArray("ruleLines").forEach(e -> ruleLines.add(e.getAsString())); + } + + // Load world sleep percentages + if (root.has("worldSleepPercentages") && root.get("worldSleepPercentages").isJsonObject()) { + worldSleepPercentages = new HashMap<>(); + JsonObject wsp = root.getAsJsonObject("worldSleepPercentages"); + for (var entry : wsp.entrySet()) { + worldSleepPercentages.put(entry.getKey(), entry.getValue().getAsInt()); + } + } + } + + @Override + protected void writeModuleSettings(@NotNull JsonObject root) { + root.addProperty("clearChatEnabled", clearChatEnabled); + root.addProperty("clearInventoryEnabled", clearInventoryEnabled); + root.addProperty("repairEnabled", repairEnabled); + root.addProperty("nearEnabled", nearEnabled); + root.addProperty("healEnabled", healEnabled); + root.addProperty("flyEnabled", flyEnabled); + root.addProperty("godEnabled", godEnabled); + root.addProperty("defaultNearRadius", defaultNearRadius); + root.addProperty("maxNearRadius", maxNearRadius); + root.addProperty("clearChatLines", clearChatLines); + root.addProperty("durabilityEnabled", durabilityEnabled); + + root.addProperty("motdEnabled", motdEnabled); + root.addProperty("rulesEnabled", rulesEnabled); + root.addProperty("discordEnabled", discordEnabled); + root.addProperty("listEnabled", listEnabled); + root.addProperty("playtimeEnabled", playtimeEnabled); + root.addProperty("joindateEnabled", joindateEnabled); + root.addProperty("afkEnabled", afkEnabled); + root.addProperty("invseeEnabled", invseeEnabled); + root.addProperty("staminaEnabled", staminaEnabled); + root.addProperty("trashEnabled", trashEnabled); + root.addProperty("maxstackEnabled", maxstackEnabled); + root.addProperty("sleepPercentageEnabled", sleepPercentageEnabled); + + root.addProperty("discordUrl", discordUrl); + root.addProperty("afkTimeoutSeconds", afkTimeoutSeconds); + root.addProperty("sleepPercentage", sleepPercentage); + + JsonArray motdArr = new JsonArray(); + motdLines.forEach(motdArr::add); + root.add("motdLines", motdArr); + + JsonArray rulesArr = new JsonArray(); + ruleLines.forEach(rulesArr::add); + root.add("ruleLines", rulesArr); + + JsonObject wsp = new JsonObject(); + worldSleepPercentages.forEach(wsp::addProperty); + root.add("worldSleepPercentages", wsp); + } + + // Existing getters + public boolean isClearChatEnabled() { return clearChatEnabled; } + public boolean isClearInventoryEnabled() { return clearInventoryEnabled; } + public boolean isRepairEnabled() { return repairEnabled; } + public boolean isNearEnabled() { return nearEnabled; } + public boolean isHealEnabled() { return healEnabled; } + public boolean isFlyEnabled() { return flyEnabled; } + public boolean isGodEnabled() { return godEnabled; } + public int getDefaultNearRadius() { return defaultNearRadius; } + public int getMaxNearRadius() { return maxNearRadius; } + public int getClearChatLines() { return clearChatLines; } + public boolean isDurabilityEnabled() { return durabilityEnabled; } + + // New getters + public boolean isMotdEnabled() { return motdEnabled; } + public boolean isRulesEnabled() { return rulesEnabled; } + public boolean isDiscordEnabled() { return discordEnabled; } + public boolean isListEnabled() { return listEnabled; } + public boolean isPlaytimeEnabled() { return playtimeEnabled; } + public boolean isJoindateEnabled() { return joindateEnabled; } + public boolean isAfkEnabled() { return afkEnabled; } + public boolean isInvseeEnabled() { return invseeEnabled; } + public boolean isStaminaEnabled() { return staminaEnabled; } + public boolean isTrashEnabled() { return trashEnabled; } + public boolean isMaxstackEnabled() { return maxstackEnabled; } + public boolean isSleepPercentageEnabled() { return sleepPercentageEnabled; } + public List getMotdLines() { return motdLines; } + public List getRuleLines() { return ruleLines; } + public String getDiscordUrl() { return discordUrl; } + public int getAfkTimeoutSeconds() { return afkTimeoutSeconds; } + public int getSleepPercentage() { return sleepPercentage; } + public void setSleepPercentage(int sleepPercentage) { this.sleepPercentage = sleepPercentage; } + public Map getWorldSleepPercentages() { return worldSleepPercentages; } +} diff --git a/src/main/java/com/hyperessentials/config/modules/VanishConfig.java b/src/main/java/com/hyperessentials/config/modules/VanishConfig.java index 085a17f..a70244b 100644 --- a/src/main/java/com/hyperessentials/config/modules/VanishConfig.java +++ b/src/main/java/com/hyperessentials/config/modules/VanishConfig.java @@ -1,15 +1,53 @@ -package com.hyperessentials.config.modules; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; -import java.nio.file.Path; -import com.hyperessentials.config.ModuleConfig; - -public class VanishConfig extends ModuleConfig { - public VanishConfig(@NotNull Path filePath) { super(filePath); } - @Override @NotNull public String getModuleName() { return "vanish"; } - @Override protected boolean getDefaultEnabled() { return false; } - @Override protected void createDefaults() {} - @Override protected void loadModuleSettings(@NotNull JsonObject root) {} - @Override protected void writeModuleSettings(@NotNull JsonObject root) {} -} +package com.hyperessentials.config.modules; + +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNull; +import java.nio.file.Path; +import com.hyperessentials.config.ModuleConfig; + +public class VanishConfig extends ModuleConfig { + + private boolean fakeLeaveMessage = true; + private boolean fakeJoinMessage = true; + private String vanishEnableMessage = "You are now vanished."; + private String vanishDisableMessage = "You are no longer vanished."; + private boolean silentJoin = false; + + public VanishConfig(@NotNull Path filePath) { super(filePath); } + + @Override @NotNull public String getModuleName() { return "vanish"; } + @Override protected boolean getDefaultEnabled() { return true; } + + @Override + protected void createDefaults() { + fakeLeaveMessage = true; + fakeJoinMessage = true; + vanishEnableMessage = "You are now vanished."; + vanishDisableMessage = "You are no longer vanished."; + silentJoin = false; + } + + @Override + protected void loadModuleSettings(@NotNull JsonObject root) { + fakeLeaveMessage = getBool(root, "fakeLeaveMessage", true); + fakeJoinMessage = getBool(root, "fakeJoinMessage", true); + vanishEnableMessage = getString(root, "vanishEnableMessage", "You are now vanished."); + vanishDisableMessage = getString(root, "vanishDisableMessage", "You are no longer vanished."); + silentJoin = getBool(root, "silentJoin", false); + } + + @Override + protected void writeModuleSettings(@NotNull JsonObject root) { + root.addProperty("fakeLeaveMessage", fakeLeaveMessage); + root.addProperty("fakeJoinMessage", fakeJoinMessage); + root.addProperty("vanishEnableMessage", vanishEnableMessage); + root.addProperty("vanishDisableMessage", vanishDisableMessage); + root.addProperty("silentJoin", silentJoin); + } + + public boolean isFakeLeaveMessage() { return fakeLeaveMessage; } + public boolean isFakeJoinMessage() { return fakeJoinMessage; } + public String getVanishEnableMessage() { return vanishEnableMessage; } + public String getVanishDisableMessage() { return vanishDisableMessage; } + public boolean isSilentJoin() { return silentJoin; } +} diff --git a/src/main/java/com/hyperessentials/config/modules/WarmupConfig.java b/src/main/java/com/hyperessentials/config/modules/WarmupConfig.java index 5d3da34..0fef939 100644 --- a/src/main/java/com/hyperessentials/config/modules/WarmupConfig.java +++ b/src/main/java/com/hyperessentials/config/modules/WarmupConfig.java @@ -1,81 +1,81 @@ -package com.hyperessentials.config.modules; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.Map; -import com.hyperessentials.config.ModuleConfig; - -public class WarmupConfig extends ModuleConfig { - private boolean cancelOnMove = true; - private boolean cancelOnDamage = true; - private boolean safeTeleport = true; - private int safeRadius = 3; - private final Map moduleSettings = new HashMap<>(); - - public record ModuleWarmupSettings(int warmup, int cooldown) {} - - public WarmupConfig(@NotNull Path filePath) { super(filePath); } - @Override @NotNull public String getModuleName() { return "warmup"; } - @Override protected void createDefaults() { - moduleSettings.put("homes", new ModuleWarmupSettings(3, 5)); - moduleSettings.put("warps", new ModuleWarmupSettings(3, 5)); - moduleSettings.put("spawns", new ModuleWarmupSettings(3, 5)); - moduleSettings.put("teleport", new ModuleWarmupSettings(3, 5)); - moduleSettings.put("rtp", new ModuleWarmupSettings(5, 30)); - } - - @Override protected void loadModuleSettings(@NotNull JsonObject root) { - cancelOnMove = getBool(root, "cancelOnMove", cancelOnMove); - cancelOnDamage = getBool(root, "cancelOnDamage", cancelOnDamage); - safeTeleport = getBool(root, "safeTeleport", safeTeleport); - safeRadius = getInt(root, "safeRadius", safeRadius); - - moduleSettings.clear(); - if (hasSection(root, "modules")) { - JsonObject modules = getSection(root, "modules"); - for (String key : modules.keySet()) { - if (modules.get(key).isJsonObject()) { - JsonObject mod = modules.getAsJsonObject(key); - int warmup = mod.has("warmup") ? mod.get("warmup").getAsInt() : 3; - int cooldown = mod.has("cooldown") ? mod.get("cooldown").getAsInt() : 5; - moduleSettings.put(key, new ModuleWarmupSettings(warmup, cooldown)); - } - } - } - if (moduleSettings.isEmpty()) { - createDefaults(); - needsSave = true; - } - } - - @Override protected void writeModuleSettings(@NotNull JsonObject root) { - root.addProperty("cancelOnMove", cancelOnMove); - root.addProperty("cancelOnDamage", cancelOnDamage); - root.addProperty("safeTeleport", safeTeleport); - root.addProperty("safeRadius", safeRadius); - - JsonObject modules = new JsonObject(); - for (Map.Entry entry : moduleSettings.entrySet()) { - JsonObject mod = new JsonObject(); - mod.addProperty("warmup", entry.getValue().warmup()); - mod.addProperty("cooldown", entry.getValue().cooldown()); - modules.add(entry.getKey(), mod); - } - root.add("modules", modules); - } - - public boolean isCancelOnMove() { return cancelOnMove; } - public boolean isCancelOnDamage() { return cancelOnDamage; } - public boolean isSafeTeleport() { return safeTeleport; } - public int getSafeRadius() { return safeRadius; } - public int getWarmup(String moduleName) { - ModuleWarmupSettings s = moduleSettings.get(moduleName); - return s != null ? s.warmup() : 3; - } - public int getCooldown(String moduleName) { - ModuleWarmupSettings s = moduleSettings.get(moduleName); - return s != null ? s.cooldown() : 5; - } -} +package com.hyperessentials.config.modules; + +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNull; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import com.hyperessentials.config.ModuleConfig; + +public class WarmupConfig extends ModuleConfig { + private boolean cancelOnMove = true; + private boolean cancelOnDamage = true; + private boolean safeTeleport = true; + private int safeRadius = 3; + private final Map moduleSettings = new HashMap<>(); + + public record ModuleWarmupSettings(int warmup, int cooldown) {} + + public WarmupConfig(@NotNull Path filePath) { super(filePath); } + @Override @NotNull public String getModuleName() { return "warmup"; } + @Override protected void createDefaults() { + moduleSettings.put("homes", new ModuleWarmupSettings(3, 5)); + moduleSettings.put("warps", new ModuleWarmupSettings(3, 5)); + moduleSettings.put("spawns", new ModuleWarmupSettings(3, 5)); + moduleSettings.put("teleport", new ModuleWarmupSettings(3, 5)); + moduleSettings.put("rtp", new ModuleWarmupSettings(5, 30)); + } + + @Override protected void loadModuleSettings(@NotNull JsonObject root) { + cancelOnMove = getBool(root, "cancelOnMove", cancelOnMove); + cancelOnDamage = getBool(root, "cancelOnDamage", cancelOnDamage); + safeTeleport = getBool(root, "safeTeleport", safeTeleport); + safeRadius = getInt(root, "safeRadius", safeRadius); + + moduleSettings.clear(); + if (hasSection(root, "modules")) { + JsonObject modules = getSection(root, "modules"); + for (String key : modules.keySet()) { + if (modules.get(key).isJsonObject()) { + JsonObject mod = modules.getAsJsonObject(key); + int warmup = mod.has("warmup") ? mod.get("warmup").getAsInt() : 3; + int cooldown = mod.has("cooldown") ? mod.get("cooldown").getAsInt() : 5; + moduleSettings.put(key, new ModuleWarmupSettings(warmup, cooldown)); + } + } + } + if (moduleSettings.isEmpty()) { + createDefaults(); + needsSave = true; + } + } + + @Override protected void writeModuleSettings(@NotNull JsonObject root) { + root.addProperty("cancelOnMove", cancelOnMove); + root.addProperty("cancelOnDamage", cancelOnDamage); + root.addProperty("safeTeleport", safeTeleport); + root.addProperty("safeRadius", safeRadius); + + JsonObject modules = new JsonObject(); + for (Map.Entry entry : moduleSettings.entrySet()) { + JsonObject mod = new JsonObject(); + mod.addProperty("warmup", entry.getValue().warmup()); + mod.addProperty("cooldown", entry.getValue().cooldown()); + modules.add(entry.getKey(), mod); + } + root.add("modules", modules); + } + + public boolean isCancelOnMove() { return cancelOnMove; } + public boolean isCancelOnDamage() { return cancelOnDamage; } + public boolean isSafeTeleport() { return safeTeleport; } + public int getSafeRadius() { return safeRadius; } + public int getWarmup(String moduleName) { + ModuleWarmupSettings s = moduleSettings.get(moduleName); + return s != null ? s.warmup() : 3; + } + public int getCooldown(String moduleName) { + ModuleWarmupSettings s = moduleSettings.get(moduleName); + return s != null ? s.cooldown() : 5; + } +} diff --git a/src/main/java/com/hyperessentials/config/modules/WarpsConfig.java b/src/main/java/com/hyperessentials/config/modules/WarpsConfig.java index 44dfbdc..a15a60a 100644 --- a/src/main/java/com/hyperessentials/config/modules/WarpsConfig.java +++ b/src/main/java/com/hyperessentials/config/modules/WarpsConfig.java @@ -1,24 +1,24 @@ -package com.hyperessentials.config.modules; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; -import java.nio.file.Path; -import com.hyperessentials.config.ModuleConfig; - -public class WarpsConfig extends ModuleConfig { - private String defaultCategory = "general"; - - public WarpsConfig(@NotNull Path filePath) { super(filePath); } - @Override @NotNull public String getModuleName() { return "warps"; } - @Override protected void createDefaults() {} - - @Override protected void loadModuleSettings(@NotNull JsonObject root) { - defaultCategory = getString(root, "defaultCategory", defaultCategory); - } - - @Override protected void writeModuleSettings(@NotNull JsonObject root) { - root.addProperty("defaultCategory", defaultCategory); - } - - public String getDefaultCategory() { return defaultCategory; } -} +package com.hyperessentials.config.modules; + +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNull; +import java.nio.file.Path; +import com.hyperessentials.config.ModuleConfig; + +public class WarpsConfig extends ModuleConfig { + private String defaultCategory = "general"; + + public WarpsConfig(@NotNull Path filePath) { super(filePath); } + @Override @NotNull public String getModuleName() { return "warps"; } + @Override protected void createDefaults() {} + + @Override protected void loadModuleSettings(@NotNull JsonObject root) { + defaultCategory = getString(root, "defaultCategory", defaultCategory); + } + + @Override protected void writeModuleSettings(@NotNull JsonObject root) { + root.addProperty("defaultCategory", defaultCategory); + } + + public String getDefaultCategory() { return defaultCategory; } +} diff --git a/src/main/java/com/hyperessentials/data/Home.java b/src/main/java/com/hyperessentials/data/Home.java new file mode 100644 index 0000000..b3c45b6 --- /dev/null +++ b/src/main/java/com/hyperessentials/data/Home.java @@ -0,0 +1,53 @@ +package com.hyperessentials.data; + +import org.jetbrains.annotations.NotNull; + +/** + * Represents a player home location. + * + * @param name the display name of the home (original casing) + * @param world the world name + * @param x x coordinate + * @param y y coordinate + * @param z z coordinate + * @param yaw yaw rotation + * @param pitch pitch rotation + * @param createdAt when the home was created (epoch milliseconds) + * @param lastUsed when the home was last teleported to (epoch milliseconds) + */ +public record Home( + @NotNull String name, + @NotNull String world, + double x, + double y, + double z, + float yaw, + float pitch, + long createdAt, + long lastUsed +) { + /** + * Creates a new home with current timestamps. + */ + public static Home create(@NotNull String name, @NotNull String world, + double x, double y, double z, + float yaw, float pitch) { + long now = System.currentTimeMillis(); + return new Home(name, world, x, y, z, yaw, pitch, now, now); + } + + /** + * Returns a copy with updated lastUsed timestamp. + */ + public Home withLastUsed(long timestamp) { + return new Home(name, world, x, y, z, yaw, pitch, createdAt, timestamp); + } + + /** + * Returns a copy with updated location (for overwriting a home in place). + */ + public Home withLocation(@NotNull String newWorld, double newX, double newY, double newZ, + float newYaw, float newPitch) { + return new Home(name, newWorld, newX, newY, newZ, newYaw, newPitch, createdAt, lastUsed); + } +} diff --git a/src/main/java/com/hyperessentials/data/Location.java b/src/main/java/com/hyperessentials/data/Location.java index f0bdbf9..ff267a3 100644 --- a/src/main/java/com/hyperessentials/data/Location.java +++ b/src/main/java/com/hyperessentials/data/Location.java @@ -1,22 +1,44 @@ -package com.hyperessentials.data; - -import org.jetbrains.annotations.NotNull; - -/** - * A simple location record for teleportation and storage. - * - * @param world the world name - * @param x x coordinate - * @param y y coordinate - * @param z z coordinate - * @param yaw yaw rotation - * @param pitch pitch rotation - */ -public record Location( - @NotNull String world, - double x, - double y, - double z, - float yaw, - float pitch -) {} +package com.hyperessentials.data; + +import org.jetbrains.annotations.NotNull; + +/** + * A simple location record for teleportation and storage. + * + * @param world the world name + * @param x x coordinate + * @param y y coordinate + * @param z z coordinate + * @param yaw yaw rotation + * @param pitch pitch rotation + */ +public record Location( + @NotNull String world, + double x, + double y, + double z, + float yaw, + float pitch +) { + public static Location fromWarp(@NotNull Warp warp) { + return new Location(warp.world(), warp.x(), warp.y(), warp.z(), warp.yaw(), warp.pitch()); + } + + public static Location fromSpawn(@NotNull Spawn spawn) { + return new Location(spawn.world(), spawn.x(), spawn.y(), spawn.z(), spawn.yaw(), spawn.pitch()); + } + + public static Location fromHome(@NotNull Home home) { + return new Location(home.world(), home.x(), home.y(), home.z(), home.yaw(), home.pitch()); + } + + public double distanceSquared(@NotNull Location other) { + if (!world.equals(other.world)) { + return Double.MAX_VALUE; + } + double dx = x - other.x; + double dy = y - other.y; + double dz = z - other.z; + return dx * dx + dy * dy + dz * dz; + } +} diff --git a/src/main/java/com/hyperessentials/data/PlayerHomes.java b/src/main/java/com/hyperessentials/data/PlayerHomes.java new file mode 100644 index 0000000..9055ca8 --- /dev/null +++ b/src/main/java/com/hyperessentials/data/PlayerHomes.java @@ -0,0 +1,78 @@ +package com.hyperessentials.data; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Holds all homes for a single player. + * Keys are stored lowercase for case-insensitive lookup; + * the original casing is preserved in {@link Home#name()}. + */ +public class PlayerHomes { + + private final UUID uuid; + private String username; + private final Map homes; + + public PlayerHomes(@NotNull UUID uuid, @NotNull String username) { + this.uuid = uuid; + this.username = username; + this.homes = new LinkedHashMap<>(); + } + + @NotNull public UUID getUuid() { return uuid; } + @NotNull public String getUsername() { return username; } + public void setUsername(@NotNull String username) { this.username = username; } + + /** + * Gets a home by name (case-insensitive). + */ + @Nullable + public Home getHome(@NotNull String name) { + return homes.get(name.toLowerCase()); + } + + /** + * Returns all homes (unmodifiable). + */ + @NotNull + public Collection getHomes() { + return Collections.unmodifiableCollection(homes.values()); + } + + /** + * Sets (creates or overwrites) a home. + */ + public void setHome(@NotNull Home home) { + homes.put(home.name().toLowerCase(), home); + } + + /** + * Removes a home by name (case-insensitive). + * + * @return true if the home existed and was removed + */ + public boolean removeHome(@NotNull String name) { + return homes.remove(name.toLowerCase()) != null; + } + + /** + * Checks if a home exists (case-insensitive). + */ + public boolean hasHome(@NotNull String name) { + return homes.containsKey(name.toLowerCase()); + } + + /** + * Returns the number of homes. + */ + public int count() { + return homes.size(); + } +} diff --git a/src/main/java/com/hyperessentials/data/PlayerStats.java b/src/main/java/com/hyperessentials/data/PlayerStats.java new file mode 100644 index 0000000..7d0d3d8 --- /dev/null +++ b/src/main/java/com/hyperessentials/data/PlayerStats.java @@ -0,0 +1,17 @@ +package com.hyperessentials.data; + +import org.jetbrains.annotations.NotNull; + +import java.time.Instant; +import java.util.UUID; + +/** + * Per-player statistics: first join date and accumulated playtime. + */ +public record PlayerStats( + @NotNull UUID uuid, + @NotNull String username, + @NotNull Instant firstJoin, + long totalPlaytimeMs, + @NotNull Instant lastJoin +) {} diff --git a/src/main/java/com/hyperessentials/data/PlayerTeleportData.java b/src/main/java/com/hyperessentials/data/PlayerTeleportData.java new file mode 100644 index 0000000..9d18d9d --- /dev/null +++ b/src/main/java/com/hyperessentials/data/PlayerTeleportData.java @@ -0,0 +1,108 @@ +package com.hyperessentials.data; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +/** + * Stores teleportation-related data for a player. + * This includes TPA toggle state, back history, and timestamps. + */ +public class PlayerTeleportData { + + private final UUID uuid; + private String username; + private boolean tpToggle; // true = accepting TPA requests + private List backHistory; + private long lastTpaRequest; + private long lastTeleport; + + public PlayerTeleportData(@NotNull UUID uuid, @NotNull String username) { + this.uuid = uuid; + this.username = username; + this.tpToggle = true; + this.backHistory = new ArrayList<>(); + this.lastTpaRequest = 0; + this.lastTeleport = 0; + } + + @NotNull + public UUID getUuid() { + return uuid; + } + + @NotNull + public String getUsername() { + return username; + } + + public void setUsername(@NotNull String username) { + this.username = username; + } + + public boolean isTpToggle() { + return tpToggle; + } + + public void setTpToggle(boolean tpToggle) { + this.tpToggle = tpToggle; + } + + public boolean toggleTpToggle() { + tpToggle = !tpToggle; + return tpToggle; + } + + public long getLastTpaRequest() { + return lastTpaRequest; + } + + public void setLastTpaRequest(long lastTpaRequest) { + this.lastTpaRequest = lastTpaRequest; + } + + public long getLastTeleport() { + return lastTeleport; + } + + public void setLastTeleport(long lastTeleport) { + this.lastTeleport = lastTeleport; + } + + @NotNull + public List getBackHistory() { + return Collections.unmodifiableList(backHistory); + } + + @Nullable + public Location getLastBackLocation() { + return backHistory.isEmpty() ? null : backHistory.get(0); + } + + public void addBackLocation(@NotNull Location location, int maxSize) { + backHistory.add(0, location); + while (backHistory.size() > maxSize) { + backHistory.remove(backHistory.size() - 1); + } + } + + @Nullable + public Location popBackLocation() { + if (backHistory.isEmpty()) { + return null; + } + return backHistory.remove(0); + } + + public void clearBackHistory() { + backHistory.clear(); + } + + public void setBackHistory(@NotNull List history) { + this.backHistory = new ArrayList<>(history); + } +} diff --git a/src/main/java/com/hyperessentials/data/Spawn.java b/src/main/java/com/hyperessentials/data/Spawn.java new file mode 100644 index 0000000..0ad2923 --- /dev/null +++ b/src/main/java/com/hyperessentials/data/Spawn.java @@ -0,0 +1,84 @@ +package com.hyperessentials.data; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a server spawn point that players can teleport to. + * + * @param name the unique name of the spawn (lowercase) + * @param world the world the spawn is in + * @param x the x coordinate + * @param y the y coordinate + * @param z the z coordinate + * @param yaw the yaw rotation + * @param pitch the pitch rotation + * @param permission optional permission required to use this spawn (null = no permission) + * @param groupPermission optional group-based permission for auto-selection (null = available to all) + * @param isDefault whether this is the default spawn + * @param createdAt when the spawn was created (epoch milliseconds) + * @param createdBy UUID string of the player who created the spawn + */ +public record Spawn( + @NotNull String name, + @NotNull String world, + double x, + double y, + double z, + float yaw, + float pitch, + @Nullable String permission, + @Nullable String groupPermission, + boolean isDefault, + long createdAt, + @Nullable String createdBy +) { + public Spawn { + name = name.toLowerCase(); + } + + public static Spawn create(@NotNull String name, @NotNull String world, + double x, double y, double z, float yaw, float pitch, + @Nullable String createdBy) { + return new Spawn( + name.toLowerCase(), + world, + x, y, z, + yaw, pitch, + null, + null, + false, + System.currentTimeMillis(), + createdBy + ); + } + + public Spawn withDefault(boolean isDefault) { + return new Spawn(name, world, x, y, z, yaw, pitch, permission, groupPermission, + isDefault, createdAt, createdBy); + } + + public Spawn withPermission(@Nullable String newPermission) { + return new Spawn(name, world, x, y, z, yaw, pitch, newPermission, groupPermission, + isDefault, createdAt, createdBy); + } + + public Spawn withGroupPermission(@Nullable String newGroupPermission) { + return new Spawn(name, world, x, y, z, yaw, pitch, permission, newGroupPermission, + isDefault, createdAt, createdBy); + } + + public Spawn withLocation(@NotNull String newWorld, double newX, double newY, double newZ, + float newYaw, float newPitch) { + return new Spawn(name, newWorld, newX, newY, newZ, newYaw, newPitch, permission, groupPermission, + isDefault, createdAt, createdBy); + } + + public boolean requiresPermission() { + return permission != null && !permission.isEmpty(); + } + + public boolean isGroupRestricted() { + return groupPermission != null && !groupPermission.isEmpty(); + } +} diff --git a/src/main/java/com/hyperessentials/data/TeleportRequest.java b/src/main/java/com/hyperessentials/data/TeleportRequest.java new file mode 100644 index 0000000..1222803 --- /dev/null +++ b/src/main/java/com/hyperessentials/data/TeleportRequest.java @@ -0,0 +1,53 @@ +package com.hyperessentials.data; + +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Represents a TPA (teleport ask) request between players. + * + * @param requester the UUID of the player who sent the request + * @param target the UUID of the player receiving the request + * @param type the type of request (TPA or TPAHERE) + * @param createdAt when the request was created (epoch milliseconds) + * @param expiresAt when the request expires (epoch milliseconds) + */ +public record TeleportRequest( + @NotNull UUID requester, + @NotNull UUID target, + @NotNull Type type, + long createdAt, + long expiresAt +) { + public enum Type { + /** Requester wants to teleport TO the target. */ + TPA, + /** Requester wants the target to teleport TO them. */ + TPAHERE + } + + public static TeleportRequest create(@NotNull UUID requester, @NotNull UUID target, + @NotNull Type type, int timeoutSecs) { + long now = System.currentTimeMillis(); + return new TeleportRequest(requester, target, type, now, now + (timeoutSecs * 1000L)); + } + + public boolean isExpired() { + return System.currentTimeMillis() > expiresAt; + } + + public long getRemainingTime() { + return Math.max(0, expiresAt - System.currentTimeMillis()); + } + + @NotNull + public UUID getTeleportingPlayer() { + return type == Type.TPA ? requester : target; + } + + @NotNull + public UUID getDestinationPlayer() { + return type == Type.TPA ? target : requester; + } +} diff --git a/src/main/java/com/hyperessentials/data/Warp.java b/src/main/java/com/hyperessentials/data/Warp.java new file mode 100644 index 0000000..c2e366a --- /dev/null +++ b/src/main/java/com/hyperessentials/data/Warp.java @@ -0,0 +1,94 @@ +package com.hyperessentials.data; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a server warp location that players can teleport to. + * + * @param name the unique name of the warp (lowercase) + * @param displayName the display name of the warp (can have formatting) + * @param category the category/group this warp belongs to + * @param world the world the warp is in + * @param x the x coordinate + * @param y the y coordinate + * @param z the z coordinate + * @param yaw the yaw rotation + * @param pitch the pitch rotation + * @param permission optional permission required to use this warp (null = no permission) + * @param description optional description of the warp + * @param createdAt when the warp was created (epoch milliseconds) + * @param createdBy UUID string of the player who created the warp + */ +public record Warp( + @NotNull String name, + @NotNull String displayName, + @NotNull String category, + @NotNull String world, + double x, + double y, + double z, + float yaw, + float pitch, + @Nullable String permission, + @Nullable String description, + long createdAt, + @Nullable String createdBy +) { + public Warp { + name = name.toLowerCase(); + if (displayName == null || displayName.isEmpty()) { + displayName = name; + } + if (category == null || category.isEmpty()) { + category = "general"; + } + } + + public static Warp create(@NotNull String name, @NotNull String world, + double x, double y, double z, float yaw, float pitch, + @Nullable String createdBy) { + return new Warp( + name.toLowerCase(), + name, + "general", + world, + x, y, z, + yaw, pitch, + null, + null, + System.currentTimeMillis(), + createdBy + ); + } + + public Warp withDisplayName(@NotNull String newDisplayName) { + return new Warp(name, newDisplayName, category, world, x, y, z, yaw, pitch, + permission, description, createdAt, createdBy); + } + + public Warp withCategory(@NotNull String newCategory) { + return new Warp(name, displayName, newCategory, world, x, y, z, yaw, pitch, + permission, description, createdAt, createdBy); + } + + public Warp withPermission(@Nullable String newPermission) { + return new Warp(name, displayName, category, world, x, y, z, yaw, pitch, + newPermission, description, createdAt, createdBy); + } + + public Warp withDescription(@Nullable String newDescription) { + return new Warp(name, displayName, category, world, x, y, z, yaw, pitch, + permission, newDescription, createdAt, createdBy); + } + + public Warp withLocation(@NotNull String newWorld, double newX, double newY, double newZ, + float newYaw, float newPitch) { + return new Warp(name, displayName, category, newWorld, newX, newY, newZ, newYaw, newPitch, + permission, description, createdAt, createdBy); + } + + public boolean requiresPermission() { + return permission != null && !permission.isEmpty(); + } +} diff --git a/src/main/java/com/hyperessentials/gui/ActivePageTracker.java b/src/main/java/com/hyperessentials/gui/ActivePageTracker.java index 1b7675a..8446f6f 100644 --- a/src/main/java/com/hyperessentials/gui/ActivePageTracker.java +++ b/src/main/java/com/hyperessentials/gui/ActivePageTracker.java @@ -1,51 +1,51 @@ -package com.hyperessentials.gui; - -import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Tracks which GUI page each player currently has open. - * Thread-safe: uses ConcurrentHashMap. - */ -public class ActivePageTracker { - - public record ActivePageInfo( - @NotNull String pageId, - @NotNull InteractiveCustomUIPage page - ) {} - - private final ConcurrentHashMap activePlayers = new ConcurrentHashMap<>(); - - public void register(@NotNull UUID playerUuid, @NotNull String pageId, - @NotNull InteractiveCustomUIPage page) { - activePlayers.put(playerUuid, new ActivePageInfo(pageId, page)); - } - - public void unregister(@NotNull UUID playerUuid) { - activePlayers.remove(playerUuid); - } - - @Nullable - public ActivePageInfo get(@NotNull UUID playerUuid) { - return activePlayers.get(playerUuid); - } - - @NotNull - public List getPlayersOnPage(@NotNull String pageId) { - List result = new ArrayList<>(); - for (Map.Entry entry : activePlayers.entrySet()) { - if (entry.getValue().pageId().equals(pageId)) { - result.add(entry.getKey()); - } - } - return result; - } - - public void clear() { - activePlayers.clear(); - } -} +package com.hyperessentials.gui; + +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Tracks which GUI page each player currently has open. + * Thread-safe: uses ConcurrentHashMap. + */ +public class ActivePageTracker { + + public record ActivePageInfo( + @NotNull String pageId, + @NotNull InteractiveCustomUIPage page + ) {} + + private final ConcurrentHashMap activePlayers = new ConcurrentHashMap<>(); + + public void register(@NotNull UUID playerUuid, @NotNull String pageId, + @NotNull InteractiveCustomUIPage page) { + activePlayers.put(playerUuid, new ActivePageInfo(pageId, page)); + } + + public void unregister(@NotNull UUID playerUuid) { + activePlayers.remove(playerUuid); + } + + @Nullable + public ActivePageInfo get(@NotNull UUID playerUuid) { + return activePlayers.get(playerUuid); + } + + @NotNull + public List getPlayersOnPage(@NotNull String pageId) { + List result = new ArrayList<>(); + for (Map.Entry entry : activePlayers.entrySet()) { + if (entry.getValue().pageId().equals(pageId)) { + result.add(entry.getKey()); + } + } + return result; + } + + public void clear() { + activePlayers.clear(); + } +} diff --git a/src/main/java/com/hyperessentials/gui/AdminPageOpener.java b/src/main/java/com/hyperessentials/gui/AdminPageOpener.java new file mode 100644 index 0000000..b182210 --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/AdminPageOpener.java @@ -0,0 +1,58 @@ +package com.hyperessentials.gui; + +import com.hyperessentials.Permissions; +import com.hyperessentials.integration.PermissionManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * Utility for opening admin GUI pages by page ID. + */ +public final class AdminPageOpener { + + private AdminPageOpener() {} + + /** + * Opens an admin page by ID. Requires admin GUI permission. + * + * @return true if the page was opened successfully + */ + public static boolean open( + @NotNull String pageId, + @NotNull Player player, + @NotNull Ref ref, + @NotNull Store store, + @NotNull PlayerRef playerRef, + @NotNull GuiManager guiManager + ) { + if (!PermissionManager.get().hasPermission(playerRef.getUuid(), Permissions.ADMIN_GUI)) { + return false; + } + + PageRegistry.Entry entry = guiManager.getAdminRegistry().getEntry(pageId); + if (entry == null) { + return false; + } + + if (entry.permission() != null + && !PermissionManager.get().hasPermission(playerRef.getUuid(), entry.permission())) { + return false; + } + + InteractiveCustomUIPage page = entry.supplier().create( + player, ref, store, playerRef, guiManager + ); + + if (page != null) { + player.getPageManager().openCustomPage(ref, store, page); + return true; + } + + return false; + } +} diff --git a/src/main/java/com/hyperessentials/gui/GuiColors.java b/src/main/java/com/hyperessentials/gui/GuiColors.java new file mode 100644 index 0000000..8f121c9 --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/GuiColors.java @@ -0,0 +1,53 @@ +package com.hyperessentials.gui; + +/** + * Centralized GUI color constants for HyperEssentials. + * Matches HyperFactions visual language with gold brand accent. + */ +public final class GuiColors { + + private GuiColors() {} + + // === Brand === + public static final String BRAND_GOLD = "#FFAA00"; + public static final String BRAND_GOLD_LIGHT = "#FFD700"; + public static final String BRAND_GOLD_DARK = "#CC8800"; + + // === Text === + public static final String TEXT_PRIMARY = "#cccccc"; + public static final String TEXT_HEADING = "#ffffff"; + public static final String TEXT_MUTED = "#888888"; + public static final String TEXT_LABEL = "#7c8b99"; + + // === Status === + public static final String STATUS_ONLINE = "#55FF55"; + public static final String STATUS_OFFLINE = "#888888"; + public static final String STATUS_ACTIVE = "#55FF55"; + public static final String STATUS_INACTIVE = "#FF5555"; + + // === Semantic === + public static final String SUCCESS = "#44cc44"; + public static final String DANGER = "#ff5555"; + public static final String WARNING = "#FFAA00"; + public static final String INFO = "#55FFFF"; + + // === Backgrounds === + public static final String BG_DARK = "#0a1119"; + public static final String BG_PANEL = "#141c26"; + public static final String BG_CARD = "#1a2a3a"; + public static final String BG_NAV = "#16212f"; + + // === Dividers === + public static final String DIVIDER = "#FFAA00"; + public static final String DIVIDER_SUBTLE = "#2a3a4a"; + + // === Module status === + public static String forModuleEnabled(boolean enabled) { + return enabled ? STATUS_ACTIVE : STATUS_INACTIVE; + } + + // === Online status === + public static String forOnlineStatus(boolean online) { + return online ? STATUS_ONLINE : STATUS_OFFLINE; + } +} diff --git a/src/main/java/com/hyperessentials/gui/GuiManager.java b/src/main/java/com/hyperessentials/gui/GuiManager.java index b1c6c1f..acf560e 100644 --- a/src/main/java/com/hyperessentials/gui/GuiManager.java +++ b/src/main/java/com/hyperessentials/gui/GuiManager.java @@ -1,47 +1,82 @@ -package com.hyperessentials.gui; - -import org.jetbrains.annotations.NotNull; - -/** - * Central GUI manager for HyperEssentials. - * Manages page registries for player and admin pages. - */ -public class GuiManager { - - private final PageRegistry playerRegistry = new PageRegistry(); - private final PageRegistry adminRegistry = new PageRegistry(); - private final ActivePageTracker pageTracker = new ActivePageTracker(); - - /** - * Gets the player page registry. - */ - @NotNull - public PageRegistry getPlayerRegistry() { - return playerRegistry; - } - - /** - * Gets the admin page registry. - */ - @NotNull - public PageRegistry getAdminRegistry() { - return adminRegistry; - } - - /** - * Gets the active page tracker. - */ - @NotNull - public ActivePageTracker getPageTracker() { - return pageTracker; - } - - /** - * Clears all registries and trackers. Used during shutdown. - */ - public void shutdown() { - playerRegistry.clear(); - adminRegistry.clear(); - pageTracker.clear(); - } -} +package com.hyperessentials.gui; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * Central GUI manager for HyperEssentials. + * Manages page registries for player and admin pages. + */ +public class GuiManager { + + private final PageRegistry playerRegistry = new PageRegistry(); + private final PageRegistry adminRegistry = new PageRegistry(); + private final ActivePageTracker pageTracker = new ActivePageTracker(); + + /** + * Gets the player page registry. + */ + @NotNull + public PageRegistry getPlayerRegistry() { + return playerRegistry; + } + + /** + * Gets the admin page registry. + */ + @NotNull + public PageRegistry getAdminRegistry() { + return adminRegistry; + } + + /** + * Gets the active page tracker. + */ + @NotNull + public ActivePageTracker getPageTracker() { + return pageTracker; + } + + /** + * Opens a player page by ID. + * + * @return true if the page was opened successfully + */ + public boolean openPlayerPage( + @NotNull String pageId, + @NotNull Player player, + @NotNull Ref ref, + @NotNull Store store, + @NotNull PlayerRef playerRef + ) { + return PlayerPageOpener.open(pageId, player, ref, store, playerRef, this); + } + + /** + * Opens an admin page by ID. + * + * @return true if the page was opened successfully + */ + public boolean openAdminPage( + @NotNull String pageId, + @NotNull Player player, + @NotNull Ref ref, + @NotNull Store store, + @NotNull PlayerRef playerRef + ) { + return AdminPageOpener.open(pageId, player, ref, store, playerRef, this); + } + + /** + * Clears all registries and trackers. Used during shutdown. + */ + public void shutdown() { + playerRegistry.clear(); + adminRegistry.clear(); + pageTracker.clear(); + } +} diff --git a/src/main/java/com/hyperessentials/gui/GuiType.java b/src/main/java/com/hyperessentials/gui/GuiType.java index 4574d15..3456870 100644 --- a/src/main/java/com/hyperessentials/gui/GuiType.java +++ b/src/main/java/com/hyperessentials/gui/GuiType.java @@ -1,11 +1,11 @@ -package com.hyperessentials.gui; - -/** - * Types of GUI pages. - */ -public enum GuiType { - /** Player-facing GUI pages */ - PLAYER, - /** Admin-only GUI pages */ - ADMIN -} +package com.hyperessentials.gui; + +/** + * Types of GUI pages. + */ +public enum GuiType { + /** Player-facing GUI pages */ + PLAYER, + /** Admin-only GUI pages */ + ADMIN +} diff --git a/src/main/java/com/hyperessentials/gui/NavBarHelper.java b/src/main/java/com/hyperessentials/gui/NavBarHelper.java index 64ec1ba..0b61571 100644 --- a/src/main/java/com/hyperessentials/gui/NavBarHelper.java +++ b/src/main/java/com/hyperessentials/gui/NavBarHelper.java @@ -1,107 +1,175 @@ -package com.hyperessentials.gui; - -import com.hyperessentials.integration.PermissionManager; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; -import com.hypixel.hytale.server.core.entity.entities.Player; -import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; -import com.hypixel.hytale.server.core.ui.builder.EventData; -import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; -import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; -import com.hypixel.hytale.server.core.universe.PlayerRef; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -/** - * Helper class for building and handling the shared navigation bar. - */ -public final class NavBarHelper { - - private NavBarHelper() {} - - /** - * Sets up the navigation bar in a page. - */ - public static void setupBar( - @NotNull PlayerRef playerRef, - @NotNull String currentPage, - @NotNull PageRegistry registry, - @NotNull UICommandBuilder cmd, - @NotNull UIEventBuilder events - ) { - List entries = registry.getAccessibleNavBarEntries(playerRef); - - if (entries.isEmpty()) { - return; - } - - cmd.set("#NavBar #NavBarTitle #NavBarTitleLabel.Text", "HyperEssentials"); - cmd.appendInline("#NavBar #NavBarButtons", "Group #NavCards { LayoutMode: Left; }"); - - int index = 0; - for (PageRegistry.Entry entry : entries) { - if (entry.id().equals(currentPage)) { - cmd.append("#NavCards", "HyperEssentials/shared/nav_button_active.ui"); - } else { - cmd.append("#NavCards", "HyperEssentials/shared/nav_button.ui"); - } - - cmd.set("#NavCards[" + index + "] #NavActionButton.Text", entry.displayName()); - - events.addEventBinding( - CustomUIEventBindingType.Activating, - "#NavCards[" + index + "] #NavActionButton", - EventData.of("Button", "Nav").append("NavTarget", entry.id()), - false - ); - - index++; - } - } - - /** - * Handles navigation events from the nav bar. - * - * @param targetId The target page ID from event data - * @param player The player entity - * @param ref Entity reference - * @param store Entity store - * @param playerRef Player reference - * @param guiManager The GUI manager - * @return true if the event was handled - */ - public static boolean handleNavEvent( - @NotNull String targetId, - @NotNull Player player, - @NotNull Ref ref, - @NotNull Store store, - @NotNull PlayerRef playerRef, - @NotNull GuiManager guiManager - ) { - if (targetId.isEmpty()) { - return false; - } - - PageRegistry.Entry entry = guiManager.getPlayerRegistry().getEntry(targetId); - if (entry == null) { - return true; - } - - if (entry.permission() != null && !PermissionManager.get().hasPermission(playerRef.getUuid(), entry.permission())) { - return true; - } - - InteractiveCustomUIPage page = entry.supplier().create( - player, ref, store, playerRef, guiManager - ); - - if (page != null) { - player.getPageManager().openCustomPage(ref, store, page); - } - - return true; - } -} +package com.hyperessentials.gui; + +import com.hyperessentials.integration.PermissionManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.ui.builder.EventData; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * Helper class for building and handling the shared navigation bar. + */ +public final class NavBarHelper { + + private NavBarHelper() {} + + /** + * Sets up the navigation bar in a page. + */ + public static void setupBar( + @NotNull PlayerRef playerRef, + @NotNull String currentPage, + @NotNull PageRegistry registry, + @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events + ) { + List entries = registry.getAccessibleNavBarEntries(playerRef); + + if (entries.isEmpty()) { + return; + } + + cmd.set("#NavBar #NavBarTitle #NavBarTitleLabel.Text", "HyperEssentials"); + cmd.appendInline("#NavBar #NavBarButtons", "Group #NavCards { LayoutMode: Left; }"); + + int index = 0; + for (PageRegistry.Entry entry : entries) { + if (entry.id().equals(currentPage)) { + cmd.append("#NavCards", "HyperEssentials/shared/nav_button_active.ui"); + } else { + cmd.append("#NavCards", "HyperEssentials/shared/nav_button.ui"); + } + + cmd.set("#NavCards[" + index + "] #NavActionButton.Text", entry.displayName()); + + events.addEventBinding( + CustomUIEventBindingType.Activating, + "#NavCards[" + index + "] #NavActionButton", + EventData.of("Button", "Nav").append("NavTarget", entry.id()), + false + ); + + index++; + } + } + + /** + * Sets up the admin navigation bar in a page. + */ + public static void setupAdminBar( + @NotNull PlayerRef playerRef, + @NotNull String currentPage, + @NotNull PageRegistry registry, + @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events + ) { + List entries = registry.getAccessibleNavBarEntries(playerRef); + + if (entries.isEmpty()) { + return; + } + + cmd.set("#NavBar #NavBarTitle #NavBarTitleLabel.Text", "HE Admin"); + cmd.appendInline("#NavBar #NavBarButtons", "Group #NavCards { LayoutMode: Left; }"); + + int index = 0; + for (PageRegistry.Entry entry : entries) { + if (entry.id().equals(currentPage)) { + cmd.append("#NavCards", "HyperEssentials/shared/nav_button_active.ui"); + } else { + cmd.append("#NavCards", "HyperEssentials/shared/nav_button.ui"); + } + + cmd.set("#NavCards[" + index + "] #NavActionButton.Text", entry.displayName()); + + events.addEventBinding( + CustomUIEventBindingType.Activating, + "#NavCards[" + index + "] #NavActionButton", + EventData.of("Button", "Nav").append("NavTarget", entry.id()), + false + ); + + index++; + } + } + + /** + * Handles navigation events from the player nav bar. + * + * @param targetId The target page ID from event data + * @param player The player entity + * @param ref Entity reference + * @param store Entity store + * @param playerRef Player reference + * @param guiManager The GUI manager + * @return true if the event was handled + */ + public static boolean handleNavEvent( + @NotNull String targetId, + @NotNull Player player, + @NotNull Ref ref, + @NotNull Store store, + @NotNull PlayerRef playerRef, + @NotNull GuiManager guiManager + ) { + return handleNavEvent(targetId, player, ref, store, playerRef, guiManager, GuiType.PLAYER); + } + + /** + * Handles navigation events from nav bar with explicit GUI type. + * + * @param targetId The target page ID from event data + * @param player The player entity + * @param ref Entity reference + * @param store Entity store + * @param playerRef Player reference + * @param guiManager The GUI manager + * @param guiType Whether this is a player or admin nav bar + * @return true if the event was handled + */ + public static boolean handleNavEvent( + @NotNull String targetId, + @NotNull Player player, + @NotNull Ref ref, + @NotNull Store store, + @NotNull PlayerRef playerRef, + @NotNull GuiManager guiManager, + @NotNull GuiType guiType + ) { + if (targetId.isEmpty()) { + return false; + } + + PageRegistry registry = guiType == GuiType.ADMIN + ? guiManager.getAdminRegistry() + : guiManager.getPlayerRegistry(); + + PageRegistry.Entry entry = registry.getEntry(targetId); + if (entry == null) { + return true; + } + + if (entry.permission() != null && !PermissionManager.get().hasPermission(playerRef.getUuid(), entry.permission())) { + return true; + } + + InteractiveCustomUIPage page = entry.supplier().create( + player, ref, store, playerRef, guiManager + ); + + if (page != null) { + player.getPageManager().openCustomPage(ref, store, page); + } + + return true; + } +} diff --git a/src/main/java/com/hyperessentials/gui/PageRegistry.java b/src/main/java/com/hyperessentials/gui/PageRegistry.java index 47ba259..16d3b27 100644 --- a/src/main/java/com/hyperessentials/gui/PageRegistry.java +++ b/src/main/java/com/hyperessentials/gui/PageRegistry.java @@ -1,110 +1,110 @@ -package com.hyperessentials.gui; - -import com.hyperessentials.integration.PermissionManager; -import com.hypixel.hytale.server.core.universe.PlayerRef; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Registry for GUI pages. Modules register their pages here. - * Pages can be auto-unregistered when a module is disabled. - */ -public class PageRegistry { - - /** - * A registered GUI page entry. - * - * @param id Unique page identifier (e.g., "homes", "warps") - * @param displayName UI display name - * @param module Owning module name - * @param permission Required permission node (null for no permission) - * @param supplier Function to create the page instance - * @param showsInNavBar Whether this page appears in the navigation bar - * @param order Display order in navigation (lower = first) - */ - public record Entry( - @NotNull String id, - @NotNull String displayName, - @NotNull String module, - @Nullable String permission, - @NotNull PageSupplier supplier, - boolean showsInNavBar, - int order - ) implements Comparable { - @Override - public int compareTo(@NotNull Entry other) { - return Integer.compare(this.order, other.order); - } - } - - private final Map entries = new ConcurrentHashMap<>(); - private final List orderedEntries = new ArrayList<>(); - - /** - * Registers a page entry. - */ - public void registerEntry(@NotNull Entry entry) { - entries.put(entry.id(), entry); - orderedEntries.add(entry); - orderedEntries.sort(Entry::compareTo); - } - - /** - * Unregisters all pages owned by a module. - */ - public void unregisterModule(@NotNull String moduleName) { - entries.values().removeIf(e -> e.module().equals(moduleName)); - orderedEntries.removeIf(e -> e.module().equals(moduleName)); - } - - /** - * Gets an entry by ID. - */ - @Nullable - public Entry getEntry(@NotNull String id) { - return entries.get(id); - } - - /** - * Gets all registered entries in display order. - */ - @NotNull - public List getEntries() { - return Collections.unmodifiableList(orderedEntries); - } - - /** - * Gets entries that should appear in the navigation bar. - */ - @NotNull - public List getNavBarEntries() { - return orderedEntries.stream() - .filter(Entry::showsInNavBar) - .toList(); - } - - /** - * Gets nav bar entries accessible to a player. - */ - @NotNull - public List getAccessibleNavBarEntries(@NotNull PlayerRef playerRef) { - return orderedEntries.stream() - .filter(Entry::showsInNavBar) - .filter(entry -> { - if (entry.permission() == null) return true; - return PermissionManager.get().hasPermission(playerRef.getUuid(), entry.permission()); - }) - .toList(); - } - - /** - * Clears all registered entries. - */ - public void clear() { - entries.clear(); - orderedEntries.clear(); - } -} +package com.hyperessentials.gui; + +import com.hyperessentials.integration.PermissionManager; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Registry for GUI pages. Modules register their pages here. + * Pages can be auto-unregistered when a module is disabled. + */ +public class PageRegistry { + + /** + * A registered GUI page entry. + * + * @param id Unique page identifier (e.g., "homes", "warps") + * @param displayName UI display name + * @param module Owning module name + * @param permission Required permission node (null for no permission) + * @param supplier Function to create the page instance + * @param showsInNavBar Whether this page appears in the navigation bar + * @param order Display order in navigation (lower = first) + */ + public record Entry( + @NotNull String id, + @NotNull String displayName, + @NotNull String module, + @Nullable String permission, + @NotNull PageSupplier supplier, + boolean showsInNavBar, + int order + ) implements Comparable { + @Override + public int compareTo(@NotNull Entry other) { + return Integer.compare(this.order, other.order); + } + } + + private final Map entries = new ConcurrentHashMap<>(); + private final List orderedEntries = new ArrayList<>(); + + /** + * Registers a page entry. + */ + public void registerEntry(@NotNull Entry entry) { + entries.put(entry.id(), entry); + orderedEntries.add(entry); + orderedEntries.sort(Entry::compareTo); + } + + /** + * Unregisters all pages owned by a module. + */ + public void unregisterModule(@NotNull String moduleName) { + entries.values().removeIf(e -> e.module().equals(moduleName)); + orderedEntries.removeIf(e -> e.module().equals(moduleName)); + } + + /** + * Gets an entry by ID. + */ + @Nullable + public Entry getEntry(@NotNull String id) { + return entries.get(id); + } + + /** + * Gets all registered entries in display order. + */ + @NotNull + public List getEntries() { + return Collections.unmodifiableList(orderedEntries); + } + + /** + * Gets entries that should appear in the navigation bar. + */ + @NotNull + public List getNavBarEntries() { + return orderedEntries.stream() + .filter(Entry::showsInNavBar) + .toList(); + } + + /** + * Gets nav bar entries accessible to a player. + */ + @NotNull + public List getAccessibleNavBarEntries(@NotNull PlayerRef playerRef) { + return orderedEntries.stream() + .filter(Entry::showsInNavBar) + .filter(entry -> { + if (entry.permission() == null) return true; + return PermissionManager.get().hasPermission(playerRef.getUuid(), entry.permission()); + }) + .toList(); + } + + /** + * Clears all registered entries. + */ + public void clear() { + entries.clear(); + orderedEntries.clear(); + } +} diff --git a/src/main/java/com/hyperessentials/gui/PageSupplier.java b/src/main/java/com/hyperessentials/gui/PageSupplier.java index 3725abd..9e64d96 100644 --- a/src/main/java/com/hyperessentials/gui/PageSupplier.java +++ b/src/main/java/com/hyperessentials/gui/PageSupplier.java @@ -1,35 +1,35 @@ -package com.hyperessentials.gui; - -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.entity.entities.Player; -import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; -import com.hypixel.hytale.server.core.universe.PlayerRef; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import org.jetbrains.annotations.Nullable; - -/** - * Functional interface for creating GUI page instances. - */ -@FunctionalInterface -public interface PageSupplier { - - /** - * Creates a new page instance. - * - * @param player The player entity - * @param ref Entity reference - * @param store Entity store - * @param playerRef Player reference - * @param guiManager The GUI manager - * @return The created page, or null if page cannot be created - */ - @Nullable - InteractiveCustomUIPage create( - Player player, - Ref ref, - Store store, - PlayerRef playerRef, - GuiManager guiManager - ); -} +package com.hyperessentials.gui; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.Nullable; + +/** + * Functional interface for creating GUI page instances. + */ +@FunctionalInterface +public interface PageSupplier { + + /** + * Creates a new page instance. + * + * @param player The player entity + * @param ref Entity reference + * @param store Entity store + * @param playerRef Player reference + * @param guiManager The GUI manager + * @return The created page, or null if page cannot be created + */ + @Nullable + InteractiveCustomUIPage create( + Player player, + Ref ref, + Store store, + PlayerRef playerRef, + GuiManager guiManager + ); +} diff --git a/src/main/java/com/hyperessentials/gui/PlayerPageOpener.java b/src/main/java/com/hyperessentials/gui/PlayerPageOpener.java new file mode 100644 index 0000000..89859ef --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/PlayerPageOpener.java @@ -0,0 +1,53 @@ +package com.hyperessentials.gui; + +import com.hyperessentials.integration.PermissionManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * Utility for opening player GUI pages by page ID. + */ +public final class PlayerPageOpener { + + private PlayerPageOpener() {} + + /** + * Opens a player page by ID. + * + * @return true if the page was opened successfully + */ + public static boolean open( + @NotNull String pageId, + @NotNull Player player, + @NotNull Ref ref, + @NotNull Store store, + @NotNull PlayerRef playerRef, + @NotNull GuiManager guiManager + ) { + PageRegistry.Entry entry = guiManager.getPlayerRegistry().getEntry(pageId); + if (entry == null) { + return false; + } + + if (entry.permission() != null + && !PermissionManager.get().hasPermission(playerRef.getUuid(), entry.permission())) { + return false; + } + + InteractiveCustomUIPage page = entry.supplier().create( + player, ref, store, playerRef, guiManager + ); + + if (page != null) { + player.getPageManager().openCustomPage(ref, store, page); + return true; + } + + return false; + } +} diff --git a/src/main/java/com/hyperessentials/gui/RefreshablePage.java b/src/main/java/com/hyperessentials/gui/RefreshablePage.java index c7a6e8e..618b6cf 100644 --- a/src/main/java/com/hyperessentials/gui/RefreshablePage.java +++ b/src/main/java/com/hyperessentials/gui/RefreshablePage.java @@ -1,13 +1,13 @@ -package com.hyperessentials.gui; - -/** - * Interface for GUI pages that support real-time content refresh. - */ -public interface RefreshablePage { - - /** - * Refreshes the page content with current data. - * Must be called on the world thread. - */ - void refreshContent(); -} +package com.hyperessentials.gui; + +/** + * Interface for GUI pages that support real-time content refresh. + */ +public interface RefreshablePage { + + /** + * Refreshes the page content with current data. + * Must be called on the world thread. + */ + void refreshContent(); +} diff --git a/src/main/java/com/hyperessentials/gui/UIHelper.java b/src/main/java/com/hyperessentials/gui/UIHelper.java index 678f656..fec87b8 100644 --- a/src/main/java/com/hyperessentials/gui/UIHelper.java +++ b/src/main/java/com/hyperessentials/gui/UIHelper.java @@ -1,163 +1,178 @@ -package com.hyperessentials.gui; - -import com.hypixel.hytale.server.core.Message; - -import java.util.HashMap; -import java.util.Map; - -/** - * Helper utilities for UI formatting and styling. - */ -public final class UIHelper { - - private static final Map COLOR_MAP = new HashMap<>(); - static { - COLOR_MAP.put('0', "#000000"); - COLOR_MAP.put('1', "#0000AA"); - COLOR_MAP.put('2', "#00AA00"); - COLOR_MAP.put('3', "#00AAAA"); - COLOR_MAP.put('4', "#AA0000"); - COLOR_MAP.put('5', "#AA00AA"); - COLOR_MAP.put('6', "#FFAA00"); - COLOR_MAP.put('7', "#AAAAAA"); - COLOR_MAP.put('8', "#555555"); - COLOR_MAP.put('9', "#5555FF"); - COLOR_MAP.put('a', "#55FF55"); - COLOR_MAP.put('b', "#55FFFF"); - COLOR_MAP.put('c', "#FF5555"); - COLOR_MAP.put('d', "#FF55FF"); - COLOR_MAP.put('e', "#FFFF55"); - COLOR_MAP.put('f', "#FFFFFF"); - COLOR_MAP.put('r', "#FFFFFF"); - } - - // Brand colors - public static final String COLOR_GOLD = "#FFAA00"; - public static final String COLOR_GOLD_LIGHT = "#FFD700"; - - // Status colors - public static final String COLOR_SUCCESS = "#4aff7f"; - public static final String COLOR_DANGER = "#ff4a4a"; - public static final String COLOR_PRIMARY = "#4a9eff"; - public static final String COLOR_WARNING = "#FFFF55"; - - // Text colors - public static final String COLOR_TEXT_PRIMARY = "#ffffff"; - public static final String COLOR_TEXT_SECONDARY = "#96a9be"; - public static final String COLOR_TEXT_MUTED = "#556677"; - - // Background colors - public static final String COLOR_BG_DARK = "#0a1119"; - public static final String COLOR_BG_LIGHT = "#141c26"; - - private UIHelper() {} - - public static String formatCoords(double x, double y, double z) { - return String.format("%.0f, %.0f, %.0f", x, y, z); - } - - public static String formatNumber(long value) { - return String.format("%,d", value); - } - - public static String formatDuration(int seconds) { - if (seconds < 60) { - return seconds + "s"; - } - int minutes = seconds / 60; - int secs = seconds % 60; - if (secs == 0) { - return minutes + "m"; - } - return minutes + "m " + secs + "s"; - } - - public static String truncate(String text, int maxLength) { - if (text == null || text.length() <= maxLength) { - return text; - } - return text.substring(0, maxLength - 3) + "..."; - } - - public static String formatWorldName(String worldName) { - if (worldName == null || worldName.isEmpty()) { - return "Unknown"; - } - String result = worldName.replace('_', ' '); - StringBuilder sb = new StringBuilder(); - boolean capitalizeNext = true; - for (char c : result.toCharArray()) { - if (c == ' ') { - capitalizeNext = true; - sb.append(c); - } else if (capitalizeNext) { - sb.append(Character.toUpperCase(c)); - capitalizeNext = false; - } else { - sb.append(Character.toLowerCase(c)); - } - } - return sb.toString(); - } - - public static String formatRelativeTime(long timestamp) { - if (timestamp == 0) { - return "Never"; - } - long diff = System.currentTimeMillis() - timestamp; - long seconds = diff / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - long days = hours / 24; - - if (days > 0) { - return days + (days == 1 ? " day ago" : " days ago"); - } else if (hours > 0) { - return hours + (hours == 1 ? " hour ago" : " hours ago"); - } else if (minutes > 0) { - return minutes + (minutes == 1 ? " minute ago" : " minutes ago"); - } else { - return "Just now"; - } - } - - public static String formatLimit(int limit) { - return limit < 0 ? "\u221E" : String.valueOf(limit); - } - - public static Message parseColorCodes(String text) { - if (text == null || text.isEmpty()) { - return Message.raw(""); - } - Message result = null; - StringBuilder currentSegment = new StringBuilder(); - String currentColor = "#FFFFFF"; - int i = 0; - while (i < text.length()) { - char c = text.charAt(i); - if (c == '\u00A7') { - if (currentSegment.length() > 0) { - Message segment = Message.raw(currentSegment.toString()).color(currentColor); - result = (result == null) ? segment : result.insert(segment); - currentSegment = new StringBuilder(); - } - if (i + 1 < text.length()) { - char colorCode = Character.toLowerCase(text.charAt(i + 1)); - String newColor = COLOR_MAP.get(colorCode); - if (newColor != null) { - currentColor = newColor; - } - i += 2; - continue; - } - } - currentSegment.append(c); - i++; - } - if (currentSegment.length() > 0) { - Message segment = Message.raw(currentSegment.toString()).color(currentColor); - result = (result == null) ? segment : result.insert(segment); - } - return result != null ? result : Message.raw(""); - } -} +package com.hyperessentials.gui; + +import com.hypixel.hytale.server.core.Message; + +import java.util.HashMap; +import java.util.Map; + +/** + * Helper utilities for UI formatting and styling. + */ +public final class UIHelper { + + private static final Map COLOR_MAP = new HashMap<>(); + static { + COLOR_MAP.put('0', "#000000"); + COLOR_MAP.put('1', "#0000AA"); + COLOR_MAP.put('2', "#00AA00"); + COLOR_MAP.put('3', "#00AAAA"); + COLOR_MAP.put('4', "#AA0000"); + COLOR_MAP.put('5', "#AA00AA"); + COLOR_MAP.put('6', "#FFAA00"); + COLOR_MAP.put('7', "#AAAAAA"); + COLOR_MAP.put('8', "#555555"); + COLOR_MAP.put('9', "#5555FF"); + COLOR_MAP.put('a', "#55FF55"); + COLOR_MAP.put('b', "#55FFFF"); + COLOR_MAP.put('c', "#FF5555"); + COLOR_MAP.put('d', "#FF55FF"); + COLOR_MAP.put('e', "#FFFF55"); + COLOR_MAP.put('f', "#FFFFFF"); + COLOR_MAP.put('r', "#FFFFFF"); + } + + // Brand colors + public static final String COLOR_GOLD = "#FFAA00"; + public static final String COLOR_GOLD_LIGHT = "#FFD700"; + + // Status colors + public static final String COLOR_SUCCESS = "#4aff7f"; + public static final String COLOR_DANGER = "#ff4a4a"; + public static final String COLOR_PRIMARY = "#4a9eff"; + public static final String COLOR_WARNING = "#FFFF55"; + + // Text colors + public static final String COLOR_TEXT_PRIMARY = "#ffffff"; + public static final String COLOR_TEXT_SECONDARY = "#96a9be"; + public static final String COLOR_TEXT_MUTED = "#556677"; + + // Background colors + public static final String COLOR_BG_DARK = "#0a1119"; + public static final String COLOR_BG_LIGHT = "#141c26"; + + private UIHelper() {} + + public static String formatCoords(double x, double y, double z) { + return String.format("%.0f, %.0f, %.0f", x, y, z); + } + + public static String formatNumber(long value) { + return String.format("%,d", value); + } + + public static String formatDuration(int seconds) { + if (seconds < 60) { + return seconds + "s"; + } + int minutes = seconds / 60; + int secs = seconds % 60; + if (secs == 0) { + return minutes + "m"; + } + return minutes + "m " + secs + "s"; + } + + public static String truncate(String text, int maxLength) { + if (text == null || text.length() <= maxLength) { + return text; + } + return text.substring(0, maxLength - 3) + "..."; + } + + public static String formatWorldName(String worldName) { + if (worldName == null || worldName.isEmpty()) { + return "Unknown"; + } + String result = worldName.replace('_', ' '); + StringBuilder sb = new StringBuilder(); + boolean capitalizeNext = true; + for (char c : result.toCharArray()) { + if (c == ' ') { + capitalizeNext = true; + sb.append(c); + } else if (capitalizeNext) { + sb.append(Character.toUpperCase(c)); + capitalizeNext = false; + } else { + sb.append(Character.toLowerCase(c)); + } + } + return sb.toString(); + } + + public static String formatRelativeTime(long timestamp) { + if (timestamp == 0) { + return "Never"; + } + long diff = System.currentTimeMillis() - timestamp; + long seconds = diff / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + long days = hours / 24; + + if (days > 0) { + return days + (days == 1 ? " day ago" : " days ago"); + } else if (hours > 0) { + return hours + (hours == 1 ? " hour ago" : " hours ago"); + } else if (minutes > 0) { + return minutes + (minutes == 1 ? " minute ago" : " minutes ago"); + } else { + return "Just now"; + } + } + + public static String formatLimit(int limit) { + return limit < 0 ? "\u221E" : String.valueOf(limit); + } + + public static String formatPlaytime(long ms) { + long totalSeconds = ms / 1000; + long days = totalSeconds / 86400; + long hours = (totalSeconds % 86400) / 3600; + long minutes = (totalSeconds % 3600) / 60; + + if (days > 0) { + return days + "d " + hours + "h " + minutes + "m"; + } else if (hours > 0) { + return hours + "h " + minutes + "m"; + } else { + return minutes + "m"; + } + } + + public static Message parseColorCodes(String text) { + if (text == null || text.isEmpty()) { + return Message.raw(""); + } + Message result = null; + StringBuilder currentSegment = new StringBuilder(); + String currentColor = "#FFFFFF"; + int i = 0; + while (i < text.length()) { + char c = text.charAt(i); + if (c == '\u00A7') { + if (currentSegment.length() > 0) { + Message segment = Message.raw(currentSegment.toString()).color(currentColor); + result = (result == null) ? segment : result.insert(segment); + currentSegment = new StringBuilder(); + } + if (i + 1 < text.length()) { + char colorCode = Character.toLowerCase(text.charAt(i + 1)); + String newColor = COLOR_MAP.get(colorCode); + if (newColor != null) { + currentColor = newColor; + } + i += 2; + continue; + } + } + currentSegment.append(c); + i++; + } + if (currentSegment.length() > 0) { + Message segment = Message.raw(currentSegment.toString()).color(currentColor); + result = (result == null) ? segment : result.insert(segment); + } + return result != null ? result : Message.raw(""); + } +} diff --git a/src/main/java/com/hyperessentials/gui/UIPaths.java b/src/main/java/com/hyperessentials/gui/UIPaths.java new file mode 100644 index 0000000..f122a8c --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/UIPaths.java @@ -0,0 +1,65 @@ +package com.hyperessentials.gui; + +/** + * Centralized UI template path constants for all HyperEssentials pages. + */ +public final class UIPaths { + + private UIPaths() {} + + private static final String BASE = "HyperEssentials/"; + + // === Shared === + public static final String STYLES = BASE + "shared/styles.ui"; + public static final String NAV_BAR = BASE + "shared/nav_bar.ui"; + public static final String NAV_BUTTON = BASE + "shared/nav_button.ui"; + public static final String NAV_BUTTON_ACTIVE = BASE + "shared/nav_button_active.ui"; + public static final String ERROR_PAGE = BASE + "shared/error_page.ui"; + public static final String EMPTY_STATE = BASE + "shared/empty_state.ui"; + public static final String CONFIRM_MODAL = BASE + "shared/confirm_modal.ui"; + public static final String STAT_ROW = BASE + "shared/stat_row.ui"; + + // === Player Pages === + public static final String PLAYER_DASHBOARD = BASE + "player/dashboard.ui"; + public static final String PLAYER_STATS = BASE + "player/stats.ui"; + + // === Homes === + public static final String HOMES_PAGE = BASE + "homes/homes_page.ui"; + public static final String HOME_ENTRY = BASE + "homes/home_entry.ui"; + + // === Warps === + public static final String WARPS_PAGE = BASE + "warps/warps_page.ui"; + public static final String WARP_ENTRY = BASE + "warps/warp_entry.ui"; + public static final String WARP_CATEGORY_HEADER = BASE + "warps/warp_category_header.ui"; + + // === Kits === + public static final String KITS_PAGE = BASE + "kits/kits_page.ui"; + public static final String KIT_ENTRY = BASE + "kits/kit_entry.ui"; + + // === Teleport === + public static final String TPA_PAGE = BASE + "teleport/tpa_page.ui"; + public static final String TPA_ENTRY = BASE + "teleport/tpa_entry.ui"; + + // === Admin === + public static final String ADMIN_NAV_BAR = BASE + "admin/admin_nav_bar.ui"; + public static final String ADMIN_NAV_BUTTON = BASE + "admin/admin_nav_button.ui"; + public static final String ADMIN_NAV_BUTTON_ACTIVE = BASE + "admin/admin_nav_button_active.ui"; + public static final String ADMIN_DASHBOARD = BASE + "admin/admin_dashboard.ui"; + public static final String ADMIN_MODULE_CARD = BASE + "admin/admin_module_card.ui"; + public static final String ADMIN_WARPS = BASE + "admin/admin_warps.ui"; + public static final String ADMIN_WARP_ENTRY = BASE + "admin/admin_warp_entry.ui"; + public static final String ADMIN_SPAWNS = BASE + "admin/admin_spawns.ui"; + public static final String ADMIN_SPAWN_ENTRY = BASE + "admin/admin_spawn_entry.ui"; + public static final String ADMIN_KITS = BASE + "admin/admin_kits.ui"; + public static final String ADMIN_KIT_ENTRY = BASE + "admin/admin_kit_entry.ui"; + public static final String ADMIN_PLAYERS = BASE + "admin/admin_players.ui"; + public static final String ADMIN_PLAYER_ENTRY = BASE + "admin/admin_player_entry.ui"; + public static final String ADMIN_PLAYER_DETAIL = BASE + "admin/admin_player_detail.ui"; + public static final String ADMIN_PLAYER_HOME_ENTRY = BASE + "admin/admin_player_home_entry.ui"; + public static final String ADMIN_MODERATION = BASE + "admin/admin_moderation.ui"; + public static final String ADMIN_PUNISHMENT_ENTRY = BASE + "admin/admin_punishment_entry.ui"; + public static final String ADMIN_ANNOUNCEMENTS = BASE + "admin/admin_announcements.ui"; + public static final String ADMIN_ANNOUNCEMENT_ENTRY = BASE + "admin/admin_announcement_entry.ui"; + public static final String ADMIN_SETTINGS = BASE + "admin/admin_settings.ui"; + public static final String ADMIN_MODULE_TOGGLE = BASE + "admin/admin_module_toggle.ui"; +} diff --git a/src/main/java/com/hyperessentials/gui/admin/AdminAnnouncementsPage.java b/src/main/java/com/hyperessentials/gui/admin/AdminAnnouncementsPage.java new file mode 100644 index 0000000..1bd733d --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/admin/AdminAnnouncementsPage.java @@ -0,0 +1,110 @@ +package com.hyperessentials.gui.admin; + +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.modules.AnnouncementsConfig; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.GuiType; +import com.hyperessentials.gui.NavBarHelper; +import com.hyperessentials.gui.UIHelper; +import com.hyperessentials.gui.UIPaths; +import com.hyperessentials.gui.data.AdminPageData; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * Admin announcements page — view current announcement messages and settings. + */ +public class AdminAnnouncementsPage extends InteractiveCustomUIPage { + + private final PlayerRef playerRef; + private final Player player; + private final GuiManager guiManager; + + public AdminAnnouncementsPage( + @NotNull Player player, + @NotNull PlayerRef playerRef, + @NotNull GuiManager guiManager + ) { + super(playerRef, CustomPageLifetime.CanDismiss, AdminPageData.CODEC); + this.player = player; + this.playerRef = playerRef; + this.guiManager = guiManager; + } + + @Override + public void build(@NotNull Ref ref, @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events, @NotNull Store store) { + cmd.append(UIPaths.ADMIN_ANNOUNCEMENTS); + NavBarHelper.setupAdminBar(playerRef, "announcements", guiManager.getAdminRegistry(), cmd, events); + populateMessages(cmd); + } + + private void populateMessages(@NotNull UICommandBuilder cmd) { + AnnouncementsConfig config = ConfigManager.get().announcements(); + List messages = config.getMessages(); + + cmd.set("#MessageCount.Text", messages.size() + " message" + (messages.size() != 1 ? "s" : "")); + + int intervalSecs = config.getIntervalSeconds(); + if (intervalSecs > 0) { + cmd.set("#IntervalLabel.Text", "Interval: " + formatInterval(intervalSecs)); + } else { + cmd.set("#IntervalLabel.Text", "Announcements disabled"); + } + + cmd.set("#ModeLabel.Text", config.isRandomize() ? "Mode: Random" : "Mode: Sequential"); + + cmd.clear("#MessageList"); + cmd.appendInline("#MessageList", "Group #IndexCards { LayoutMode: Top; }"); + + if (messages.isEmpty()) { + cmd.append("#IndexCards", UIPaths.EMPTY_STATE); + cmd.set("#IndexCards[0] #EmptyTitle.Text", "No Messages"); + cmd.set("#IndexCards[0] #EmptyMessage.Text", "Add messages in announcements.json config."); + return; + } + + int i = 0; + for (String message : messages) { + cmd.append("#IndexCards", UIPaths.ADMIN_ANNOUNCEMENT_ENTRY); + String idx = "#IndexCards[" + i + "]"; + + cmd.set(idx + " #EntryNumber.Text", "#" + (i + 1)); + cmd.set(idx + " #MessageText.Text", UIHelper.truncate(message, 60)); + + i++; + } + } + + private String formatInterval(int seconds) { + if (seconds < 60) return seconds + "s"; + if (seconds < 3600) return (seconds / 60) + "m"; + return (seconds / 3600) + "h " + ((seconds % 3600) / 60) + "m"; + } + + @Override + public void handleDataEvent(@NotNull Ref ref, @NotNull Store store, + @NotNull AdminPageData data) { + super.handleDataEvent(ref, store, data); + + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent( + data.navTarget != null ? data.navTarget : "", + player, ref, store, playerRef, guiManager, GuiType.ADMIN + ); + return; + } + + sendUpdate(); + } +} diff --git a/src/main/java/com/hyperessentials/gui/admin/AdminDashboardPage.java b/src/main/java/com/hyperessentials/gui/admin/AdminDashboardPage.java new file mode 100644 index 0000000..cdc82ca --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/admin/AdminDashboardPage.java @@ -0,0 +1,117 @@ +package com.hyperessentials.gui.admin; + +import com.hyperessentials.BuildInfo; +import com.hyperessentials.gui.GuiColors; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.GuiType; +import com.hyperessentials.gui.NavBarHelper; +import com.hyperessentials.gui.UIPaths; +import com.hyperessentials.gui.data.AdminPageData; +import com.hyperessentials.module.Module; +import com.hyperessentials.module.ModuleRegistry; +import com.hyperessentials.module.kits.KitsModule; +import com.hyperessentials.module.spawns.SpawnsModule; +import com.hyperessentials.module.warps.WarpsModule; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * Admin dashboard — server overview, stats cards, module status grid. + */ +public class AdminDashboardPage extends InteractiveCustomUIPage { + + private final PlayerRef playerRef; + private final Player player; + private final GuiManager guiManager; + private final ModuleRegistry moduleRegistry; + + public AdminDashboardPage( + @NotNull Player player, + @NotNull PlayerRef playerRef, + @NotNull GuiManager guiManager, + @NotNull ModuleRegistry moduleRegistry + ) { + super(playerRef, CustomPageLifetime.CanDismiss, AdminPageData.CODEC); + this.player = player; + this.playerRef = playerRef; + this.guiManager = guiManager; + this.moduleRegistry = moduleRegistry; + } + + @Override + public void build(@NotNull Ref ref, @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events, @NotNull Store store) { + cmd.append(UIPaths.ADMIN_DASHBOARD); + NavBarHelper.setupAdminBar(playerRef, "dashboard", guiManager.getAdminRegistry(), cmd, events); + populateDashboard(cmd); + } + + private void populateDashboard(@NotNull UICommandBuilder cmd) { + // Version info + cmd.set("#VersionLabel.Text", "v" + BuildInfo.VERSION); + + // Online count + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin != null) { + cmd.set("#OnlineCount.Text", String.valueOf(plugin.getTrackedPlayers().size())); + } + + // Warp count + WarpsModule warps = moduleRegistry.getModule(WarpsModule.class); + if (warps != null && warps.isEnabled() && warps.getWarpManager() != null) { + cmd.set("#WarpCount.Text", String.valueOf(warps.getWarpManager().getWarpCount())); + } + + // Spawn count + SpawnsModule spawns = moduleRegistry.getModule(SpawnsModule.class); + if (spawns != null && spawns.isEnabled() && spawns.getSpawnManager() != null) { + cmd.set("#SpawnCount.Text", String.valueOf(spawns.getSpawnManager().getSpawnCount())); + } + + // Kit count + KitsModule kits = moduleRegistry.getModule(KitsModule.class); + if (kits != null && kits.isEnabled() && kits.getKitManager() != null) { + cmd.set("#KitCount.Text", String.valueOf(kits.getKitManager().getAllKits().size())); + } + + // Module status grid + cmd.clear("#ModuleList"); + int idx = 0; + for (Module module : moduleRegistry.getModules()) { + cmd.append("#ModuleList", UIPaths.ADMIN_MODULE_CARD); + String selector = "#ModuleList[" + idx + "]"; + cmd.set(selector + " #ModuleName.Text", module.getDisplayName()); + + boolean enabled = module.isEnabled(); + cmd.set(selector + " #ModuleStatus.Text", enabled ? "Enabled" : "Disabled"); + cmd.set(selector + " #ModuleStatus.Style.TextColor", GuiColors.forModuleEnabled(enabled)); + cmd.set(selector + " #StatusDot.Background.Color", GuiColors.forModuleEnabled(enabled)); + idx++; + } + } + + @Override + public void handleDataEvent(@NotNull Ref ref, @NotNull Store store, + @NotNull AdminPageData data) { + super.handleDataEvent(ref, store, data); + + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent( + data.navTarget != null ? data.navTarget : "", + player, ref, store, playerRef, guiManager, GuiType.ADMIN + ); + return; + } + + sendUpdate(); + } +} diff --git a/src/main/java/com/hyperessentials/gui/admin/AdminKitsPage.java b/src/main/java/com/hyperessentials/gui/admin/AdminKitsPage.java new file mode 100644 index 0000000..cd64695 --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/admin/AdminKitsPage.java @@ -0,0 +1,169 @@ +package com.hyperessentials.gui.admin; + +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.GuiType; +import com.hyperessentials.gui.NavBarHelper; +import com.hyperessentials.gui.UIPaths; +import com.hyperessentials.gui.data.AdminPageData; +import com.hyperessentials.module.kits.KitManager; +import com.hyperessentials.module.kits.data.Kit; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.ui.builder.EventData; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +/** + * Admin kits page — list all kits with create from inventory/delete. + */ +public class AdminKitsPage extends InteractiveCustomUIPage { + + private final PlayerRef playerRef; + private final Player player; + private final Ref pageRef; + private final Store pageStore; + private final KitManager kitManager; + private final GuiManager guiManager; + + public AdminKitsPage( + @NotNull Player player, + @NotNull PlayerRef playerRef, + @NotNull Ref ref, + @NotNull Store store, + @NotNull KitManager kitManager, + @NotNull GuiManager guiManager + ) { + super(playerRef, CustomPageLifetime.CanDismiss, AdminPageData.CODEC); + this.player = player; + this.playerRef = playerRef; + this.pageRef = ref; + this.pageStore = store; + this.kitManager = kitManager; + this.guiManager = guiManager; + } + + @Override + public void build(@NotNull Ref ref, @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events, @NotNull Store store) { + cmd.append(UIPaths.ADMIN_KITS); + NavBarHelper.setupAdminBar(playerRef, "kits", guiManager.getAdminRegistry(), cmd, events); + buildKitList(cmd, events); + } + + private void buildKitList(@NotNull UICommandBuilder cmd, @NotNull UIEventBuilder events) { + Collection allKits = kitManager.getAllKits(); + cmd.set("#KitCount.Text", allKits.size() + " kit" + (allKits.size() != 1 ? "s" : "")); + + // Create button + events.addEventBinding( + CustomUIEventBindingType.Activating, "#CreateBtn", + EventData.of("Button", "Create"), false + ); + + // Sort kits by name + List sorted = new ArrayList<>(allKits); + sorted.sort(Comparator.comparing(Kit::name)); + + cmd.clear("#KitList"); + cmd.appendInline("#KitList", "Group #IndexCards { LayoutMode: Top; }"); + + if (sorted.isEmpty()) { + cmd.append("#IndexCards", UIPaths.EMPTY_STATE); + cmd.set("#IndexCards[0] #EmptyTitle.Text", "No Kits"); + cmd.set("#IndexCards[0] #EmptyMessage.Text", "Create one from your current inventory."); + return; + } + + int i = 0; + for (Kit kit : sorted) { + cmd.append("#IndexCards", UIPaths.ADMIN_KIT_ENTRY); + String idx = "#IndexCards[" + i + "]"; + + cmd.set(idx + " #KitName.Text", kit.displayName()); + cmd.set(idx + " #KitItems.Text", kit.items().size() + " items"); + + // Cooldown info + if (kit.cooldownSeconds() > 0) { + cmd.set(idx + " #KitCooldown.Text", formatCooldown(kit.cooldownSeconds())); + } + + // One-time badge + if (kit.oneTime()) { + cmd.set(idx + " #KitOneTime.Text", "One-time"); + } + + events.addEventBinding( + CustomUIEventBindingType.Activating, + idx + " #DeleteBtn", + EventData.of("Button", "Delete").append("Target", kit.name()), + false + ); + + i++; + } + } + + private String formatCooldown(int seconds) { + if (seconds < 60) return seconds + "s cooldown"; + if (seconds < 3600) return (seconds / 60) + "m cooldown"; + if (seconds < 86400) return (seconds / 3600) + "h cooldown"; + return (seconds / 86400) + "d cooldown"; + } + + @Override + public void handleDataEvent(@NotNull Ref ref, @NotNull Store store, + @NotNull AdminPageData data) { + super.handleDataEvent(ref, store, data); + + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent( + data.navTarget != null ? data.navTarget : "", + player, ref, store, playerRef, guiManager, GuiType.ADMIN + ); + return; + } + + if (data.button == null) { + sendUpdate(); + return; + } + + switch (data.button) { + case "Create" -> handleCreate(); + case "Delete" -> handleDelete(data.target); + default -> sendUpdate(); + } + } + + private void handleCreate() { + // Capture kit from player's current inventory + String name = "kit_" + System.currentTimeMillis() % 100000; + kitManager.captureFromInventory(playerRef, pageStore, pageRef, name); + rebuildList(); + } + + private void handleDelete(String kitName) { + if (kitName == null) return; + kitManager.deleteKit(kitName); + rebuildList(); + } + + private void rebuildList() { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + buildKitList(cmd, events); + sendUpdate(cmd, events, false); + } +} diff --git a/src/main/java/com/hyperessentials/gui/admin/AdminModerationPage.java b/src/main/java/com/hyperessentials/gui/admin/AdminModerationPage.java new file mode 100644 index 0000000..57ea0bb --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/admin/AdminModerationPage.java @@ -0,0 +1,195 @@ +package com.hyperessentials.gui.admin; + +import com.hyperessentials.gui.GuiColors; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.GuiType; +import com.hyperessentials.gui.NavBarHelper; +import com.hyperessentials.gui.UIHelper; +import com.hyperessentials.gui.UIPaths; +import com.hyperessentials.gui.data.AdminPageData; +import com.hyperessentials.module.moderation.ModerationManager; +import com.hyperessentials.module.moderation.data.Punishment; +import com.hyperessentials.module.moderation.data.PunishmentType; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.ui.builder.EventData; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; + +/** + * Admin moderation page — list punishments with filter and revoke. + */ +public class AdminModerationPage extends InteractiveCustomUIPage { + + private static final DateTimeFormatter DATE_FORMAT = + DateTimeFormatter.ofPattern("MMM d, yyyy HH:mm").withZone(ZoneId.systemDefault()); + + private final PlayerRef playerRef; + private final Player player; + private final ModerationManager moderationManager; + private final GuiManager guiManager; + private boolean showActiveOnly = true; + + public AdminModerationPage( + @NotNull Player player, + @NotNull PlayerRef playerRef, + @NotNull ModerationManager moderationManager, + @NotNull GuiManager guiManager + ) { + super(playerRef, CustomPageLifetime.CanDismiss, AdminPageData.CODEC); + this.player = player; + this.playerRef = playerRef; + this.moderationManager = moderationManager; + this.guiManager = guiManager; + } + + @Override + public void build(@NotNull Ref ref, @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events, @NotNull Store store) { + cmd.append(UIPaths.ADMIN_MODERATION); + NavBarHelper.setupAdminBar(playerRef, "moderation", guiManager.getAdminRegistry(), cmd, events); + buildPunishmentList(cmd, events); + } + + private void buildPunishmentList(@NotNull UICommandBuilder cmd, @NotNull UIEventBuilder events) { + // Filter buttons + events.addEventBinding( + CustomUIEventBindingType.Activating, "#FilterActive", + EventData.of("Button", "FilterActive"), false + ); + events.addEventBinding( + CustomUIEventBindingType.Activating, "#FilterAll", + EventData.of("Button", "FilterAll"), false + ); + + List punishments = moderationManager.getAllPunishments(showActiveOnly); + punishments.sort(Comparator.comparing(Punishment::issuedAt).reversed()); + + String label = punishments.size() + " punishment" + (punishments.size() != 1 ? "s" : ""); + if (showActiveOnly) label += " (active)"; + cmd.set("#PunishmentCount.Text", label); + + cmd.clear("#PunishmentList"); + cmd.appendInline("#PunishmentList", "Group #IndexCards { LayoutMode: Top; }"); + + if (punishments.isEmpty()) { + cmd.append("#IndexCards", UIPaths.EMPTY_STATE); + cmd.set("#IndexCards[0] #EmptyTitle.Text", showActiveOnly ? "No Active Punishments" : "No Punishments"); + cmd.set("#IndexCards[0] #EmptyMessage.Text", "No punishment records found."); + return; + } + + int i = 0; + for (Punishment p : punishments) { + cmd.append("#IndexCards", UIPaths.ADMIN_PUNISHMENT_ENTRY); + String idx = "#IndexCards[" + i + "]"; + + // Type badge and color + String typeLabel = p.type().name(); + String typeColor = switch (p.type()) { + case BAN -> GuiColors.DANGER; + case MUTE -> GuiColors.WARNING; + case KICK -> GuiColors.INFO; + }; + cmd.set(idx + " #TypeBadge.Text", typeLabel); + cmd.set(idx + " #TypeBadge.Style.TextColor", typeColor); + cmd.set(idx + " #TypeBar.Background.Color", typeColor); + + cmd.set(idx + " #PlayerName.Text", p.playerName()); + cmd.set(idx + " #IssuerName.Text", "by " + p.issuerName()); + cmd.set(idx + " #Reason.Text", p.reason() != null ? p.reason() : "No reason"); + + // Expires info + if (p.isEffective()) { + if (p.isPermanent()) { + cmd.set(idx + " #Expires.Text", "Permanent"); + } else { + cmd.set(idx + " #Expires.Text", UIHelper.formatDuration((int) (p.getRemainingMillis() / 1000))); + } + } else { + cmd.set(idx + " #Expires.Text", p.active() ? "Expired" : "Revoked"); + } + + // Only show revoke button for active punishments (not kicks) + if (p.isEffective() && p.type() != PunishmentType.KICK) { + events.addEventBinding( + CustomUIEventBindingType.Activating, + idx + " #RevokeBtn", + EventData.of("Button", "Revoke").append("Target", p.id().toString()), + false + ); + } + + i++; + } + } + + @Override + public void handleDataEvent(@NotNull Ref ref, @NotNull Store store, + @NotNull AdminPageData data) { + super.handleDataEvent(ref, store, data); + + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent( + data.navTarget != null ? data.navTarget : "", + player, ref, store, playerRef, guiManager, GuiType.ADMIN + ); + return; + } + + if (data.button == null) { + sendUpdate(); + return; + } + + switch (data.button) { + case "FilterActive" -> { showActiveOnly = true; rebuildList(); } + case "FilterAll" -> { showActiveOnly = false; rebuildList(); } + case "Revoke" -> { handleRevoke(data.target); } + default -> sendUpdate(); + } + } + + private void handleRevoke(String punishmentIdStr) { + if (punishmentIdStr == null) return; + + try { + UUID punishmentId = UUID.fromString(punishmentIdStr); + // Find the punishment and revoke it based on type + List all = moderationManager.getAllPunishments(true); + for (Punishment p : all) { + if (p.id().equals(punishmentId)) { + if (p.type() == PunishmentType.BAN) { + moderationManager.unban(p.playerUuid(), playerRef.getUuid(), playerRef.getUsername()); + } else if (p.type() == PunishmentType.MUTE) { + moderationManager.unmute(p.playerUuid(), playerRef.getUuid(), playerRef.getUsername()); + } + break; + } + } + } catch (IllegalArgumentException ignored) { + } + + rebuildList(); + } + + private void rebuildList() { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + buildPunishmentList(cmd, events); + sendUpdate(cmd, events, false); + } +} diff --git a/src/main/java/com/hyperessentials/gui/admin/AdminPlayersPage.java b/src/main/java/com/hyperessentials/gui/admin/AdminPlayersPage.java new file mode 100644 index 0000000..c2c10fd --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/admin/AdminPlayersPage.java @@ -0,0 +1,101 @@ +package com.hyperessentials.gui.admin; + +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.GuiType; +import com.hyperessentials.gui.NavBarHelper; +import com.hyperessentials.gui.UIPaths; +import com.hyperessentials.gui.data.AdminPageData; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Admin players page — list online players with status info. + */ +public class AdminPlayersPage extends InteractiveCustomUIPage { + + private final PlayerRef playerRef; + private final Player player; + private final GuiManager guiManager; + + public AdminPlayersPage( + @NotNull Player player, + @NotNull PlayerRef playerRef, + @NotNull GuiManager guiManager + ) { + super(playerRef, CustomPageLifetime.CanDismiss, AdminPageData.CODEC); + this.player = player; + this.playerRef = playerRef; + this.guiManager = guiManager; + } + + @Override + public void build(@NotNull Ref ref, @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events, @NotNull Store store) { + cmd.append(UIPaths.ADMIN_PLAYERS); + NavBarHelper.setupAdminBar(playerRef, "players", guiManager.getAdminRegistry(), cmd, events); + buildPlayerList(cmd); + } + + private void buildPlayerList(@NotNull UICommandBuilder cmd) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + Map tracked = plugin != null ? plugin.getTrackedPlayers() : Map.of(); + + cmd.set("#PlayerCount.Text", tracked.size() + " player" + (tracked.size() != 1 ? "s" : "") + " online"); + + // Sort players by username + List sorted = new ArrayList<>(tracked.values()); + sorted.sort(Comparator.comparing(PlayerRef::getUsername, String.CASE_INSENSITIVE_ORDER)); + + cmd.clear("#PlayerList"); + cmd.appendInline("#PlayerList", "Group #IndexCards { LayoutMode: Top; }"); + + if (sorted.isEmpty()) { + cmd.append("#IndexCards", UIPaths.EMPTY_STATE); + cmd.set("#IndexCards[0] #EmptyTitle.Text", "No Players"); + cmd.set("#IndexCards[0] #EmptyMessage.Text", "No players are currently online."); + return; + } + + int i = 0; + for (PlayerRef p : sorted) { + cmd.append("#IndexCards", UIPaths.ADMIN_PLAYER_ENTRY); + String idx = "#IndexCards[" + i + "]"; + + cmd.set(idx + " #PlayerName.Text", p.getUsername()); + cmd.set(idx + " #PlayerInfo.Text", "UUID: " + p.getUuid().toString().substring(0, 8) + "..."); + + i++; + } + } + + @Override + public void handleDataEvent(@NotNull Ref ref, @NotNull Store store, + @NotNull AdminPageData data) { + super.handleDataEvent(ref, store, data); + + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent( + data.navTarget != null ? data.navTarget : "", + player, ref, store, playerRef, guiManager, GuiType.ADMIN + ); + return; + } + + sendUpdate(); + } +} diff --git a/src/main/java/com/hyperessentials/gui/admin/AdminSettingsPage.java b/src/main/java/com/hyperessentials/gui/admin/AdminSettingsPage.java new file mode 100644 index 0000000..8e64a02 --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/admin/AdminSettingsPage.java @@ -0,0 +1,119 @@ +package com.hyperessentials.gui.admin; + +import com.hyperessentials.BuildInfo; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.gui.GuiColors; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.GuiType; +import com.hyperessentials.gui.NavBarHelper; +import com.hyperessentials.gui.UIPaths; +import com.hyperessentials.gui.data.AdminPageData; +import com.hyperessentials.module.Module; +import com.hyperessentials.module.ModuleRegistry; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.ui.builder.EventData; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +/** + * Admin settings page — version info, config reload, module status. + */ +public class AdminSettingsPage extends InteractiveCustomUIPage { + + private final PlayerRef playerRef; + private final Player player; + private final GuiManager guiManager; + private final ModuleRegistry moduleRegistry; + private final Path dataDir; + + public AdminSettingsPage( + @NotNull Player player, + @NotNull PlayerRef playerRef, + @NotNull GuiManager guiManager, + @NotNull ModuleRegistry moduleRegistry, + @NotNull Path dataDir + ) { + super(playerRef, CustomPageLifetime.CanDismiss, AdminPageData.CODEC); + this.player = player; + this.playerRef = playerRef; + this.guiManager = guiManager; + this.moduleRegistry = moduleRegistry; + this.dataDir = dataDir; + } + + @Override + public void build(@NotNull Ref ref, @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events, @NotNull Store store) { + cmd.append(UIPaths.ADMIN_SETTINGS); + NavBarHelper.setupAdminBar(playerRef, "settings", guiManager.getAdminRegistry(), cmd, events); + populateSettings(cmd, events); + } + + private void populateSettings(@NotNull UICommandBuilder cmd, @NotNull UIEventBuilder events) { + // Version and data dir + cmd.set("#VersionLabel.Text", "v" + BuildInfo.VERSION); + cmd.set("#DataDirLabel.Text", "Data: " + dataDir.toAbsolutePath()); + + // Reload button + events.addEventBinding( + CustomUIEventBindingType.Activating, "#ReloadBtn", + EventData.of("Button", "Reload"), false + ); + + // Module status list + cmd.clear("#ModuleList"); + int idx = 0; + for (Module module : moduleRegistry.getModules()) { + cmd.append("#ModuleList", UIPaths.ADMIN_MODULE_TOGGLE); + String selector = "#ModuleList[" + idx + "]"; + + cmd.set(selector + " #ModuleName.Text", module.getDisplayName()); + boolean enabled = module.isEnabled(); + cmd.set(selector + " #ModuleStatus.Text", enabled ? "Enabled" : "Disabled"); + cmd.set(selector + " #ModuleStatus.Style.TextColor", GuiColors.forModuleEnabled(enabled)); + cmd.set(selector + " #StatusDot.Background.Color", GuiColors.forModuleEnabled(enabled)); + idx++; + } + } + + @Override + public void handleDataEvent(@NotNull Ref ref, @NotNull Store store, + @NotNull AdminPageData data) { + super.handleDataEvent(ref, store, data); + + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent( + data.navTarget != null ? data.navTarget : "", + player, ref, store, playerRef, guiManager, GuiType.ADMIN + ); + return; + } + + if ("Reload".equals(data.button)) { + ConfigManager.get().reloadAll(); + Logger.info("[Admin] Configuration reloaded by %s", playerRef.getUsername()); + rebuildSettings(); + return; + } + + sendUpdate(); + } + + private void rebuildSettings() { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + populateSettings(cmd, events); + sendUpdate(cmd, events, false); + } +} diff --git a/src/main/java/com/hyperessentials/gui/admin/AdminSpawnsPage.java b/src/main/java/com/hyperessentials/gui/admin/AdminSpawnsPage.java new file mode 100644 index 0000000..6a43874 --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/admin/AdminSpawnsPage.java @@ -0,0 +1,169 @@ +package com.hyperessentials.gui.admin; + +import com.hyperessentials.data.Spawn; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.GuiType; +import com.hyperessentials.gui.NavBarHelper; +import com.hyperessentials.gui.UIHelper; +import com.hyperessentials.gui.UIPaths; +import com.hyperessentials.gui.data.AdminPageData; +import com.hyperessentials.module.spawns.SpawnManager; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.ui.builder.EventData; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +/** + * Admin spawns page — list all spawns with create/delete. + */ +public class AdminSpawnsPage extends InteractiveCustomUIPage { + + private final PlayerRef playerRef; + private final Player player; + private final SpawnManager spawnManager; + private final GuiManager guiManager; + + public AdminSpawnsPage( + @NotNull Player player, + @NotNull PlayerRef playerRef, + @NotNull SpawnManager spawnManager, + @NotNull GuiManager guiManager + ) { + super(playerRef, CustomPageLifetime.CanDismiss, AdminPageData.CODEC); + this.player = player; + this.playerRef = playerRef; + this.spawnManager = spawnManager; + this.guiManager = guiManager; + } + + @Override + public void build(@NotNull Ref ref, @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events, @NotNull Store store) { + cmd.append(UIPaths.ADMIN_SPAWNS); + NavBarHelper.setupAdminBar(playerRef, "spawns", guiManager.getAdminRegistry(), cmd, events); + buildSpawnList(cmd, events); + } + + private void buildSpawnList(@NotNull UICommandBuilder cmd, @NotNull UIEventBuilder events) { + Collection allSpawns = spawnManager.getAllSpawns(); + cmd.set("#SpawnCount.Text", allSpawns.size() + " spawn" + (allSpawns.size() != 1 ? "s" : "")); + + // Create button + events.addEventBinding( + CustomUIEventBindingType.Activating, "#CreateBtn", + EventData.of("Button", "Create"), false + ); + + // Build spawn list sorted by default status then name + List sorted = new ArrayList<>(allSpawns); + sorted.sort(Comparator.comparing(Spawn::isDefault).reversed().thenComparing(Spawn::name)); + + cmd.clear("#SpawnList"); + cmd.appendInline("#SpawnList", "Group #IndexCards { LayoutMode: Top; }"); + + if (sorted.isEmpty()) { + cmd.append("#IndexCards", UIPaths.EMPTY_STATE); + cmd.set("#IndexCards[0] #EmptyTitle.Text", "No Spawns"); + cmd.set("#IndexCards[0] #EmptyMessage.Text", "Set a spawn at your current location."); + return; + } + + int i = 0; + for (Spawn spawn : sorted) { + cmd.append("#IndexCards", UIPaths.ADMIN_SPAWN_ENTRY); + String idx = "#IndexCards[" + i + "]"; + + cmd.set(idx + " #SpawnName.Text", spawn.name()); + cmd.set(idx + " #DefaultBadge.Text", spawn.isDefault() ? "DEFAULT" : ""); + cmd.set(idx + " #SpawnWorld.Text", UIHelper.formatWorldName(spawn.world())); + cmd.set(idx + " #SpawnCoords.Text", UIHelper.formatCoords(spawn.x(), spawn.y(), spawn.z())); + + events.addEventBinding( + CustomUIEventBindingType.Activating, + idx + " #DeleteBtn", + EventData.of("Button", "Delete").append("Target", spawn.name()), + false + ); + + i++; + } + } + + @Override + public void handleDataEvent(@NotNull Ref ref, @NotNull Store store, + @NotNull AdminPageData data) { + super.handleDataEvent(ref, store, data); + + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent( + data.navTarget != null ? data.navTarget : "", + player, ref, store, playerRef, guiManager, GuiType.ADMIN + ); + return; + } + + if (data.button == null) { + sendUpdate(); + return; + } + + switch (data.button) { + case "Create" -> handleCreate(); + case "Delete" -> handleDelete(data.target); + default -> sendUpdate(); + } + } + + private void handleCreate() { + var pos = playerRef.getTransform().getPosition(); + var rot = playerRef.getTransform().getRotation(); + String worldName = ""; + + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin != null) { + var worldUuid = playerRef.getWorldUuid(); + if (worldUuid != null) { + var world = com.hypixel.hytale.server.core.universe.Universe.get().getWorld(worldUuid); + if (world != null) { + worldName = world.getName(); + } + } + } + + String name = "spawn_" + System.currentTimeMillis() % 100000; + Spawn spawn = Spawn.create(name, worldName, + pos.getX(), pos.getY(), pos.getZ(), + rot.getY(), rot.getX(), + playerRef.getUuid().toString()); + spawnManager.setSpawn(spawn); + + rebuildList(); + } + + private void handleDelete(String spawnName) { + if (spawnName == null) return; + spawnManager.deleteSpawn(spawnName); + rebuildList(); + } + + private void rebuildList() { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + buildSpawnList(cmd, events); + sendUpdate(cmd, events, false); + } +} diff --git a/src/main/java/com/hyperessentials/gui/admin/AdminWarpsPage.java b/src/main/java/com/hyperessentials/gui/admin/AdminWarpsPage.java new file mode 100644 index 0000000..d3be5a0 --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/admin/AdminWarpsPage.java @@ -0,0 +1,171 @@ +package com.hyperessentials.gui.admin; + +import com.hyperessentials.data.Warp; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.GuiType; +import com.hyperessentials.gui.NavBarHelper; +import com.hyperessentials.gui.UIHelper; +import com.hyperessentials.gui.UIPaths; +import com.hyperessentials.gui.data.AdminPageData; +import com.hyperessentials.module.warps.WarpManager; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.ui.builder.EventData; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +/** + * Admin warps page — list all warps with create/delete. + */ +public class AdminWarpsPage extends InteractiveCustomUIPage { + + private final PlayerRef playerRef; + private final Player player; + private final WarpManager warpManager; + private final GuiManager guiManager; + + public AdminWarpsPage( + @NotNull Player player, + @NotNull PlayerRef playerRef, + @NotNull WarpManager warpManager, + @NotNull GuiManager guiManager + ) { + super(playerRef, CustomPageLifetime.CanDismiss, AdminPageData.CODEC); + this.player = player; + this.playerRef = playerRef; + this.warpManager = warpManager; + this.guiManager = guiManager; + } + + @Override + public void build(@NotNull Ref ref, @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events, @NotNull Store store) { + cmd.append(UIPaths.ADMIN_WARPS); + NavBarHelper.setupAdminBar(playerRef, "warps", guiManager.getAdminRegistry(), cmd, events); + buildWarpList(cmd, events); + } + + private void buildWarpList(@NotNull UICommandBuilder cmd, @NotNull UIEventBuilder events) { + Collection allWarps = warpManager.getAllWarps(); + cmd.set("#WarpCount.Text", allWarps.size() + " warp" + (allWarps.size() != 1 ? "s" : "")); + + // Create button + events.addEventBinding( + CustomUIEventBindingType.Activating, "#CreateBtn", + EventData.of("Button", "Create"), false + ); + + // Build warp list sorted by category then name + List sorted = new ArrayList<>(allWarps); + sorted.sort(Comparator.comparing(Warp::category).thenComparing(Warp::name)); + + cmd.clear("#WarpList"); + cmd.appendInline("#WarpList", "Group #IndexCards { LayoutMode: Top; }"); + + if (sorted.isEmpty()) { + cmd.append("#IndexCards", UIPaths.EMPTY_STATE); + cmd.set("#IndexCards[0] #EmptyTitle.Text", "No Warps"); + cmd.set("#IndexCards[0] #EmptyMessage.Text", "Create one at your current location."); + return; + } + + int i = 0; + for (Warp warp : sorted) { + cmd.append("#IndexCards", UIPaths.ADMIN_WARP_ENTRY); + String idx = "#IndexCards[" + i + "]"; + + cmd.set(idx + " #WarpName.Text", warp.displayName()); + cmd.set(idx + " #WarpCategory.Text", warp.category()); + cmd.set(idx + " #WarpWorld.Text", UIHelper.formatWorldName(warp.world())); + cmd.set(idx + " #WarpCoords.Text", UIHelper.formatCoords(warp.x(), warp.y(), warp.z())); + + events.addEventBinding( + CustomUIEventBindingType.Activating, + idx + " #DeleteBtn", + EventData.of("Button", "Delete").append("Target", warp.name()), + false + ); + + i++; + } + } + + @Override + public void handleDataEvent(@NotNull Ref ref, @NotNull Store store, + @NotNull AdminPageData data) { + super.handleDataEvent(ref, store, data); + + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent( + data.navTarget != null ? data.navTarget : "", + player, ref, store, playerRef, guiManager, GuiType.ADMIN + ); + return; + } + + if (data.button == null) { + sendUpdate(); + return; + } + + switch (data.button) { + case "Create" -> handleCreate(); + case "Delete" -> handleDelete(data.target); + default -> sendUpdate(); + } + } + + private void handleCreate() { + // Create warp at player's current location + var pos = playerRef.getTransform().getPosition(); + var rot = playerRef.getTransform().getRotation(); + String worldName = ""; + + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin != null) { + var worldUuid = playerRef.getWorldUuid(); + if (worldUuid != null) { + var world = com.hypixel.hytale.server.core.universe.Universe.get().getWorld(worldUuid); + if (world != null) { + worldName = world.getName(); + } + } + } + + // Generate a unique name based on timestamp + String name = "warp_" + System.currentTimeMillis() % 100000; + Warp warp = Warp.create(name, worldName, + pos.getX(), pos.getY(), pos.getZ(), + rot.getY(), rot.getX(), + playerRef.getUuid().toString()); + warpManager.setWarp(warp); + + rebuildList(); + } + + private void handleDelete(String warpName) { + if (warpName == null) return; + warpManager.deleteWarp(warpName); + rebuildList(); + } + + private void rebuildList() { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + buildWarpList(cmd, events); + sendUpdate(cmd, events, false); + } +} diff --git a/src/main/java/com/hyperessentials/gui/data/AdminPageData.java b/src/main/java/com/hyperessentials/gui/data/AdminPageData.java new file mode 100644 index 0000000..a40aa73 --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/data/AdminPageData.java @@ -0,0 +1,56 @@ +package com.hyperessentials.gui.data; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import org.jetbrains.annotations.Nullable; + +/** + * Event data codec for all admin GUI pages. + * Extends the player pattern with additional fields for admin actions. + */ +public class AdminPageData { + + public @Nullable String button; + public @Nullable String navTarget; + public @Nullable String target; + public @Nullable String value; + public @Nullable String filter; + public int page; + + public static final BuilderCodec CODEC = BuilderCodec + .builder(AdminPageData.class, AdminPageData::new) + .addField( + new KeyedCodec<>("Button", Codec.STRING), + (data, v) -> data.button = v, + data -> data.button + ) + .addField( + new KeyedCodec<>("NavTarget", Codec.STRING), + (data, v) -> data.navTarget = v, + data -> data.navTarget + ) + .addField( + new KeyedCodec<>("Target", Codec.STRING), + (data, v) -> data.target = v, + data -> data.target + ) + .addField( + new KeyedCodec<>("Value", Codec.STRING), + (data, v) -> data.value = v, + data -> data.value + ) + .addField( + new KeyedCodec<>("Filter", Codec.STRING), + (data, v) -> data.filter = v, + data -> data.filter + ) + .addField( + new KeyedCodec<>("Page", Codec.INTEGER), + (data, v) -> data.page = v, + data -> data.page + ) + .build(); + + public AdminPageData() {} +} diff --git a/src/main/java/com/hyperessentials/gui/data/PlayerPageData.java b/src/main/java/com/hyperessentials/gui/data/PlayerPageData.java new file mode 100644 index 0000000..3eef8b3 --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/data/PlayerPageData.java @@ -0,0 +1,44 @@ +package com.hyperessentials.gui.data; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import org.jetbrains.annotations.Nullable; + +/** + * Event data codec for all player GUI pages. + * Follows the HyperFactions NavAwareData pattern. + */ +public class PlayerPageData { + + public @Nullable String button; + public @Nullable String navTarget; + public @Nullable String target; + public int page; + + public static final BuilderCodec CODEC = BuilderCodec + .builder(PlayerPageData.class, PlayerPageData::new) + .addField( + new KeyedCodec<>("Button", Codec.STRING), + (data, value) -> data.button = value, + data -> data.button + ) + .addField( + new KeyedCodec<>("NavTarget", Codec.STRING), + (data, value) -> data.navTarget = value, + data -> data.navTarget + ) + .addField( + new KeyedCodec<>("Target", Codec.STRING), + (data, value) -> data.target = value, + data -> data.target + ) + .addField( + new KeyedCodec<>("Page", Codec.INTEGER), + (data, value) -> data.page = value, + data -> data.page + ) + .build(); + + public PlayerPageData() {} +} diff --git a/src/main/java/com/hyperessentials/gui/player/HomesPage.java b/src/main/java/com/hyperessentials/gui/player/HomesPage.java new file mode 100644 index 0000000..39dfb2a --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/player/HomesPage.java @@ -0,0 +1,201 @@ +package com.hyperessentials.gui.player; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.Home; +import com.hyperessentials.data.Location; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.GuiType; +import com.hyperessentials.gui.NavBarHelper; +import com.hyperessentials.gui.UIHelper; +import com.hyperessentials.gui.UIPaths; +import com.hyperessentials.gui.data.PlayerPageData; +import com.hyperessentials.module.homes.HomeManager; +import com.hyperessentials.module.warmup.WarmupManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.protocol.packets.interface_.Page; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport; +import com.hypixel.hytale.server.core.ui.builder.EventData; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +/** + * Player homes page — browse, teleport to, and delete homes. + */ +public class HomesPage extends InteractiveCustomUIPage { + + private final PlayerRef playerRef; + private final Player player; + private final HomeManager homeManager; + private final WarmupManager warmupManager; + private final GuiManager guiManager; + + private Ref lastRef; + private Store lastStore; + + public HomesPage( + @NotNull Player player, + @NotNull PlayerRef playerRef, + @NotNull HomeManager homeManager, + @NotNull WarmupManager warmupManager, + @NotNull GuiManager guiManager + ) { + super(playerRef, CustomPageLifetime.CanDismiss, PlayerPageData.CODEC); + this.player = player; + this.playerRef = playerRef; + this.homeManager = homeManager; + this.warmupManager = warmupManager; + this.guiManager = guiManager; + } + + @Override + public void build(@NotNull Ref ref, @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events, @NotNull Store store) { + this.lastRef = ref; + this.lastStore = store; + + cmd.append(UIPaths.HOMES_PAGE); + NavBarHelper.setupBar(playerRef, "homes", guiManager.getPlayerRegistry(), cmd, events); + buildHomeList(cmd, events); + } + + private void buildHomeList(@NotNull UICommandBuilder cmd, @NotNull UIEventBuilder events) { + UUID uuid = playerRef.getUuid(); + Collection homes = homeManager.getHomes(uuid); + int limit = homeManager.getHomeLimit(uuid); + + cmd.set("#HomeCount.Text", homes.size() + " / " + UIHelper.formatLimit(limit) + " homes"); + + cmd.clear("#HomeList"); + cmd.appendInline("#HomeList", "Group #IndexCards { LayoutMode: Top; }"); + + if (homes.isEmpty()) { + cmd.append("#IndexCards", UIPaths.EMPTY_STATE); + cmd.set("#IndexCards[0] #EmptyTitle.Text", "No Homes"); + cmd.set("#IndexCards[0] #EmptyMessage.Text", "Use /sethome to create your first home."); + return; + } + + List sorted = new ArrayList<>(homes); + sorted.sort((a, b) -> a.name().compareToIgnoreCase(b.name())); + + int i = 0; + for (Home home : sorted) { + cmd.append("#IndexCards", UIPaths.HOME_ENTRY); + String idx = "#IndexCards[" + i + "]"; + + cmd.set(idx + " #HomeName.Text", home.name()); + cmd.set(idx + " #HomeWorld.Text", UIHelper.formatWorldName(home.world())); + cmd.set(idx + " #HomeCoords.Text", UIHelper.formatCoords(home.x(), home.y(), home.z())); + + events.addEventBinding( + CustomUIEventBindingType.Activating, + idx + " #TeleportBtn", + EventData.of("Button", "Teleport").append("Target", home.name()), + false + ); + + events.addEventBinding( + CustomUIEventBindingType.Activating, + idx + " #DeleteBtn", + EventData.of("Button", "Delete").append("Target", home.name()), + false + ); + + i++; + } + } + + @Override + public void handleDataEvent(@NotNull Ref ref, @NotNull Store store, + @NotNull PlayerPageData data) { + super.handleDataEvent(ref, store, data); + + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent( + data.navTarget != null ? data.navTarget : "", + player, ref, store, playerRef, guiManager, GuiType.PLAYER + ); + return; + } + + if (data.button == null) { + sendUpdate(); + return; + } + + switch (data.button) { + case "Teleport" -> handleTeleport(ref, data.target); + case "Delete" -> handleDelete(ref, store, data.target); + default -> sendUpdate(); + } + } + + private void handleTeleport(@NotNull Ref ref, String homeName) { + if (homeName == null) return; + + UUID uuid = playerRef.getUuid(); + Home home = homeManager.getHome(uuid, homeName); + if (home == null) { + rebuildList(); + return; + } + + if (warmupManager.isOnCooldown(uuid, "homes", "home")) { + rebuildList(); + return; + } + + Location dest = Location.fromHome(home); + warmupManager.startWarmup(uuid, "homes", "home", () -> { + World targetWorld = Universe.get().getWorld(dest.world()); + if (targetWorld == null) return; + targetWorld.execute(() -> { + if (!ref.isValid()) return; + Store s = ref.getStore(); + Vector3d position = new Vector3d(dest.x(), dest.y(), dest.z()); + Vector3f rotation = new Vector3f(dest.pitch(), dest.yaw(), 0); + Teleport teleport = Teleport.createForPlayer(targetWorld, position, rotation); + s.addComponent(ref, Teleport.getComponentType(), teleport); + }); + }); + + // Close GUI after starting teleport + player.getPageManager().setPage(ref, lastStore, Page.None); + } + + private void handleDelete(@NotNull Ref ref, @NotNull Store store, + String homeName) { + if (homeName == null) return; + + UUID uuid = playerRef.getUuid(); + boolean deleted = homeManager.deleteHome(uuid, homeName); + if (deleted) { + rebuildList(); + } + } + + private void rebuildList() { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + buildHomeList(cmd, events); + sendUpdate(cmd, events, false); + } +} diff --git a/src/main/java/com/hyperessentials/gui/player/KitsPage.java b/src/main/java/com/hyperessentials/gui/player/KitsPage.java new file mode 100644 index 0000000..1d2230b --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/player/KitsPage.java @@ -0,0 +1,182 @@ +package com.hyperessentials.gui.player; + +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.GuiType; +import com.hyperessentials.gui.NavBarHelper; +import com.hyperessentials.gui.UIHelper; +import com.hyperessentials.gui.UIPaths; +import com.hyperessentials.gui.data.PlayerPageData; +import com.hyperessentials.module.kits.KitManager; +import com.hyperessentials.module.kits.data.Kit; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.ui.builder.EventData; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Player kits page — browse available kits, claim with cooldown, preview items. + */ +public class KitsPage extends InteractiveCustomUIPage { + + private final PlayerRef playerRef; + private final Player player; + private final KitManager kitManager; + private final GuiManager guiManager; + + private Ref lastRef; + private Store lastStore; + + public KitsPage( + @NotNull Player player, + @NotNull PlayerRef playerRef, + @NotNull KitManager kitManager, + @NotNull GuiManager guiManager + ) { + super(playerRef, CustomPageLifetime.CanDismiss, PlayerPageData.CODEC); + this.player = player; + this.playerRef = playerRef; + this.kitManager = kitManager; + this.guiManager = guiManager; + } + + @Override + public void build(@NotNull Ref ref, @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events, @NotNull Store store) { + this.lastRef = ref; + this.lastStore = store; + + cmd.append(UIPaths.KITS_PAGE); + NavBarHelper.setupBar(playerRef, "kits", guiManager.getPlayerRegistry(), cmd, events); + buildKitList(ref, store, cmd, events); + } + + private void buildKitList(@NotNull Ref ref, @NotNull Store store, + @NotNull UICommandBuilder cmd, @NotNull UIEventBuilder events) { + UUID uuid = playerRef.getUuid(); + List kits = kitManager.getAvailableKits(uuid); + + cmd.set("#KitCount.Text", kits.size() + " kits available"); + + cmd.clear("#KitList"); + cmd.appendInline("#KitList", "Group #IndexCards { LayoutMode: Top; }"); + + if (kits.isEmpty()) { + cmd.append("#IndexCards", UIPaths.EMPTY_STATE); + cmd.set("#IndexCards[0] #EmptyTitle.Text", "No Kits"); + cmd.set("#IndexCards[0] #EmptyMessage.Text", "No kits are currently available to you."); + return; + } + + List sorted = new ArrayList<>(kits); + sorted.sort((a, b) -> a.displayName().compareToIgnoreCase(b.displayName())); + + int i = 0; + for (Kit kit : sorted) { + cmd.append("#IndexCards", UIPaths.KIT_ENTRY); + String idx = "#IndexCards[" + i + "]"; + + cmd.set(idx + " #KitName.Text", kit.displayName()); + cmd.set(idx + " #KitItems.Text", kit.items().size() + " items"); + + // Cooldown info + long remainingMs = kitManager.getRemainingCooldown(uuid, kit.name()); + if (remainingMs > 0) { + int remainingSecs = (int) (remainingMs / 1000); + cmd.set(idx + " #KitCooldown.Text", "Cooldown: " + UIHelper.formatDuration(remainingSecs)); + } else if (kit.oneTime()) { + cmd.set(idx + " #KitCooldown.Text", "One-time kit"); + } else if (kit.cooldownSeconds() > 0) { + cmd.set(idx + " #KitCooldown.Text", "Ready"); + } + + events.addEventBinding( + CustomUIEventBindingType.Activating, + idx + " #ClaimBtn", + EventData.of("Button", "Claim").append("Target", kit.name()), + false + ); + + events.addEventBinding( + CustomUIEventBindingType.Activating, + idx + " #PreviewBtn", + EventData.of("Button", "Preview").append("Target", kit.name()), + false + ); + + i++; + } + } + + @Override + public void handleDataEvent(@NotNull Ref ref, @NotNull Store store, + @NotNull PlayerPageData data) { + super.handleDataEvent(ref, store, data); + + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent( + data.navTarget != null ? data.navTarget : "", + player, ref, store, playerRef, guiManager, GuiType.PLAYER + ); + return; + } + + if (data.button == null) { + sendUpdate(); + return; + } + + switch (data.button) { + case "Claim" -> handleClaim(ref, store, data.target); + case "Preview" -> handlePreview(data.target); + default -> sendUpdate(); + } + } + + private void handleClaim(@NotNull Ref ref, @NotNull Store store, + String kitName) { + if (kitName == null) return; + + UUID uuid = playerRef.getUuid(); + Kit kit = kitManager.getKit(kitName); + if (kit == null) { + rebuildList(ref, store); + return; + } + + KitManager.ClaimResult result = kitManager.claimKit(uuid, playerRef, store, ref, kit); + + // Rebuild list to update cooldown status after claim attempt + rebuildList(ref, store); + } + + private void handlePreview(String kitName) { + if (kitName == null) return; + + Kit kit = kitManager.getKit(kitName); + if (kit == null) return; + + // Preview shows kit items in chat (keeps GUI open) + // The PreviewKitCommand already handles this via text + // For GUI, we just rebuild with current state + rebuildList(lastRef, lastStore); + } + + private void rebuildList(@NotNull Ref ref, @NotNull Store store) { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + buildKitList(ref, store, cmd, events); + sendUpdate(cmd, events, false); + } +} diff --git a/src/main/java/com/hyperessentials/gui/player/PlayerDashboardPage.java b/src/main/java/com/hyperessentials/gui/player/PlayerDashboardPage.java new file mode 100644 index 0000000..d8001b4 --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/player/PlayerDashboardPage.java @@ -0,0 +1,142 @@ +package com.hyperessentials.gui.player; + +import com.hyperessentials.data.PlayerStats; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.GuiType; +import com.hyperessentials.gui.NavBarHelper; +import com.hyperessentials.gui.UIHelper; +import com.hyperessentials.gui.UIPaths; +import com.hyperessentials.gui.data.PlayerPageData; +import com.hyperessentials.module.homes.HomeManager; +import com.hyperessentials.module.teleport.TpaManager; +import com.hyperessentials.module.utility.UtilityManager; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.ui.builder.EventData; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * Player dashboard page — welcome screen with stat cards and quick actions. + */ +public class PlayerDashboardPage extends InteractiveCustomUIPage { + + private static final DateTimeFormatter DATE_FORMAT = + DateTimeFormatter.ofPattern("MMM d, yyyy").withZone(ZoneId.systemDefault()); + + private final PlayerRef playerRef; + private final Player player; + private final GuiManager guiManager; + @Nullable private final HomeManager homeManager; + @Nullable private final TpaManager tpaManager; + @Nullable private final UtilityManager utilityManager; + + public PlayerDashboardPage( + @NotNull Player player, + @NotNull PlayerRef playerRef, + @NotNull GuiManager guiManager, + @Nullable HomeManager homeManager, + @Nullable TpaManager tpaManager, + @Nullable UtilityManager utilityManager + ) { + super(playerRef, CustomPageLifetime.CanDismiss, PlayerPageData.CODEC); + this.player = player; + this.playerRef = playerRef; + this.guiManager = guiManager; + this.homeManager = homeManager; + this.tpaManager = tpaManager; + this.utilityManager = utilityManager; + } + + @Override + public void build(@NotNull Ref ref, @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events, @NotNull Store store) { + cmd.append(UIPaths.PLAYER_DASHBOARD); + NavBarHelper.setupBar(playerRef, "dashboard", guiManager.getPlayerRegistry(), cmd, events); + populateDashboard(cmd, events); + } + + private void populateDashboard(@NotNull UICommandBuilder cmd, @NotNull UIEventBuilder events) { + UUID uuid = playerRef.getUuid(); + + // Welcome text + cmd.set("#WelcomeText.Text", "Welcome, " + playerRef.getUsername() + "!"); + + // Stat cards + if (homeManager != null) { + int homeCount = homeManager.getHomes(uuid).size(); + cmd.set("#HomesCount.Text", String.valueOf(homeCount)); + } + + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin != null) { + int onlineCount = plugin.getTrackedPlayers().size(); + cmd.set("#OnlineCount.Text", String.valueOf(onlineCount)); + } + + if (tpaManager != null) { + int tpaCount = tpaManager.getIncomingRequests(uuid).size(); + cmd.set("#TpaCount.Text", String.valueOf(tpaCount)); + } + + // Quick action buttons + events.addEventBinding( + CustomUIEventBindingType.Activating, "#HomesBtn", + EventData.of("Button", "NavDirect").append("Target", "homes"), false + ); + events.addEventBinding( + CustomUIEventBindingType.Activating, "#WarpsBtn", + EventData.of("Button", "NavDirect").append("Target", "warps"), false + ); + events.addEventBinding( + CustomUIEventBindingType.Activating, "#KitsBtn", + EventData.of("Button", "NavDirect").append("Target", "kits"), false + ); + + // Player stats + if (utilityManager != null) { + long playtimeMs = utilityManager.getTotalPlaytimeMs(uuid); + cmd.set("#PlaytimeLabel.Text", "Playtime: " + UIHelper.formatPlaytime(playtimeMs)); + + PlayerStats stats = utilityManager.getPlayerStats(uuid); + if (stats != null) { + cmd.set("#JoinDateLabel.Text", "First joined: " + DATE_FORMAT.format(stats.firstJoin())); + } + } + } + + @Override + public void handleDataEvent(@NotNull Ref ref, @NotNull Store store, + @NotNull PlayerPageData data) { + super.handleDataEvent(ref, store, data); + + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent( + data.navTarget != null ? data.navTarget : "", + player, ref, store, playerRef, guiManager, GuiType.PLAYER + ); + return; + } + + if ("NavDirect".equals(data.button) && data.target != null) { + guiManager.openPlayerPage(data.target, player, ref, store, playerRef); + return; + } + + sendUpdate(); + } +} diff --git a/src/main/java/com/hyperessentials/gui/player/StatsPage.java b/src/main/java/com/hyperessentials/gui/player/StatsPage.java new file mode 100644 index 0000000..b45468c --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/player/StatsPage.java @@ -0,0 +1,136 @@ +package com.hyperessentials.gui.player; + +import com.hyperessentials.data.PlayerStats; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.GuiType; +import com.hyperessentials.gui.NavBarHelper; +import com.hyperessentials.gui.UIHelper; +import com.hyperessentials.gui.UIPaths; +import com.hyperessentials.gui.data.PlayerPageData; +import com.hyperessentials.module.utility.UtilityManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * Player stats page — displays playtime, join date, session info, and status indicators. + */ +public class StatsPage extends InteractiveCustomUIPage { + + private static final DateTimeFormatter DATE_FORMAT = + DateTimeFormatter.ofPattern("MMM d, yyyy 'at' HH:mm").withZone(ZoneId.systemDefault()); + + private final PlayerRef playerRef; + private final Player player; + private final GuiManager guiManager; + @Nullable private final UtilityManager utilityManager; + + public StatsPage( + @NotNull Player player, + @NotNull PlayerRef playerRef, + @NotNull GuiManager guiManager, + @Nullable UtilityManager utilityManager + ) { + super(playerRef, CustomPageLifetime.CanDismiss, PlayerPageData.CODEC); + this.player = player; + this.playerRef = playerRef; + this.guiManager = guiManager; + this.utilityManager = utilityManager; + } + + @Override + public void build(@NotNull Ref ref, @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events, @NotNull Store store) { + cmd.append(UIPaths.PLAYER_STATS); + NavBarHelper.setupBar(playerRef, "stats", guiManager.getPlayerRegistry(), cmd, events); + populateStats(cmd); + } + + private void populateStats(@NotNull UICommandBuilder cmd) { + UUID uuid = playerRef.getUuid(); + + cmd.set("#PlayerName.Text", playerRef.getUsername()); + + // Stats rows + cmd.clear("#StatsList"); + int statIdx = 0; + + if (utilityManager != null) { + PlayerStats stats = utilityManager.getPlayerStats(uuid); + + // First join date + if (stats != null) { + appendStatRow(cmd, statIdx++, "First Joined", DATE_FORMAT.format(stats.firstJoin())); + appendStatRow(cmd, statIdx++, "Last Login", DATE_FORMAT.format(stats.lastJoin())); + } + + // Total playtime + long playtimeMs = utilityManager.getTotalPlaytimeMs(uuid); + appendStatRow(cmd, statIdx++, "Total Playtime", UIHelper.formatPlaytime(playtimeMs)); + + // Session time + Instant sessionStart = utilityManager.getSessionStart(uuid); + if (sessionStart != null) { + long sessionMs = System.currentTimeMillis() - sessionStart.toEpochMilli(); + appendStatRow(cmd, statIdx++, "Current Session", UIHelper.formatPlaytime(sessionMs)); + } + } + + // Status indicators + cmd.clear("#StatusList"); + int statusIdx = 0; + + if (utilityManager != null) { + appendStatusRow(cmd, statusIdx++, "AFK", utilityManager.isAfk(uuid)); + appendStatusRow(cmd, statusIdx++, "Fly Mode", utilityManager.isFlying(uuid)); + appendStatusRow(cmd, statusIdx++, "God Mode", utilityManager.isGod(uuid)); + appendStatusRow(cmd, statusIdx++, "Infinite Stamina", utilityManager.isInfiniteStamina(uuid)); + } + } + + private void appendStatRow(@NotNull UICommandBuilder cmd, int index, String label, String value) { + cmd.append("#StatsList", UIPaths.STAT_ROW); + String idx = "#StatsList[" + index + "]"; + cmd.set(idx + " #StatLabel.Text", label); + cmd.set(idx + " #StatValue.Text", value); + } + + private void appendStatusRow(@NotNull UICommandBuilder cmd, int index, String label, boolean active) { + cmd.append("#StatusList", UIPaths.STAT_ROW); + String idx = "#StatusList[" + index + "]"; + cmd.set(idx + " #StatLabel.Text", label); + cmd.set(idx + " #StatValue.Text", active ? "Active" : "Inactive"); + if (active) { + cmd.set(idx + " #StatValue.Style.TextColor", "#4aff7f"); + } + } + + @Override + public void handleDataEvent(@NotNull Ref ref, @NotNull Store store, + @NotNull PlayerPageData data) { + super.handleDataEvent(ref, store, data); + + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent( + data.navTarget != null ? data.navTarget : "", + player, ref, store, playerRef, guiManager, GuiType.PLAYER + ); + return; + } + + sendUpdate(); + } +} diff --git a/src/main/java/com/hyperessentials/gui/player/TpaPage.java b/src/main/java/com/hyperessentials/gui/player/TpaPage.java new file mode 100644 index 0000000..4e047a8 --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/player/TpaPage.java @@ -0,0 +1,209 @@ +package com.hyperessentials.gui.player; + +import com.hyperessentials.data.TeleportRequest; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.GuiType; +import com.hyperessentials.gui.NavBarHelper; +import com.hyperessentials.gui.UIPaths; +import com.hyperessentials.gui.data.PlayerPageData; +import com.hyperessentials.module.teleport.TpaManager; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.ui.builder.EventData; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.UUID; + +/** + * Player TPA page — list incoming TPA requests with accept/deny, toggle TPA acceptance. + */ +public class TpaPage extends InteractiveCustomUIPage { + + private final PlayerRef playerRef; + private final Player player; + private final TpaManager tpaManager; + private final GuiManager guiManager; + + public TpaPage( + @NotNull Player player, + @NotNull PlayerRef playerRef, + @NotNull TpaManager tpaManager, + @NotNull GuiManager guiManager + ) { + super(playerRef, CustomPageLifetime.CanDismiss, PlayerPageData.CODEC); + this.player = player; + this.playerRef = playerRef; + this.tpaManager = tpaManager; + this.guiManager = guiManager; + } + + @Override + public void build(@NotNull Ref ref, @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events, @NotNull Store store) { + cmd.append(UIPaths.TPA_PAGE); + NavBarHelper.setupBar(playerRef, "tpa", guiManager.getPlayerRegistry(), cmd, events); + buildRequestList(cmd, events); + } + + private void buildRequestList(@NotNull UICommandBuilder cmd, @NotNull UIEventBuilder events) { + UUID uuid = playerRef.getUuid(); + + // Toggle button + boolean accepting = tpaManager.isAcceptingRequests(uuid); + cmd.set("#ToggleBtn.Text", accepting ? "ENABLED" : "DISABLED"); + cmd.set("#ToggleLabel.Text", accepting ? "Accepting TPA requests" : "TPA requests disabled"); + + events.addEventBinding( + CustomUIEventBindingType.Activating, "#ToggleBtn", + EventData.of("Button", "Toggle"), false + ); + + // Request list + List incoming = tpaManager.getIncomingRequests(uuid); + cmd.set("#RequestCount.Text", incoming.size() + " incoming request" + (incoming.size() != 1 ? "s" : "")); + + cmd.clear("#RequestList"); + cmd.appendInline("#RequestList", "Group #IndexCards { LayoutMode: Top; }"); + + if (incoming.isEmpty()) { + cmd.append("#IndexCards", UIPaths.EMPTY_STATE); + cmd.set("#IndexCards[0] #EmptyTitle.Text", "No Requests"); + cmd.set("#IndexCards[0] #EmptyMessage.Text", "No pending TPA requests."); + return; + } + + int i = 0; + for (TeleportRequest request : incoming) { + cmd.append("#IndexCards", UIPaths.TPA_ENTRY); + String idx = "#IndexCards[" + i + "]"; + + // Resolve requester name + String requesterName = resolvePlayerName(request.requester()); + cmd.set(idx + " #RequesterName.Text", requesterName); + + // Request type + String typeLabel = request.type() == TeleportRequest.Type.TPA ? "wants to teleport to you" : "wants you to teleport to them"; + cmd.set(idx + " #RequestType.Text", typeLabel); + + // Time remaining + long remainingMs = request.getRemainingTime(); + if (remainingMs > 0) { + int remainingSecs = (int) (remainingMs / 1000); + cmd.set(idx + " #TimeRemaining.Text", remainingSecs + "s left"); + } + + events.addEventBinding( + CustomUIEventBindingType.Activating, + idx + " #AcceptBtn", + EventData.of("Button", "Accept").append("Target", request.requester().toString()), + false + ); + + events.addEventBinding( + CustomUIEventBindingType.Activating, + idx + " #DenyBtn", + EventData.of("Button", "Deny").append("Target", request.requester().toString()), + false + ); + + i++; + } + } + + @Override + public void handleDataEvent(@NotNull Ref ref, @NotNull Store store, + @NotNull PlayerPageData data) { + super.handleDataEvent(ref, store, data); + + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent( + data.navTarget != null ? data.navTarget : "", + player, ref, store, playerRef, guiManager, GuiType.PLAYER + ); + return; + } + + if (data.button == null) { + sendUpdate(); + return; + } + + switch (data.button) { + case "Accept" -> handleAccept(data.target); + case "Deny" -> handleDeny(data.target); + case "Toggle" -> handleToggle(); + default -> sendUpdate(); + } + } + + private void handleAccept(String requesterUuidStr) { + if (requesterUuidStr == null) return; + + try { + UUID requesterUuid = UUID.fromString(requesterUuidStr); + List incoming = tpaManager.getIncomingRequests(playerRef.getUuid()); + for (TeleportRequest request : incoming) { + if (request.requester().equals(requesterUuid)) { + tpaManager.acceptRequest(request); + break; + } + } + } catch (IllegalArgumentException ignored) { + } + + rebuildList(); + } + + private void handleDeny(String requesterUuidStr) { + if (requesterUuidStr == null) return; + + try { + UUID requesterUuid = UUID.fromString(requesterUuidStr); + List incoming = tpaManager.getIncomingRequests(playerRef.getUuid()); + for (TeleportRequest request : incoming) { + if (request.requester().equals(requesterUuid)) { + tpaManager.denyRequest(request); + break; + } + } + } catch (IllegalArgumentException ignored) { + } + + rebuildList(); + } + + private void handleToggle() { + UUID uuid = playerRef.getUuid(); + // TpaManager stores toggle state — toggle it + tpaManager.toggleTpToggle(uuid); + rebuildList(); + } + + private String resolvePlayerName(UUID uuid) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin != null) { + PlayerRef ref = plugin.getTrackedPlayer(uuid); + if (ref != null) { + return ref.getUsername(); + } + } + return uuid.toString().substring(0, 8) + "..."; + } + + private void rebuildList() { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + buildRequestList(cmd, events); + sendUpdate(cmd, events, false); + } +} diff --git a/src/main/java/com/hyperessentials/gui/player/WarpsPage.java b/src/main/java/com/hyperessentials/gui/player/WarpsPage.java new file mode 100644 index 0000000..b2354d3 --- /dev/null +++ b/src/main/java/com/hyperessentials/gui/player/WarpsPage.java @@ -0,0 +1,197 @@ +package com.hyperessentials.gui.player; + +import com.hyperessentials.data.Location; +import com.hyperessentials.data.Warp; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.gui.GuiType; +import com.hyperessentials.gui.NavBarHelper; +import com.hyperessentials.gui.UIHelper; +import com.hyperessentials.gui.UIPaths; +import com.hyperessentials.gui.data.PlayerPageData; +import com.hyperessentials.module.warmup.WarmupManager; +import com.hyperessentials.module.warps.WarpManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.protocol.packets.interface_.Page; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport; +import com.hypixel.hytale.server.core.ui.builder.EventData; +import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; +import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Player warps page — browse warps grouped by category, teleport to warps. + */ +public class WarpsPage extends InteractiveCustomUIPage { + + private final PlayerRef playerRef; + private final Player player; + private final WarpManager warpManager; + private final WarmupManager warmupManager; + private final GuiManager guiManager; + + private Ref lastRef; + private Store lastStore; + + public WarpsPage( + @NotNull Player player, + @NotNull PlayerRef playerRef, + @NotNull WarpManager warpManager, + @NotNull WarmupManager warmupManager, + @NotNull GuiManager guiManager + ) { + super(playerRef, CustomPageLifetime.CanDismiss, PlayerPageData.CODEC); + this.player = player; + this.playerRef = playerRef; + this.warpManager = warpManager; + this.warmupManager = warmupManager; + this.guiManager = guiManager; + } + + @Override + public void build(@NotNull Ref ref, @NotNull UICommandBuilder cmd, + @NotNull UIEventBuilder events, @NotNull Store store) { + this.lastRef = ref; + this.lastStore = store; + + cmd.append(UIPaths.WARPS_PAGE); + NavBarHelper.setupBar(playerRef, "warps", guiManager.getPlayerRegistry(), cmd, events); + buildWarpList(cmd, events); + } + + private void buildWarpList(@NotNull UICommandBuilder cmd, @NotNull UIEventBuilder events) { + UUID uuid = playerRef.getUuid(); + List warps = warpManager.getAccessibleWarps(uuid); + + cmd.set("#WarpCount.Text", warps.size() + " warps available"); + + cmd.clear("#WarpList"); + cmd.appendInline("#WarpList", "Group #IndexCards { LayoutMode: Top; }"); + + if (warps.isEmpty()) { + cmd.append("#IndexCards", UIPaths.EMPTY_STATE); + cmd.set("#IndexCards[0] #EmptyTitle.Text", "No Warps"); + cmd.set("#IndexCards[0] #EmptyMessage.Text", "No warps are currently available."); + return; + } + + // Group by category + Map> grouped = warps.stream() + .collect(Collectors.groupingBy(Warp::category, LinkedHashMap::new, Collectors.toList())); + + // Sort each category's warps alphabetically + for (List catWarps : grouped.values()) { + catWarps.sort((a, b) -> a.displayName().compareToIgnoreCase(b.displayName())); + } + + int i = 0; + for (Map.Entry> entry : grouped.entrySet()) { + String category = entry.getKey(); + List catWarps = entry.getValue(); + + // Category header + cmd.append("#IndexCards", UIPaths.WARP_CATEGORY_HEADER); + String catIdx = "#IndexCards[" + i + "]"; + cmd.set(catIdx + " #CategoryName.Text", category.substring(0, 1).toUpperCase() + category.substring(1)); + i++; + + // Warp entries + for (Warp warp : catWarps) { + cmd.append("#IndexCards", UIPaths.WARP_ENTRY); + String idx = "#IndexCards[" + i + "]"; + + cmd.set(idx + " #WarpName.Text", warp.displayName()); + cmd.set(idx + " #WarpCategory.Text", warp.category()); + cmd.set(idx + " #WarpWorld.Text", UIHelper.formatWorldName(warp.world())); + cmd.set(idx + " #WarpCoords.Text", UIHelper.formatCoords(warp.x(), warp.y(), warp.z())); + + events.addEventBinding( + CustomUIEventBindingType.Activating, + idx + " #TeleportBtn", + EventData.of("Button", "Teleport").append("Target", warp.name()), + false + ); + + i++; + } + } + } + + @Override + public void handleDataEvent(@NotNull Ref ref, @NotNull Store store, + @NotNull PlayerPageData data) { + super.handleDataEvent(ref, store, data); + + if ("Nav".equals(data.button)) { + NavBarHelper.handleNavEvent( + data.navTarget != null ? data.navTarget : "", + player, ref, store, playerRef, guiManager, GuiType.PLAYER + ); + return; + } + + if (data.button == null) { + sendUpdate(); + return; + } + + if ("Teleport".equals(data.button)) { + handleTeleport(ref, data.target); + } else { + sendUpdate(); + } + } + + private void handleTeleport(@NotNull Ref ref, String warpName) { + if (warpName == null) return; + + UUID uuid = playerRef.getUuid(); + Warp warp = warpManager.getWarp(warpName); + if (warp == null) { + rebuildList(); + return; + } + + if (warmupManager.isOnCooldown(uuid, "warps", "warp")) { + rebuildList(); + return; + } + + Location dest = Location.fromWarp(warp); + warmupManager.startWarmup(uuid, "warps", "warp", () -> { + World targetWorld = Universe.get().getWorld(dest.world()); + if (targetWorld == null) return; + targetWorld.execute(() -> { + if (!ref.isValid()) return; + Store s = ref.getStore(); + Vector3d position = new Vector3d(dest.x(), dest.y(), dest.z()); + Vector3f rotation = new Vector3f(dest.pitch(), dest.yaw(), 0); + Teleport teleport = Teleport.createForPlayer(targetWorld, position, rotation); + s.addComponent(ref, Teleport.getComponentType(), teleport); + }); + }); + + // Close GUI after starting teleport + player.getPageManager().setPage(ref, lastStore, Page.None); + } + + private void rebuildList() { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + buildWarpList(cmd, events); + sendUpdate(cmd, events, false); + } +} diff --git a/src/main/java/com/hyperessentials/integration/BorderHook.java b/src/main/java/com/hyperessentials/integration/BorderHook.java new file mode 100644 index 0000000..610b8cf --- /dev/null +++ b/src/main/java/com/hyperessentials/integration/BorderHook.java @@ -0,0 +1,22 @@ +package com.hyperessentials.integration; + +import org.jetbrains.annotations.NotNull; + +/** + * Hook for world border enforcement in RTP. + * Default implementation allows all locations. + * When HyperBorder is built, it registers via {@code rtpManager.setBorderHook(hook)}. + */ +@FunctionalInterface +public interface BorderHook { + + /** + * Checks whether the given coordinates are within the world border. + * + * @param worldName the world name + * @param x world X coordinate + * @param z world Z coordinate + * @return true if within border, false if outside + */ + boolean isWithinBorder(@NotNull String worldName, double x, double z); +} diff --git a/src/main/java/com/hyperessentials/integration/EcotaleIntegration.java b/src/main/java/com/hyperessentials/integration/EcotaleIntegration.java index a1b60bb..9d7d1b5 100644 --- a/src/main/java/com/hyperessentials/integration/EcotaleIntegration.java +++ b/src/main/java/com/hyperessentials/integration/EcotaleIntegration.java @@ -1,20 +1,34 @@ -package com.hyperessentials.integration; - -import com.hyperessentials.util.Logger; - -/** - * Stub for future Ecotale economy integration. - */ -public final class EcotaleIntegration { - - private static boolean available = false; - - private EcotaleIntegration() {} - - public static void init() { - // TODO: Detect and initialize Ecotale integration - Logger.debug("[Integration] Ecotale integration not yet implemented"); - } - - public static boolean isAvailable() { return available; } -} +package com.hyperessentials.integration; + +import com.hyperessentials.util.Logger; + +/** + * Detects Ecotale economy plugin availability via reflection. + */ +public final class EcotaleIntegration { + + private static boolean available = false; + private static boolean checked = false; + + private EcotaleIntegration() {} + + /** Attempts to detect Ecotale via reflection. */ + public static void init() { + if (checked) { + return; + } + checked = true; + + try { + Class.forName("com.ecotale.Ecotale"); + available = true; + Logger.info("[Integration] Ecotale detected"); + } catch (ClassNotFoundException e) { + available = false; + Logger.debug("[Integration] Ecotale not detected"); + } + } + + /** Returns true if Ecotale is available on the server. */ + public static boolean isAvailable() { return available; } +} diff --git a/src/main/java/com/hyperessentials/integration/FactionTerritoryChecker.java b/src/main/java/com/hyperessentials/integration/FactionTerritoryChecker.java new file mode 100644 index 0000000..bd09c40 --- /dev/null +++ b/src/main/java/com/hyperessentials/integration/FactionTerritoryChecker.java @@ -0,0 +1,95 @@ +package com.hyperessentials.integration; + +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.modules.HomesConfig; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Centralized faction territory check for home operations. + * Reads HomesConfig faction toggles and delegates to HyperFactionsIntegration. + */ +public final class FactionTerritoryChecker { + + public enum Result { + ALLOWED, + BLOCKED_OWN_TERRITORY, + BLOCKED_ALLY_TERRITORY, + BLOCKED_ENEMY_TERRITORY, + BLOCKED_NEUTRAL_TERRITORY, + BLOCKED_WILDERNESS; + + /** + * Returns a player-facing denial message for this result. + */ + @NotNull + public String getDenialMessage() { + return switch (this) { + case BLOCKED_OWN_TERRITORY -> "You cannot use homes in your faction's territory."; + case BLOCKED_ALLY_TERRITORY -> "You cannot use homes in allied territory."; + case BLOCKED_ENEMY_TERRITORY -> "You cannot use homes in enemy territory."; + case BLOCKED_NEUTRAL_TERRITORY -> "You cannot use homes in neutral territory."; + case BLOCKED_WILDERNESS -> "You cannot use homes in the wilderness."; + case ALLOWED -> ""; + }; + } + } + + private FactionTerritoryChecker() {} + + /** + * Checks whether a player can use a home at the given location. + * Returns ALLOWED if HyperFactions is absent or faction checks are disabled. + * + * @param playerUuid the player's UUID + * @param world the world name + * @param x the x coordinate + * @param z the z coordinate + * @return the check result + */ + @NotNull + public static Result canUseHome(@NotNull UUID playerUuid, @NotNull String world, + double x, double z) { + // Soft dependency absent — allow everything + if (!HyperFactionsIntegration.isAvailable()) { + return Result.ALLOWED; + } + + // Master toggle off — allow everything + HomesConfig config = ConfigManager.get().homes(); + if (!config.isFactionsEnabled()) { + return Result.ALLOWED; + } + + // Check if location is in a faction claim + String factionName = HyperFactionsIntegration.getFactionAtLocation(world, x, z); + if (factionName == null) { + // Wilderness + if (config.isAllowInWilderness()) { + return Result.ALLOWED; + } + Logger.debug("[Factions] Blocked home in wilderness for %s", playerUuid); + return Result.BLOCKED_WILDERNESS; + } + + // Get relation between player and territory owner + String relation = HyperFactionsIntegration.getRelationAtLocation(playerUuid, world, x, z); + if (relation == null) { + // Player not in a faction, treat as neutral + relation = "NEUTRAL"; + } + + return switch (relation) { + case "OWN" -> config.isAllowInOwnTerritory() + ? Result.ALLOWED : Result.BLOCKED_OWN_TERRITORY; + case "ALLY" -> config.isAllowInAllyTerritory() + ? Result.ALLOWED : Result.BLOCKED_ALLY_TERRITORY; + case "ENEMY" -> config.isAllowInEnemyTerritory() + ? Result.ALLOWED : Result.BLOCKED_ENEMY_TERRITORY; + default -> config.isAllowInNeutralTerritory() + ? Result.ALLOWED : Result.BLOCKED_NEUTRAL_TERRITORY; + }; + } +} diff --git a/src/main/java/com/hyperessentials/integration/HyperFactionsIntegration.java b/src/main/java/com/hyperessentials/integration/HyperFactionsIntegration.java index 6dec746..ded059a 100644 --- a/src/main/java/com/hyperessentials/integration/HyperFactionsIntegration.java +++ b/src/main/java/com/hyperessentials/integration/HyperFactionsIntegration.java @@ -1,130 +1,130 @@ -package com.hyperessentials.integration; - -import com.hyperessentials.util.Logger; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.lang.reflect.Method; -import java.util.UUID; - -/** - * Reflection-based soft dependency on HyperFactions. - */ -public final class HyperFactionsIntegration { - - private static boolean available = false; - private static boolean initialized = false; - - private static Method isAvailableMethod; - private static Method getClaimManagerMethod; - private static Method getFactionManagerMethod; - private static Method getRelationManagerMethod; - private static Method getClaimOwnerAtMethod; - private static Method getFactionMethod; - private static Method getPlayerFactionIdMethod; - private static Method getRelationMethod; - private static Method factionNameMethod; - private static Method relationTypeNameMethod; - - private HyperFactionsIntegration() {} - - public static void init() { - if (initialized) return; - initialized = true; - - try { - Class apiClass = Class.forName("com.hyperfactions.api.HyperFactionsAPI"); - isAvailableMethod = apiClass.getMethod("isAvailable"); - boolean apiAvailable = (boolean) isAvailableMethod.invoke(null); - - if (!apiAvailable) { - Logger.info("[Integration] HyperFactions detected but not yet available"); - return; - } - - getClaimManagerMethod = apiClass.getMethod("getClaimManager"); - getFactionManagerMethod = apiClass.getMethod("getFactionManager"); - getRelationManagerMethod = apiClass.getMethod("getRelationManager"); - - Object claimManager = getClaimManagerMethod.invoke(null); - if (claimManager != null) { - getClaimOwnerAtMethod = claimManager.getClass() - .getMethod("getClaimOwnerAt", String.class, double.class, double.class); - } - - Object factionManager = getFactionManagerMethod.invoke(null); - if (factionManager != null) { - getFactionMethod = factionManager.getClass().getMethod("getFaction", UUID.class); - getPlayerFactionIdMethod = factionManager.getClass().getMethod("getPlayerFactionId", UUID.class); - } - - Object relationManager = getRelationManagerMethod.invoke(null); - if (relationManager != null) { - getRelationMethod = relationManager.getClass().getMethod("getRelation", UUID.class, UUID.class); - } - - Class factionClass = Class.forName("com.hyperfactions.data.Faction"); - factionNameMethod = factionClass.getMethod("name"); - - Class relationTypeClass = Class.forName("com.hyperfactions.data.RelationType"); - relationTypeNameMethod = relationTypeClass.getMethod("name"); - - available = true; - Logger.info("[Integration] HyperFactions integration initialized successfully"); - - } catch (ClassNotFoundException e) { - Logger.info("[Integration] HyperFactions not found - territory features disabled"); - } catch (Exception e) { - Logger.warn("[Integration] Failed to initialize HyperFactions integration: %s", e.getMessage()); - } - } - - public static boolean isAvailable() { return available; } - - @Nullable - public static String getFactionAtLocation(@NotNull String world, double x, double z) { - if (!available) return null; - try { - Object claimManager = getClaimManagerMethod.invoke(null); - if (claimManager == null) return null; - UUID factionId = (UUID) getClaimOwnerAtMethod.invoke(claimManager, world, x, z); - if (factionId == null) return null; - Object factionManager = getFactionManagerMethod.invoke(null); - if (factionManager == null) return null; - Object faction = getFactionMethod.invoke(factionManager, factionId); - if (faction == null) return null; - return (String) factionNameMethod.invoke(faction); - } catch (Exception e) { - return null; - } - } - - @Nullable - public static String getRelationAtLocation(@NotNull UUID playerUuid, @NotNull String world, double x, double z) { - if (!available) return null; - try { - Object claimManager = getClaimManagerMethod.invoke(null); - if (claimManager == null) return null; - UUID territoryFactionId = (UUID) getClaimOwnerAtMethod.invoke(claimManager, world, x, z); - if (territoryFactionId == null) return null; - Object factionManager = getFactionManagerMethod.invoke(null); - if (factionManager == null) return null; - UUID playerFactionId = (UUID) getPlayerFactionIdMethod.invoke(factionManager, playerUuid); - if (playerFactionId == null) return null; - if (playerFactionId.equals(territoryFactionId)) return "OWN"; - Object relationManager = getRelationManagerMethod.invoke(null); - if (relationManager == null) return null; - Object relationType = getRelationMethod.invoke(relationManager, playerFactionId, territoryFactionId); - if (relationType == null) return "NEUTRAL"; - return (String) relationTypeNameMethod.invoke(relationType); - } catch (Exception e) { - return null; - } - } - - @NotNull - public static String getTerritoryLabel(@NotNull String world, double x, double z) { - String factionName = getFactionAtLocation(world, x, z); - return factionName != null ? factionName : "Wilderness"; - } -} +package com.hyperessentials.integration; + +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Method; +import java.util.UUID; + +/** + * Reflection-based soft dependency on HyperFactions. + */ +public final class HyperFactionsIntegration { + + private static boolean available = false; + private static boolean initialized = false; + + private static Method isAvailableMethod; + private static Method getClaimManagerMethod; + private static Method getFactionManagerMethod; + private static Method getRelationManagerMethod; + private static Method getClaimOwnerAtMethod; + private static Method getFactionMethod; + private static Method getPlayerFactionIdMethod; + private static Method getRelationMethod; + private static Method factionNameMethod; + private static Method relationTypeNameMethod; + + private HyperFactionsIntegration() {} + + public static void init() { + if (initialized) return; + initialized = true; + + try { + Class apiClass = Class.forName("com.hyperfactions.api.HyperFactionsAPI"); + isAvailableMethod = apiClass.getMethod("isAvailable"); + boolean apiAvailable = (boolean) isAvailableMethod.invoke(null); + + if (!apiAvailable) { + Logger.info("[Integration] HyperFactions detected but not yet available"); + return; + } + + getClaimManagerMethod = apiClass.getMethod("getClaimManager"); + getFactionManagerMethod = apiClass.getMethod("getFactionManager"); + getRelationManagerMethod = apiClass.getMethod("getRelationManager"); + + Object claimManager = getClaimManagerMethod.invoke(null); + if (claimManager != null) { + getClaimOwnerAtMethod = claimManager.getClass() + .getMethod("getClaimOwnerAt", String.class, double.class, double.class); + } + + Object factionManager = getFactionManagerMethod.invoke(null); + if (factionManager != null) { + getFactionMethod = factionManager.getClass().getMethod("getFaction", UUID.class); + getPlayerFactionIdMethod = factionManager.getClass().getMethod("getPlayerFactionId", UUID.class); + } + + Object relationManager = getRelationManagerMethod.invoke(null); + if (relationManager != null) { + getRelationMethod = relationManager.getClass().getMethod("getRelation", UUID.class, UUID.class); + } + + Class factionClass = Class.forName("com.hyperfactions.data.Faction"); + factionNameMethod = factionClass.getMethod("name"); + + Class relationTypeClass = Class.forName("com.hyperfactions.data.RelationType"); + relationTypeNameMethod = relationTypeClass.getMethod("name"); + + available = true; + Logger.info("[Integration] HyperFactions integration initialized successfully"); + + } catch (ClassNotFoundException e) { + Logger.info("[Integration] HyperFactions not found - territory features disabled"); + } catch (Exception e) { + Logger.warn("[Integration] Failed to initialize HyperFactions integration: %s", e.getMessage()); + } + } + + public static boolean isAvailable() { return available; } + + @Nullable + public static String getFactionAtLocation(@NotNull String world, double x, double z) { + if (!available) return null; + try { + Object claimManager = getClaimManagerMethod.invoke(null); + if (claimManager == null) return null; + UUID factionId = (UUID) getClaimOwnerAtMethod.invoke(claimManager, world, x, z); + if (factionId == null) return null; + Object factionManager = getFactionManagerMethod.invoke(null); + if (factionManager == null) return null; + Object faction = getFactionMethod.invoke(factionManager, factionId); + if (faction == null) return null; + return (String) factionNameMethod.invoke(faction); + } catch (Exception e) { + return null; + } + } + + @Nullable + public static String getRelationAtLocation(@NotNull UUID playerUuid, @NotNull String world, double x, double z) { + if (!available) return null; + try { + Object claimManager = getClaimManagerMethod.invoke(null); + if (claimManager == null) return null; + UUID territoryFactionId = (UUID) getClaimOwnerAtMethod.invoke(claimManager, world, x, z); + if (territoryFactionId == null) return null; + Object factionManager = getFactionManagerMethod.invoke(null); + if (factionManager == null) return null; + UUID playerFactionId = (UUID) getPlayerFactionIdMethod.invoke(factionManager, playerUuid); + if (playerFactionId == null) return null; + if (playerFactionId.equals(territoryFactionId)) return "OWN"; + Object relationManager = getRelationManagerMethod.invoke(null); + if (relationManager == null) return null; + Object relationType = getRelationMethod.invoke(relationManager, playerFactionId, territoryFactionId); + if (relationType == null) return "NEUTRAL"; + return (String) relationTypeNameMethod.invoke(relationType); + } catch (Exception e) { + return null; + } + } + + @NotNull + public static String getTerritoryLabel(@NotNull String world, double x, double z) { + String factionName = getFactionAtLocation(world, x, z); + return factionName != null ? factionName : "Wilderness"; + } +} diff --git a/src/main/java/com/hyperessentials/integration/HyperPermsProviderAdapter.java b/src/main/java/com/hyperessentials/integration/HyperPermsProviderAdapter.java index 41a15ad..9b473b7 100644 --- a/src/main/java/com/hyperessentials/integration/HyperPermsProviderAdapter.java +++ b/src/main/java/com/hyperessentials/integration/HyperPermsProviderAdapter.java @@ -1,132 +1,132 @@ -package com.hyperessentials.integration; - -import com.hyperessentials.util.Logger; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Method; -import java.util.Optional; -import java.util.UUID; - -/** - * Adapter wrapping HyperPerms as a PermissionProvider. - * Uses reflection to avoid hard dependency. - */ -public class HyperPermsProviderAdapter implements PermissionProvider { - - private boolean available = false; - private Object hyperPermsInstance = null; - private Method hasPermissionMethod = null; - private Method getUserManagerMethod = null; - - @Override - @NotNull - public String getName() { - return "HyperPerms"; - } - - public void init() { - try { - Class bootstrapClass = Class.forName("com.hyperperms.HyperPermsBootstrap"); - Method getInstanceMethod = bootstrapClass.getMethod("getInstance"); - hyperPermsInstance = getInstanceMethod.invoke(null); - - if (hyperPermsInstance == null) { - available = false; - return; - } - - Class instanceClass = hyperPermsInstance.getClass(); - hasPermissionMethod = instanceClass.getMethod("hasPermission", UUID.class, String.class); - getUserManagerMethod = instanceClass.getMethod("getUserManager"); - - available = true; - Logger.info("[PermissionManager] HyperPerms provider initialized"); - - } catch (ClassNotFoundException e) { - available = false; - Logger.debug("[HyperPermsProvider] HyperPerms not found"); - } catch (Exception e) { - available = false; - Logger.debug("[HyperPermsProvider] Failed to initialize: %s", e.getMessage()); - } - } - - @Override - public boolean isAvailable() { - return available; - } - - @Override - @NotNull - public Optional hasPermission(@NotNull UUID playerUuid, @NotNull String permission) { - if (!available || hyperPermsInstance == null || hasPermissionMethod == null) { - return Optional.empty(); - } - try { - Object result = hasPermissionMethod.invoke(hyperPermsInstance, playerUuid, permission); - if (result instanceof Boolean) { - return Optional.of((Boolean) result); - } - return Optional.empty(); - } catch (Exception e) { - return Optional.empty(); - } - } - - @Override - public int getPermissionValue(@NotNull UUID playerUuid, @NotNull String prefix, int defaultValue) { - if (!available || hyperPermsInstance == null || getUserManagerMethod == null) { - return defaultValue; - } - try { - Object userManager = getUserManagerMethod.invoke(hyperPermsInstance); - if (userManager == null) return defaultValue; - - Method getUserMethod = userManager.getClass().getMethod("getUser", UUID.class); - Object user = getUserMethod.invoke(userManager, playerUuid); - if (user == null) return defaultValue; - - Method getPermissionsMethod = user.getClass().getMethod("getEffectivePermissions"); - Object permissions = getPermissionsMethod.invoke(user); - - if (permissions instanceof Iterable iterable) { - int highestValue = defaultValue; - for (Object perm : iterable) { - String permStr = perm.toString(); - if (permStr.startsWith(prefix)) { - try { - int value = Integer.parseInt(permStr.substring(prefix.length())); - if (value > highestValue) highestValue = value; - } catch (NumberFormatException ignored) {} - } - } - return highestValue; - } - } catch (Exception e) { - Logger.debug("[HyperPermsProvider] Failed to get permission value: %s", e.getMessage()); - } - return defaultValue; - } - - @Override - @NotNull - public String getPrimaryGroup(@NotNull UUID playerUuid) { - if (!available || hyperPermsInstance == null || getUserManagerMethod == null) { - return "default"; - } - try { - Object userManager = getUserManagerMethod.invoke(hyperPermsInstance); - if (userManager == null) return "default"; - - Method getUserMethod = userManager.getClass().getMethod("getUser", UUID.class); - Object user = getUserMethod.invoke(userManager, playerUuid); - if (user == null) return "default"; - - Method getPrimaryGroupMethod = user.getClass().getMethod("getPrimaryGroup"); - Object result = getPrimaryGroupMethod.invoke(user); - return result != null ? result.toString() : "default"; - } catch (Exception e) { - return "default"; - } - } -} +package com.hyperessentials.integration; + +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Method; +import java.util.Optional; +import java.util.UUID; + +/** + * Adapter wrapping HyperPerms as a PermissionProvider. + * Uses reflection to avoid hard dependency. + */ +public class HyperPermsProviderAdapter implements PermissionProvider { + + private boolean available = false; + private Object hyperPermsInstance = null; + private Method hasPermissionMethod = null; + private Method getUserManagerMethod = null; + + @Override + @NotNull + public String getName() { + return "HyperPerms"; + } + + public void init() { + try { + Class bootstrapClass = Class.forName("com.hyperperms.HyperPermsBootstrap"); + Method getInstanceMethod = bootstrapClass.getMethod("getInstance"); + hyperPermsInstance = getInstanceMethod.invoke(null); + + if (hyperPermsInstance == null) { + available = false; + return; + } + + Class instanceClass = hyperPermsInstance.getClass(); + hasPermissionMethod = instanceClass.getMethod("hasPermission", UUID.class, String.class); + getUserManagerMethod = instanceClass.getMethod("getUserManager"); + + available = true; + Logger.info("[PermissionManager] HyperPerms provider initialized"); + + } catch (ClassNotFoundException e) { + available = false; + Logger.debug("[HyperPermsProvider] HyperPerms not found"); + } catch (Exception e) { + available = false; + Logger.debug("[HyperPermsProvider] Failed to initialize: %s", e.getMessage()); + } + } + + @Override + public boolean isAvailable() { + return available; + } + + @Override + @NotNull + public Optional hasPermission(@NotNull UUID playerUuid, @NotNull String permission) { + if (!available || hyperPermsInstance == null || hasPermissionMethod == null) { + return Optional.empty(); + } + try { + Object result = hasPermissionMethod.invoke(hyperPermsInstance, playerUuid, permission); + if (result instanceof Boolean) { + return Optional.of((Boolean) result); + } + return Optional.empty(); + } catch (Exception e) { + return Optional.empty(); + } + } + + @Override + public int getPermissionValue(@NotNull UUID playerUuid, @NotNull String prefix, int defaultValue) { + if (!available || hyperPermsInstance == null || getUserManagerMethod == null) { + return defaultValue; + } + try { + Object userManager = getUserManagerMethod.invoke(hyperPermsInstance); + if (userManager == null) return defaultValue; + + Method getUserMethod = userManager.getClass().getMethod("getUser", UUID.class); + Object user = getUserMethod.invoke(userManager, playerUuid); + if (user == null) return defaultValue; + + Method getPermissionsMethod = user.getClass().getMethod("getEffectivePermissions"); + Object permissions = getPermissionsMethod.invoke(user); + + if (permissions instanceof Iterable iterable) { + int highestValue = defaultValue; + for (Object perm : iterable) { + String permStr = perm.toString(); + if (permStr.startsWith(prefix)) { + try { + int value = Integer.parseInt(permStr.substring(prefix.length())); + if (value > highestValue) highestValue = value; + } catch (NumberFormatException ignored) {} + } + } + return highestValue; + } + } catch (Exception e) { + Logger.debug("[HyperPermsProvider] Failed to get permission value: %s", e.getMessage()); + } + return defaultValue; + } + + @Override + @NotNull + public String getPrimaryGroup(@NotNull UUID playerUuid) { + if (!available || hyperPermsInstance == null || getUserManagerMethod == null) { + return "default"; + } + try { + Object userManager = getUserManagerMethod.invoke(hyperPermsInstance); + if (userManager == null) return "default"; + + Method getUserMethod = userManager.getClass().getMethod("getUser", UUID.class); + Object user = getUserMethod.invoke(userManager, playerUuid); + if (user == null) return "default"; + + Method getPrimaryGroupMethod = user.getClass().getMethod("getPrimaryGroup"); + Object result = getPrimaryGroupMethod.invoke(user); + return result != null ? result.toString() : "default"; + } catch (Exception e) { + return "default"; + } + } +} diff --git a/src/main/java/com/hyperessentials/integration/PermissionManager.java b/src/main/java/com/hyperessentials/integration/PermissionManager.java index 5dba05e..01ad362 100644 --- a/src/main/java/com/hyperessentials/integration/PermissionManager.java +++ b/src/main/java/com/hyperessentials/integration/PermissionManager.java @@ -1,156 +1,156 @@ -package com.hyperessentials.integration; - -import com.hyperessentials.Permissions; -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.util.Logger; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -/** - * Unified permission manager with chain-of-responsibility pattern. - */ -public class PermissionManager { - - private static final PermissionManager INSTANCE = new PermissionManager(); - - private final List providers = new ArrayList<>(); - private boolean initialized = false; - - private PermissionManager() {} - - public static PermissionManager get() { - return INSTANCE; - } - - public void init() { - if (initialized) return; - providers.clear(); - - HyperPermsProviderAdapter hyperPermsProvider = new HyperPermsProviderAdapter(); - hyperPermsProvider.init(); - if (hyperPermsProvider.isAvailable()) { - providers.add(hyperPermsProvider); - } - - initialized = true; - - if (providers.isEmpty()) { - Logger.info("[PermissionManager] No permission providers found - using fallback mode"); - } else { - Logger.info("[PermissionManager] Initialized with %d provider(s): %s", - providers.size(), getProviderNames()); - } - } - - public boolean hasPermission(@NotNull UUID playerUuid, @NotNull String permission) { - boolean isUserLevel = isUserLevelPermission(permission); - - for (PermissionProvider provider : providers) { - Optional result = provider.hasPermission(playerUuid, permission); - if (result.isPresent()) { - if (result.get()) return true; - if (!isUserLevel) return false; - break; - } - } - - String categoryWildcard = getCategoryWildcard(permission); - if (categoryWildcard != null) { - for (PermissionProvider provider : providers) { - Optional result = provider.hasPermission(playerUuid, categoryWildcard); - if (result.isPresent() && result.get()) return true; - } - } - - for (PermissionProvider provider : providers) { - Optional result = provider.hasPermission(playerUuid, Permissions.ROOT + ".*"); - if (result.isPresent() && result.get()) return true; - } - - return handleFallback(playerUuid, permission); - } - - public int getPermissionValue(@NotNull UUID playerUuid, @NotNull String prefix, int defaultValue) { - for (PermissionProvider provider : providers) { - int value = provider.getPermissionValue(playerUuid, prefix, Integer.MIN_VALUE); - if (value != Integer.MIN_VALUE) return value; - } - return defaultValue; - } - - @NotNull - public String getPrimaryGroup(@NotNull UUID playerUuid) { - for (PermissionProvider provider : providers) { - String group = provider.getPrimaryGroup(playerUuid); - if (group != null && !group.isEmpty() && !"default".equals(group)) return group; - } - return "default"; - } - - private String getCategoryWildcard(@NotNull String permission) { - if (!permission.startsWith(Permissions.ROOT + ".")) return null; - int lastDot = permission.lastIndexOf('.'); - if (lastDot <= Permissions.ROOT.length()) return null; - return permission.substring(0, lastDot) + ".*"; - } - - private boolean isUserLevelPermission(@NotNull String permission) { - if (permission.startsWith(Permissions.ADMIN)) return false; - if (permission.startsWith(Permissions.BYPASS)) return false; - return permission.startsWith(Permissions.ROOT + "."); - } - - private boolean handleFallback(@NotNull UUID playerUuid, @NotNull String permission) { - ConfigManager config = ConfigManager.get(); - - if (permission.startsWith(Permissions.ADMIN)) { - return isPlayerOp(playerUuid); - } - if (permission.startsWith(Permissions.BYPASS)) { - return false; - } - - return config.core().isAllowWithoutPermissionMod(); - } - - private boolean isPlayerOp(@NotNull UUID playerUuid) { - try { - Class permModuleClass = Class.forName("com.hypixel.hytale.server.core.permissions.PermissionsModule"); - java.lang.reflect.Method getMethod = permModuleClass.getMethod("get"); - Object permModule = getMethod.invoke(null); - if (permModule == null) return false; - - java.lang.reflect.Method getGroupsMethod = permModuleClass.getMethod("getGroupsForUser", UUID.class); - Object groupsObj = getGroupsMethod.invoke(permModule, playerUuid); - if (groupsObj instanceof java.util.Set groups) { - if (groups.contains("OP")) return true; - } - - java.lang.reflect.Method hasPermMethod = permModuleClass.getMethod("hasPermission", UUID.class, String.class); - Object result = hasPermMethod.invoke(permModule, playerUuid, "*"); - if (result instanceof Boolean) return (Boolean) result; - } catch (Exception e) { - Logger.debug("[PermissionManager] Error checking OP status: %s", e.getMessage()); - } - return false; - } - - @NotNull - public String getProviderNames() { - if (providers.isEmpty()) return "none"; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < providers.size(); i++) { - if (i > 0) sb.append(", "); - sb.append(providers.get(i).getName()); - } - return sb.toString(); - } - - public boolean hasProviders() { - return !providers.isEmpty(); - } -} +package com.hyperessentials.integration; + +import com.hyperessentials.Permissions; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Unified permission manager with chain-of-responsibility pattern. + */ +public class PermissionManager { + + private static final PermissionManager INSTANCE = new PermissionManager(); + + private final List providers = new ArrayList<>(); + private boolean initialized = false; + + private PermissionManager() {} + + public static PermissionManager get() { + return INSTANCE; + } + + public void init() { + if (initialized) return; + providers.clear(); + + HyperPermsProviderAdapter hyperPermsProvider = new HyperPermsProviderAdapter(); + hyperPermsProvider.init(); + if (hyperPermsProvider.isAvailable()) { + providers.add(hyperPermsProvider); + } + + initialized = true; + + if (providers.isEmpty()) { + Logger.info("[PermissionManager] No permission providers found - using fallback mode"); + } else { + Logger.info("[PermissionManager] Initialized with %d provider(s): %s", + providers.size(), getProviderNames()); + } + } + + public boolean hasPermission(@NotNull UUID playerUuid, @NotNull String permission) { + boolean isUserLevel = isUserLevelPermission(permission); + + for (PermissionProvider provider : providers) { + Optional result = provider.hasPermission(playerUuid, permission); + if (result.isPresent()) { + if (result.get()) return true; + if (!isUserLevel) return false; + break; + } + } + + String categoryWildcard = getCategoryWildcard(permission); + if (categoryWildcard != null) { + for (PermissionProvider provider : providers) { + Optional result = provider.hasPermission(playerUuid, categoryWildcard); + if (result.isPresent() && result.get()) return true; + } + } + + for (PermissionProvider provider : providers) { + Optional result = provider.hasPermission(playerUuid, Permissions.ROOT + ".*"); + if (result.isPresent() && result.get()) return true; + } + + return handleFallback(playerUuid, permission); + } + + public int getPermissionValue(@NotNull UUID playerUuid, @NotNull String prefix, int defaultValue) { + for (PermissionProvider provider : providers) { + int value = provider.getPermissionValue(playerUuid, prefix, Integer.MIN_VALUE); + if (value != Integer.MIN_VALUE) return value; + } + return defaultValue; + } + + @NotNull + public String getPrimaryGroup(@NotNull UUID playerUuid) { + for (PermissionProvider provider : providers) { + String group = provider.getPrimaryGroup(playerUuid); + if (group != null && !group.isEmpty() && !"default".equals(group)) return group; + } + return "default"; + } + + private String getCategoryWildcard(@NotNull String permission) { + if (!permission.startsWith(Permissions.ROOT + ".")) return null; + int lastDot = permission.lastIndexOf('.'); + if (lastDot <= Permissions.ROOT.length()) return null; + return permission.substring(0, lastDot) + ".*"; + } + + private boolean isUserLevelPermission(@NotNull String permission) { + if (permission.startsWith(Permissions.ADMIN)) return false; + if (permission.startsWith(Permissions.BYPASS)) return false; + return permission.startsWith(Permissions.ROOT + "."); + } + + private boolean handleFallback(@NotNull UUID playerUuid, @NotNull String permission) { + ConfigManager config = ConfigManager.get(); + + if (permission.startsWith(Permissions.ADMIN)) { + return isPlayerOp(playerUuid); + } + if (permission.startsWith(Permissions.BYPASS)) { + return false; + } + + return config.core().isAllowWithoutPermissionMod(); + } + + private boolean isPlayerOp(@NotNull UUID playerUuid) { + try { + Class permModuleClass = Class.forName("com.hypixel.hytale.server.core.permissions.PermissionsModule"); + java.lang.reflect.Method getMethod = permModuleClass.getMethod("get"); + Object permModule = getMethod.invoke(null); + if (permModule == null) return false; + + java.lang.reflect.Method getGroupsMethod = permModuleClass.getMethod("getGroupsForUser", UUID.class); + Object groupsObj = getGroupsMethod.invoke(permModule, playerUuid); + if (groupsObj instanceof java.util.Set groups) { + if (groups.contains("OP")) return true; + } + + java.lang.reflect.Method hasPermMethod = permModuleClass.getMethod("hasPermission", UUID.class, String.class); + Object result = hasPermMethod.invoke(permModule, playerUuid, "*"); + if (result instanceof Boolean) return (Boolean) result; + } catch (Exception e) { + Logger.debug("[PermissionManager] Error checking OP status: %s", e.getMessage()); + } + return false; + } + + @NotNull + public String getProviderNames() { + if (providers.isEmpty()) return "none"; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < providers.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(providers.get(i).getName()); + } + return sb.toString(); + } + + public boolean hasProviders() { + return !providers.isEmpty(); + } +} diff --git a/src/main/java/com/hyperessentials/integration/PermissionProvider.java b/src/main/java/com/hyperessentials/integration/PermissionProvider.java index 764d109..88ae19b 100644 --- a/src/main/java/com/hyperessentials/integration/PermissionProvider.java +++ b/src/main/java/com/hyperessentials/integration/PermissionProvider.java @@ -1,25 +1,25 @@ -package com.hyperessentials.integration; - -import org.jetbrains.annotations.NotNull; - -import java.util.Optional; -import java.util.UUID; - -/** - * Interface for permission providers. - */ -public interface PermissionProvider { - - @NotNull - String getName(); - - boolean isAvailable(); - - @NotNull - Optional hasPermission(@NotNull UUID playerUuid, @NotNull String permission); - - int getPermissionValue(@NotNull UUID playerUuid, @NotNull String prefix, int defaultValue); - - @NotNull - String getPrimaryGroup(@NotNull UUID playerUuid); -} +package com.hyperessentials.integration; + +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.UUID; + +/** + * Interface for permission providers. + */ +public interface PermissionProvider { + + @NotNull + String getName(); + + boolean isAvailable(); + + @NotNull + Optional hasPermission(@NotNull UUID playerUuid, @NotNull String permission); + + int getPermissionValue(@NotNull UUID playerUuid, @NotNull String prefix, int defaultValue); + + @NotNull + String getPrimaryGroup(@NotNull UUID playerUuid); +} diff --git a/src/main/java/com/hyperessentials/integration/WerchatIntegration.java b/src/main/java/com/hyperessentials/integration/WerchatIntegration.java index d57cbed..2a28697 100644 --- a/src/main/java/com/hyperessentials/integration/WerchatIntegration.java +++ b/src/main/java/com/hyperessentials/integration/WerchatIntegration.java @@ -1,20 +1,20 @@ -package com.hyperessentials.integration; - -import com.hyperessentials.util.Logger; - -/** - * Stub for future Werchat chat integration. - */ -public final class WerchatIntegration { - - private static boolean available = false; - - private WerchatIntegration() {} - - public static void init() { - // TODO: Detect and initialize Werchat integration - Logger.debug("[Integration] Werchat integration not yet implemented"); - } - - public static boolean isAvailable() { return available; } -} +package com.hyperessentials.integration; + +import com.hyperessentials.util.Logger; + +/** + * Stub for future Werchat chat integration. + */ +public final class WerchatIntegration { + + private static boolean available = false; + + private WerchatIntegration() {} + + public static void init() { + // TODO: Detect and initialize Werchat integration + Logger.debug("[Integration] Werchat integration not yet implemented"); + } + + public static boolean isAvailable() { return available; } +} diff --git a/src/main/java/com/hyperessentials/integration/economy/VaultEconomyProvider.java b/src/main/java/com/hyperessentials/integration/economy/VaultEconomyProvider.java new file mode 100644 index 0000000..53a286e --- /dev/null +++ b/src/main/java/com/hyperessentials/integration/economy/VaultEconomyProvider.java @@ -0,0 +1,212 @@ +package com.hyperessentials.integration.economy; + +import com.hyperessentials.util.Logger; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.util.Optional; +import java.util.UUID; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Economy provider for VaultUnlocked (Vault2 Economy API). + * Uses reflection to integrate with VaultUnlocked's Economy interface. + * + *

Follows lazy-init + permanentFailure pattern: safe to call init() multiple + * times. ClassNotFoundException means VaultUnlocked is not installed (permanent). + * + *

Actual class paths (VaultUnlocked-Hytale 2.19.0): + *

    + *
  • Main: net.cfh.vault.VaultUnlocked
  • + *
  • Economy: net.milkbowl.vault2.economy.Economy
  • + *
  • EconomyResponse: net.milkbowl.vault2.economy.EconomyResponse
  • + *
+ */ +public class VaultEconomyProvider { + + private static final String PLUGIN_NAME = "HyperEssentials"; + + private volatile boolean available = false; + private volatile boolean permanentFailure = false; + + // Reflection references + private Object economyService = null; + private Method getBalanceMethod = null; + private Method hasMethod = null; + private Method withdrawMethod = null; + private Method depositMethod = null; + private Method transactionSuccessMethod = null; + + /** + * Initializes the VaultUnlocked economy provider. + * Safe to call multiple times. + */ + public void init() { + if (available || permanentFailure) { + return; + } + + try { + Class vaultUnlockedClass = Class.forName("net.cfh.vault.VaultUnlocked"); + + Method economyMethod = vaultUnlockedClass.getMethod("economy"); + Object econOptional = economyMethod.invoke(null); + if (econOptional instanceof Optional opt) { + economyService = opt.orElse(null); + } + + if (economyService == null) { + Logger.debug("[VaultEconomyProvider] Economy service not available yet"); + return; + } + + Class economyClass = Class.forName("net.milkbowl.vault2.economy.Economy"); + getBalanceMethod = economyClass.getMethod("getBalance", String.class, UUID.class); + hasMethod = economyClass.getMethod("has", String.class, UUID.class, BigDecimal.class); + withdrawMethod = economyClass.getMethod("withdraw", String.class, UUID.class, BigDecimal.class); + depositMethod = economyClass.getMethod("deposit", String.class, UUID.class, BigDecimal.class); + + Class responseClass = Class.forName("net.milkbowl.vault2.economy.EconomyResponse"); + transactionSuccessMethod = responseClass.getMethod("transactionSuccess"); + + available = true; + Logger.info("[Integration] VaultUnlocked economy provider initialized"); + + } catch (ClassNotFoundException | NoClassDefFoundError e) { + permanentFailure = true; + Logger.debug("[VaultEconomyProvider] VaultUnlocked not installed"); + } catch (Exception e) { + Logger.debug("[VaultEconomyProvider] Init deferred: %s", e.getMessage()); + } + } + + private void ensureInitialized() { + if (!available && !permanentFailure) { + init(); + } + } + + /** Checks if the economy provider is available. */ + public boolean isAvailable() { + ensureInitialized(); + return available; + } + + /** Checks if initialization permanently failed (VaultUnlocked not installed). */ + public boolean isPermanentFailure() { + return permanentFailure; + } + + /** Gets the name of the economy plugin registered with VaultUnlocked. */ + @Nullable + public String getEconomyName() { + ensureInitialized(); + if (!available || economyService == null) { + return null; + } + try { + Method getNameMethod = economyService.getClass().getMethod("getName"); + Object result = getNameMethod.invoke(economyService); + return result instanceof String s ? s : null; + } catch (Exception e) { + Logger.debug("[VaultEconomyProvider] Could not get economy name: %s", e.getMessage()); + return null; + } + } + + /** Gets a player's balance as BigDecimal. */ + @NotNull + public BigDecimal getBalanceBigDecimal(@NotNull UUID playerUuid) { + ensureInitialized(); + if (!available || economyService == null || getBalanceMethod == null) { + return BigDecimal.ZERO; + } + try { + Object result = getBalanceMethod.invoke(economyService, PLUGIN_NAME, playerUuid); + if (result instanceof BigDecimal bd) { + return bd; + } + return BigDecimal.ZERO; + } catch (Exception e) { + Logger.debug("[VaultEconomyProvider] Exception getting balance: %s", e.getMessage()); + return BigDecimal.ZERO; + } + } + + /** Gets a player's balance as double. */ + public double getBalance(@NotNull UUID playerUuid) { + return getBalanceBigDecimal(playerUuid).doubleValue(); + } + + /** Checks if a player has at least the specified amount. */ + public boolean has(@NotNull UUID playerUuid, @NotNull BigDecimal amount) { + ensureInitialized(); + if (!available || economyService == null || hasMethod == null) { + return false; + } + try { + Object result = hasMethod.invoke(economyService, PLUGIN_NAME, playerUuid, amount); + return result instanceof Boolean b && b; + } catch (Exception e) { + Logger.debug("[VaultEconomyProvider] Exception checking has: %s", e.getMessage()); + return false; + } + } + + /** Checks if a player has at least the specified amount (double overload). */ + public boolean has(@NotNull UUID playerUuid, double amount) { + return has(playerUuid, BigDecimal.valueOf(amount)); + } + + /** Withdraws money from a player's account. */ + public boolean withdraw(@NotNull UUID playerUuid, @NotNull BigDecimal amount) { + ensureInitialized(); + if (!available || economyService == null || withdrawMethod == null) { + return false; + } + try { + Object response = withdrawMethod.invoke(economyService, PLUGIN_NAME, playerUuid, amount); + return isSuccess(response); + } catch (Exception e) { + Logger.debug("[VaultEconomyProvider] Exception withdrawing: %s", e.getMessage()); + return false; + } + } + + /** Withdraws money from a player's account (double overload). */ + public boolean withdraw(@NotNull UUID playerUuid, double amount) { + return withdraw(playerUuid, BigDecimal.valueOf(amount)); + } + + /** Deposits money into a player's account. */ + public boolean deposit(@NotNull UUID playerUuid, @NotNull BigDecimal amount) { + ensureInitialized(); + if (!available || economyService == null || depositMethod == null) { + return false; + } + try { + Object response = depositMethod.invoke(economyService, PLUGIN_NAME, playerUuid, amount); + return isSuccess(response); + } catch (Exception e) { + Logger.debug("[VaultEconomyProvider] Exception depositing: %s", e.getMessage()); + return false; + } + } + + /** Deposits money into a player's account (double overload). */ + public boolean deposit(@NotNull UUID playerUuid, double amount) { + return deposit(playerUuid, BigDecimal.valueOf(amount)); + } + + private boolean isSuccess(Object response) { + if (response == null || transactionSuccessMethod == null) { + return false; + } + try { + Object result = transactionSuccessMethod.invoke(response); + return result instanceof Boolean b && b; + } catch (Exception e) { + return false; + } + } +} diff --git a/src/main/java/com/hyperessentials/listener/DeathListener.java b/src/main/java/com/hyperessentials/listener/DeathListener.java new file mode 100644 index 0000000..5066c4a --- /dev/null +++ b/src/main/java/com/hyperessentials/listener/DeathListener.java @@ -0,0 +1,50 @@ +package com.hyperessentials.listener; + +import com.hyperessentials.HyperEssentials; +import com.hyperessentials.data.Location; +import com.hyperessentials.module.teleport.BackManager; +import com.hyperessentials.module.teleport.TeleportModule; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Listener for player death events. + * Saves death location to back history. + */ +public class DeathListener { + + private final HyperEssentials hyperEssentials; + + public DeathListener(@NotNull HyperEssentials hyperEssentials) { + this.hyperEssentials = hyperEssentials; + } + + /** + * Called when a player dies. + * Saves their death location if configured. + */ + public void onPlayerDeath(@NotNull UUID playerUuid, @NotNull Location deathLocation) { + TeleportModule tm = hyperEssentials.getTeleportModule(); + if (tm == null || !tm.isEnabled()) { + return; + } + + BackManager backManager = tm.getBackManager(); + if (backManager != null) { + backManager.onDeath(playerUuid, deathLocation); + Logger.debug("Saved death location for %s", playerUuid); + } + } + + /** + * Called when a player respawns. + * Can teleport to spawn on respawn if configured. + */ + public void onPlayerRespawn(@NotNull UUID playerUuid) { + // Teleport to spawn on respawn would require the player's store/ref + // from the respawn event, handled at the platform level + Logger.debug("Player respawning: %s", playerUuid); + } +} diff --git a/src/main/java/com/hyperessentials/migration/Migration.java b/src/main/java/com/hyperessentials/migration/Migration.java new file mode 100644 index 0000000..91e4e09 --- /dev/null +++ b/src/main/java/com/hyperessentials/migration/Migration.java @@ -0,0 +1,37 @@ +package com.hyperessentials.migration; + +import java.nio.file.Path; +import org.jetbrains.annotations.NotNull; + +/** + * Core interface for all migrations. + * Migrations handle versioned changes to configuration files, data structures, + * or database schemas. + */ +public interface Migration { + + /** Gets the unique identifier (e.g., "config-v1-to-v2"). */ + @NotNull + String id(); + + /** Gets the type of this migration. */ + @NotNull + MigrationType type(); + + /** Gets the source version this migration upgrades from. */ + int fromVersion(); + + /** Gets the target version this migration upgrades to. */ + int toVersion(); + + /** Gets a human-readable description of what this migration does. */ + @NotNull + String description(); + + /** Checks if this migration is applicable to the current data directory. */ + boolean isApplicable(@NotNull Path dataDir); + + /** Executes the migration. */ + @NotNull + MigrationResult execute(@NotNull Path dataDir, @NotNull MigrationOptions options); +} diff --git a/src/main/java/com/hyperessentials/migration/MigrationOptions.java b/src/main/java/com/hyperessentials/migration/MigrationOptions.java new file mode 100644 index 0000000..2485a56 --- /dev/null +++ b/src/main/java/com/hyperessentials/migration/MigrationOptions.java @@ -0,0 +1,42 @@ +package com.hyperessentials.migration; + +import java.nio.file.Path; +import org.jetbrains.annotations.NotNull; + +/** + * Options for migration execution. + * + * @param backupPath path where backup was created + * @param dryRun if true, don't actually modify files + * @param verbose if true, log detailed progress + * @param progressCallback callback for progress updates (may be null) + */ +public record MigrationOptions( + Path backupPath, + boolean dryRun, + boolean verbose, + ProgressCallback progressCallback +) { + /** Callback interface for migration progress updates. */ + @FunctionalInterface + public interface ProgressCallback { + void onProgress(@NotNull String step, int current, int total); + } + + /** Creates default options with no callback. */ + public static MigrationOptions defaults(Path backupPath) { + return new MigrationOptions(backupPath, false, false, null); + } + + /** Creates options with progress callback. */ + public static MigrationOptions withProgress(Path backupPath, ProgressCallback callback) { + return new MigrationOptions(backupPath, false, false, callback); + } + + /** Reports progress if a callback is registered. */ + public void reportProgress(@NotNull String step, int current, int total) { + if (progressCallback != null) { + progressCallback.onProgress(step, current, total); + } + } +} diff --git a/src/main/java/com/hyperessentials/migration/MigrationRegistry.java b/src/main/java/com/hyperessentials/migration/MigrationRegistry.java new file mode 100644 index 0000000..d4f40d7 --- /dev/null +++ b/src/main/java/com/hyperessentials/migration/MigrationRegistry.java @@ -0,0 +1,74 @@ +package com.hyperessentials.migration; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; + +/** + * Registry for all available migrations. + * + *

Migrations are registered by type and can be queried for applicable + * migrations based on current data state. + */ +public class MigrationRegistry { + + private static final MigrationRegistry INSTANCE = new MigrationRegistry(); + + private final Map> migrations = new ConcurrentHashMap<>(); + + private MigrationRegistry() { + // No built-in migrations yet — register them here as they are created + } + + /** Gets the singleton registry instance. */ + @NotNull + public static MigrationRegistry get() { + return INSTANCE; + } + + /** Registers a migration. */ + public void register(@NotNull Migration migration) { + migrations.computeIfAbsent(migration.type(), k -> new ArrayList<>()).add(migration); + } + + /** Gets all migrations of a specific type, sorted by fromVersion. */ + @NotNull + public List getMigrations(@NotNull MigrationType type) { + List typeMigrations = migrations.get(type); + if (typeMigrations == null) { + return List.of(); + } + return typeMigrations.stream() + .sorted(Comparator.comparingInt(Migration::fromVersion)) + .toList(); + } + + /** Gets all applicable migrations for the given data directory, in execution order. */ + @NotNull + public List getApplicableMigrations(@NotNull MigrationType type, @NotNull Path dataDir) { + return getMigrations(type).stream() + .filter(m -> m.isApplicable(dataDir)) + .toList(); + } + + /** Builds a migration chain from current version to latest. */ + @NotNull + public List buildMigrationChain(@NotNull MigrationType type, @NotNull Path dataDir) { + List applicable = getApplicableMigrations(type, dataDir); + if (applicable.isEmpty()) { + return List.of(); + } + // For now, return applicable migrations in order. + // Future: build a proper chain checking version continuity. + return applicable; + } + + /** Checks if any migrations are pending for a type. */ + public boolean hasPendingMigrations(@NotNull MigrationType type, @NotNull Path dataDir) { + return !getApplicableMigrations(type, dataDir).isEmpty(); + } +} diff --git a/src/main/java/com/hyperessentials/migration/MigrationResult.java b/src/main/java/com/hyperessentials/migration/MigrationResult.java new file mode 100644 index 0000000..ed3d2c1 --- /dev/null +++ b/src/main/java/com/hyperessentials/migration/MigrationResult.java @@ -0,0 +1,69 @@ +package com.hyperessentials.migration; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Result of a migration execution. + * + * @param success true if migration completed successfully + * @param migrationId unique identifier of the migration + * @param fromVersion source version before migration + * @param toVersion target version after migration + * @param backupPath path to the backup created before migration + * @param filesCreated list of files created during migration + * @param filesModified list of files modified during migration + * @param warnings list of non-fatal warnings encountered + * @param errorMessage error message if migration failed + * @param rolledBack true if migration was rolled back after failure + * @param duration time taken to execute the migration + */ +public record MigrationResult( + boolean success, + @NotNull String migrationId, + int fromVersion, + int toVersion, + @Nullable Path backupPath, + @NotNull List filesCreated, + @NotNull List filesModified, + @NotNull List warnings, + @Nullable String errorMessage, + boolean rolledBack, + @NotNull Duration duration +) { + /** Creates a successful migration result. */ + public static MigrationResult success( + @NotNull String migrationId, int fromVersion, int toVersion, + @Nullable Path backupPath, @NotNull List filesCreated, + @NotNull List filesModified, @NotNull List warnings, + @NotNull Duration duration) { + return new MigrationResult( + true, migrationId, fromVersion, toVersion, backupPath, + filesCreated, filesModified, warnings, null, false, duration + ); + } + + /** Creates a failed migration result. */ + public static MigrationResult failure( + @NotNull String migrationId, int fromVersion, int toVersion, + @Nullable Path backupPath, @NotNull String errorMessage, + boolean rolledBack, @NotNull Duration duration) { + return new MigrationResult( + false, migrationId, fromVersion, toVersion, backupPath, + List.of(), List.of(), List.of(), errorMessage, rolledBack, duration + ); + } + + /** Creates a skipped migration result (not applicable). */ + public static MigrationResult skipped( + @NotNull String migrationId, int fromVersion, int toVersion) { + return new MigrationResult( + true, migrationId, fromVersion, toVersion, null, + List.of(), List.of(), List.of("Migration was skipped - not applicable"), + null, false, Duration.ZERO + ); + } +} diff --git a/src/main/java/com/hyperessentials/migration/MigrationRunner.java b/src/main/java/com/hyperessentials/migration/MigrationRunner.java new file mode 100644 index 0000000..9fbe560 --- /dev/null +++ b/src/main/java/com/hyperessentials/migration/MigrationRunner.java @@ -0,0 +1,261 @@ +package com.hyperessentials.migration; + +import com.hyperessentials.util.Logger; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Executes migrations with backup and rollback support. + * + *

The runner handles: + *

    + *
  • Discovering applicable migrations via {@link MigrationRegistry}
  • + *
  • Creating timestamped backups before each migration
  • + *
  • Executing migrations in order (chaining v1 -> v2 -> v3)
  • + *
  • Rolling back on failure by restoring from backup
  • + *
  • Reporting progress via callbacks
  • + *
+ */ +public class MigrationRunner { + + private static final DateTimeFormatter BACKUP_TIMESTAMP = + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss").withZone(ZoneId.systemDefault()); + + private final Path dataDir; + private final MigrationOptions.ProgressCallback progressCallback; + + public MigrationRunner(@NotNull Path dataDir) { + this(dataDir, null); + } + + public MigrationRunner(@NotNull Path dataDir, @Nullable MigrationOptions.ProgressCallback progressCallback) { + this.dataDir = dataDir; + this.progressCallback = progressCallback; + } + + /** Runs all pending migrations of a specific type. */ + @NotNull + public List runPendingMigrations(@NotNull MigrationType type) { + List migrations = MigrationRegistry.get().buildMigrationChain(type, dataDir); + + if (migrations.isEmpty()) { + Logger.debug("[Migration] No pending %s migrations", type); + return List.of(); + } + + Logger.info("[Migration] Found %d pending %s migration(s)", migrations.size(), type); + + List results = new ArrayList<>(); + for (Migration migration : migrations) { + MigrationResult result = runMigration(migration); + results.add(result); + + if (!result.success()) { + Logger.severe("[Migration] Migration '%s' failed: %s", migration.id(), result.errorMessage()); + if (result.rolledBack()) { + Logger.info("[Migration] Successfully rolled back to backup"); + } + break; + } + + Logger.info("[Migration] Migration '%s' completed successfully in %dms", + migration.id(), result.duration().toMillis()); + } + + return results; + } + + /** Runs a single migration with backup and rollback support. */ + @NotNull + public MigrationResult runMigration(@NotNull Migration migration) { + Instant startTime = Instant.now(); + + Logger.info("[Migration] Running migration '%s': %s", migration.id(), migration.description()); + Logger.info("[Migration] Upgrading from v%d to v%d", migration.fromVersion(), migration.toVersion()); + + // Create backup + Path backupPath = null; + try { + backupPath = createBackup(migration); + Logger.info("[Migration] Created backup at: %s", backupPath); + } catch (IOException e) { + Duration duration = Duration.between(startTime, Instant.now()); + Logger.severe("[Migration] Failed to create backup: %s", e.getMessage()); + return MigrationResult.failure( + migration.id(), migration.fromVersion(), migration.toVersion(), + null, "Failed to create backup: " + e.getMessage(), false, duration + ); + } + + // Execute migration + try { + MigrationOptions options = progressCallback != null + ? MigrationOptions.withProgress(backupPath, progressCallback) + : MigrationOptions.defaults(backupPath); + + MigrationResult result = migration.execute(dataDir, options); + + if (!result.success() && backupPath != null) { + try { + rollback(migration, backupPath); + Duration duration = Duration.between(startTime, Instant.now()); + return MigrationResult.failure( + migration.id(), migration.fromVersion(), migration.toVersion(), + backupPath, + result.errorMessage() != null ? result.errorMessage() : "Migration failed", + true, duration + ); + } catch (IOException rollbackError) { + Logger.severe("[Migration] Rollback failed: %s", rollbackError.getMessage()); + Duration duration = Duration.between(startTime, Instant.now()); + return MigrationResult.failure( + migration.id(), migration.fromVersion(), migration.toVersion(), + backupPath, + "Migration failed and rollback also failed: " + rollbackError.getMessage(), + false, duration + ); + } + } + + return result; + + } catch (Exception e) { + Logger.severe("[Migration] Migration threw exception: %s", e.getMessage()); + + boolean rolledBack = false; + if (backupPath != null) { + try { + rollback(migration, backupPath); + rolledBack = true; + } catch (IOException rollbackError) { + Logger.severe("[Migration] Rollback failed: %s", rollbackError.getMessage()); + } + } + + Duration duration = Duration.between(startTime, Instant.now()); + return MigrationResult.failure( + migration.id(), migration.fromVersion(), migration.toVersion(), + backupPath, e.getMessage(), rolledBack, duration + ); + } + } + + /** + * Creates a backup before running a migration. + * + *

Backup naming: backup_migration_v{from}-to-v{to}_{timestamp}.zip + *
Location: backups/ directory + */ + @NotNull + private Path createBackup(@NotNull Migration migration) throws IOException { + Path backupsDir = dataDir.resolve("backups"); + Files.createDirectories(backupsDir); + + String timestamp = BACKUP_TIMESTAMP.format(Instant.now()); + String fileName = String.format("backup_migration_v%d-to-v%d_%s.zip", + migration.fromVersion(), migration.toVersion(), timestamp); + Path backupFile = backupsDir.resolve(fileName); + + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(backupFile.toFile()))) { + switch (migration.type()) { + case CONFIG -> { + Path configFile = dataDir.resolve("config.json"); + if (Files.exists(configFile)) { + addFileToZip(zos, configFile, "config.json"); + } + Path configDir = dataDir.resolve("config"); + if (Files.exists(configDir) && Files.isDirectory(configDir)) { + addDirectoryToZip(zos, configDir, "config"); + } + } + case DATA -> { + Path dataSubDir = dataDir.resolve("data"); + if (Files.exists(dataSubDir) && Files.isDirectory(dataSubDir)) { + addDirectoryToZip(zos, dataSubDir, "data"); + } + } + case SCHEMA -> { + Logger.warn("[Migration] Schema migration backup not fully implemented"); + } + } + } + + Logger.info("[Migration] Created ZIP backup: %s", backupFile.getFileName()); + return backupFile; + } + + private void addFileToZip(@NotNull ZipOutputStream zos, @NotNull Path file, + @NotNull String entryName) throws IOException { + zos.putNextEntry(new ZipEntry(entryName)); + Files.copy(file, zos); + zos.closeEntry(); + } + + private void addDirectoryToZip(@NotNull ZipOutputStream zos, @NotNull Path dir, + @NotNull String zipPath) throws IOException { + Files.walkFileTree(dir, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + String entryName = zipPath + "/" + dir.relativize(file).toString().replace("\\", "/"); + addFileToZip(zos, file, entryName); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir2, BasicFileAttributes attrs) throws IOException { + String entryName = zipPath + "/" + dir.relativize(dir2).toString().replace("\\", "/") + "/"; + if (!entryName.equals(zipPath + "//")) { + zos.putNextEntry(new ZipEntry(entryName)); + zos.closeEntry(); + } + return FileVisitResult.CONTINUE; + } + }); + } + + /** Rolls back a failed migration by restoring from the ZIP backup. */ + private void rollback(@NotNull Migration migration, @NotNull Path backupPath) throws IOException { + Logger.info("[Migration] Rolling back migration '%s' from backup: %s", + migration.id(), backupPath.getFileName()); + + if (!Files.exists(backupPath) || !backupPath.toString().endsWith(".zip")) { + throw new IOException("Backup file not found or invalid: " + backupPath); + } + + try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(backupPath))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + Path targetPath = dataDir.resolve(entry.getName()); + if (entry.isDirectory()) { + Files.createDirectories(targetPath); + } else { + Files.createDirectories(targetPath.getParent()); + Files.copy(zis, targetPath, StandardCopyOption.REPLACE_EXISTING); + } + zis.closeEntry(); + } + } + + Logger.info("[Migration] Rollback complete - restored from backup"); + } + + /** Convenience static method to run pending migrations. */ + @NotNull + public static List runPendingMigrations( + @NotNull Path dataDir, @NotNull MigrationType type) { + return new MigrationRunner(dataDir).runPendingMigrations(type); + } +} diff --git a/src/main/java/com/hyperessentials/migration/MigrationType.java b/src/main/java/com/hyperessentials/migration/MigrationType.java new file mode 100644 index 0000000..d7d20b4 --- /dev/null +++ b/src/main/java/com/hyperessentials/migration/MigrationType.java @@ -0,0 +1,15 @@ +package com.hyperessentials.migration; + +/** + * Types of migrations supported by the migration framework. + */ +public enum MigrationType { + /** Configuration file migrations (config.json, module configs). */ + CONFIG, + + /** Data structure migrations (player data, warps, spawns). */ + DATA, + + /** Database schema migrations (for future SQL support). */ + SCHEMA +} diff --git a/src/main/java/com/hyperessentials/module/AbstractModule.java b/src/main/java/com/hyperessentials/module/AbstractModule.java index 9db5693..61e852d 100644 --- a/src/main/java/com/hyperessentials/module/AbstractModule.java +++ b/src/main/java/com/hyperessentials/module/AbstractModule.java @@ -1,37 +1,37 @@ -package com.hyperessentials.module; - -import com.hyperessentials.config.ModuleConfig; -import com.hyperessentials.util.Logger; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Base implementation of {@link Module} with common lifecycle logic. - */ -public abstract class AbstractModule implements Module { - - private boolean enabled = false; - - @Override - public boolean isEnabled() { - return enabled; - } - - @Override - public void onEnable() { - enabled = true; - Logger.info("[Module] %s enabled", getDisplayName()); - } - - @Override - public void onDisable() { - enabled = false; - Logger.info("[Module] %s disabled", getDisplayName()); - } - - @Override - @Nullable - public ModuleConfig getModuleConfig() { - return null; - } -} +package com.hyperessentials.module; + +import com.hyperessentials.config.ModuleConfig; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Base implementation of {@link Module} with common lifecycle logic. + */ +public abstract class AbstractModule implements Module { + + private boolean enabled = false; + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void onEnable() { + enabled = true; + Logger.info("[Module] %s enabled", getDisplayName()); + } + + @Override + public void onDisable() { + enabled = false; + Logger.info("[Module] %s disabled", getDisplayName()); + } + + @Override + @Nullable + public ModuleConfig getModuleConfig() { + return null; + } +} diff --git a/src/main/java/com/hyperessentials/module/Module.java b/src/main/java/com/hyperessentials/module/Module.java index 6a52b48..5134b1b 100644 --- a/src/main/java/com/hyperessentials/module/Module.java +++ b/src/main/java/com/hyperessentials/module/Module.java @@ -1,45 +1,45 @@ -package com.hyperessentials.module; - -import com.hyperessentials.config.ModuleConfig; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Interface for HyperEssentials modules. - * Each module represents a feature group that can be independently enabled/disabled. - */ -public interface Module { - - /** - * Gets the unique module name. - */ - @NotNull - String getName(); - - /** - * Gets the display name for UI. - */ - @NotNull - String getDisplayName(); - - /** - * Whether this module is currently enabled. - */ - boolean isEnabled(); - - /** - * Called when the module is enabled. - */ - void onEnable(); - - /** - * Called when the module is disabled. - */ - void onDisable(); - - /** - * Gets the module's configuration. - */ - @Nullable - ModuleConfig getModuleConfig(); -} +package com.hyperessentials.module; + +import com.hyperessentials.config.ModuleConfig; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Interface for HyperEssentials modules. + * Each module represents a feature group that can be independently enabled/disabled. + */ +public interface Module { + + /** + * Gets the unique module name. + */ + @NotNull + String getName(); + + /** + * Gets the display name for UI. + */ + @NotNull + String getDisplayName(); + + /** + * Whether this module is currently enabled. + */ + boolean isEnabled(); + + /** + * Called when the module is enabled. + */ + void onEnable(); + + /** + * Called when the module is disabled. + */ + void onDisable(); + + /** + * Gets the module's configuration. + */ + @Nullable + ModuleConfig getModuleConfig(); +} diff --git a/src/main/java/com/hyperessentials/module/ModuleRegistry.java b/src/main/java/com/hyperessentials/module/ModuleRegistry.java index da0d95a..b0db1de 100644 --- a/src/main/java/com/hyperessentials/module/ModuleRegistry.java +++ b/src/main/java/com/hyperessentials/module/ModuleRegistry.java @@ -1,108 +1,108 @@ -package com.hyperessentials.module; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.util.Logger; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Registry and lifecycle manager for all modules. - */ -public class ModuleRegistry { - - private final Map modules = new ConcurrentHashMap<>(); - private final List enableOrder = new ArrayList<>(); - - /** - * Registers a module. - */ - public void register(@NotNull Module module) { - modules.put(module.getName(), module); - enableOrder.add(module.getName()); - Logger.debug("[ModuleRegistry] Registered module: %s", module.getName()); - } - - /** - * Enables all modules that are configured as enabled. - */ - public void enableAll() { - ConfigManager config = ConfigManager.get(); - int enabled = 0; - - for (String name : enableOrder) { - Module module = modules.get(name); - if (module != null && config.isModuleEnabled(name)) { - try { - module.onEnable(); - enabled++; - } catch (Exception e) { - Logger.severe("[ModuleRegistry] Failed to enable module %s: %s", name, e.getMessage()); - } - } - } - - Logger.info("[ModuleRegistry] Enabled %d/%d modules", enabled, modules.size()); - } - - /** - * Disables all enabled modules in reverse order. - */ - public void disableAll() { - List reversed = new ArrayList<>(enableOrder); - Collections.reverse(reversed); - - for (String name : reversed) { - Module module = modules.get(name); - if (module != null && module.isEnabled()) { - try { - module.onDisable(); - } catch (Exception e) { - Logger.severe("[ModuleRegistry] Failed to disable module %s: %s", name, e.getMessage()); - } - } - } - } - - /** - * Gets a module by name. - */ - @Nullable - public Module getModule(@NotNull String name) { - return modules.get(name); - } - - /** - * Gets a module by class. - */ - @SuppressWarnings("unchecked") - @Nullable - public T getModule(@NotNull Class clazz) { - for (Module module : modules.values()) { - if (clazz.isInstance(module)) { - return (T) module; - } - } - return null; - } - - /** - * Gets all registered modules. - */ - @NotNull - public Collection getModules() { - return Collections.unmodifiableCollection(modules.values()); - } - - /** - * Gets all enabled modules. - */ - @NotNull - public List getEnabledModules() { - return modules.values().stream() - .filter(Module::isEnabled) - .toList(); - } -} +package com.hyperessentials.module; + +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Registry and lifecycle manager for all modules. + */ +public class ModuleRegistry { + + private final Map modules = new ConcurrentHashMap<>(); + private final List enableOrder = new ArrayList<>(); + + /** + * Registers a module. + */ + public void register(@NotNull Module module) { + modules.put(module.getName(), module); + enableOrder.add(module.getName()); + Logger.debug("[ModuleRegistry] Registered module: %s", module.getName()); + } + + /** + * Enables all modules that are configured as enabled. + */ + public void enableAll() { + ConfigManager config = ConfigManager.get(); + int enabled = 0; + + for (String name : enableOrder) { + Module module = modules.get(name); + if (module != null && config.isModuleEnabled(name)) { + try { + module.onEnable(); + enabled++; + } catch (Exception e) { + Logger.severe("[ModuleRegistry] Failed to enable module %s: %s", name, e.getMessage()); + } + } + } + + Logger.info("[ModuleRegistry] Enabled %d/%d modules", enabled, modules.size()); + } + + /** + * Disables all enabled modules in reverse order. + */ + public void disableAll() { + List reversed = new ArrayList<>(enableOrder); + Collections.reverse(reversed); + + for (String name : reversed) { + Module module = modules.get(name); + if (module != null && module.isEnabled()) { + try { + module.onDisable(); + } catch (Exception e) { + Logger.severe("[ModuleRegistry] Failed to disable module %s: %s", name, e.getMessage()); + } + } + } + } + + /** + * Gets a module by name. + */ + @Nullable + public Module getModule(@NotNull String name) { + return modules.get(name); + } + + /** + * Gets a module by class. + */ + @SuppressWarnings("unchecked") + @Nullable + public T getModule(@NotNull Class clazz) { + for (Module module : modules.values()) { + if (clazz.isInstance(module)) { + return (T) module; + } + } + return null; + } + + /** + * Gets all registered modules. + */ + @NotNull + public Collection getModules() { + return Collections.unmodifiableCollection(modules.values()); + } + + /** + * Gets all enabled modules. + */ + @NotNull + public List getEnabledModules() { + return modules.values().stream() + .filter(Module::isEnabled) + .toList(); + } +} diff --git a/src/main/java/com/hyperessentials/module/announcements/AnnouncementScheduler.java b/src/main/java/com/hyperessentials/module/announcements/AnnouncementScheduler.java new file mode 100644 index 0000000..9bdb0fb --- /dev/null +++ b/src/main/java/com/hyperessentials/module/announcements/AnnouncementScheduler.java @@ -0,0 +1,98 @@ +package com.hyperessentials.module.announcements; + +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.modules.AnnouncementsConfig; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.server.core.HytaleServer; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Schedules periodic broadcast announcements. + */ +public class AnnouncementScheduler { + + private final AtomicInteger currentIndex = new AtomicInteger(0); + private final Random random = new Random(); + private ScheduledFuture task; + + public void start() { + int intervalSeconds = ConfigManager.get().announcements().getIntervalSeconds(); + task = HytaleServer.SCHEDULED_EXECUTOR.scheduleAtFixedRate( + this::broadcastNext, intervalSeconds, intervalSeconds, TimeUnit.SECONDS + ); + Logger.info("[Announcements] Scheduler started (interval: %ds)", intervalSeconds); + } + + public void shutdown() { + if (task != null) { + task.cancel(false); + task = null; + } + } + + public void restart() { + shutdown(); + start(); + } + + private void broadcastNext() { + try { + AnnouncementsConfig config = ConfigManager.get().announcements(); + List messages = config.getMessages(); + + if (messages.isEmpty()) return; + + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null || plugin.getTrackedPlayers().isEmpty()) return; + + String text; + if (config.isRandomize()) { + text = messages.get(random.nextInt(messages.size())); + } else { + int idx = currentIndex.getAndUpdate(i -> (i + 1) % messages.size()); + text = messages.get(idx); + } + + Message announcement = buildAnnouncement(text, config); + + for (PlayerRef player : plugin.getTrackedPlayers().values()) { + player.sendMessage(announcement); + } + } catch (Exception e) { + Logger.warn("[Announcements] Error broadcasting: %s", e.getMessage()); + } + } + + /** + * Broadcasts a single message to all players immediately. + */ + public void broadcastNow(@NotNull String text) { + AnnouncementsConfig config = ConfigManager.get().announcements(); + Message announcement = buildAnnouncement(text, config); + + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null) return; + + for (PlayerRef player : plugin.getTrackedPlayers().values()) { + player.sendMessage(announcement); + } + } + + @NotNull + private Message buildAnnouncement(@NotNull String text, @NotNull AnnouncementsConfig config) { + return Message.raw("[").color(CommandUtil.COLOR_DARK_GRAY) + .insert(Message.raw(config.getPrefixText()).color(config.getPrefixColor())) + .insert(Message.raw("] ").color(CommandUtil.COLOR_DARK_GRAY)) + .insert(Message.raw(text).color(config.getMessageColor())); + } +} diff --git a/src/main/java/com/hyperessentials/module/announcements/AnnouncementsModule.java b/src/main/java/com/hyperessentials/module/announcements/AnnouncementsModule.java index 6e4bccc..9e16661 100644 --- a/src/main/java/com/hyperessentials/module/announcements/AnnouncementsModule.java +++ b/src/main/java/com/hyperessentials/module/announcements/AnnouncementsModule.java @@ -1,43 +1,71 @@ -package com.hyperessentials.module.announcements; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.config.ModuleConfig; -import com.hyperessentials.module.AbstractModule; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Announcements module for HyperEssentials. - */ -public class AnnouncementsModule extends AbstractModule { - - @Override - @NotNull - public String getName() { - return "announcements"; - } - - @Override - @NotNull - public String getDisplayName() { - return "Announcements"; - } - - @Override - public void onEnable() { - super.onEnable(); - // TODO: Register commands, listeners, and storage - } - - @Override - public void onDisable() { - // TODO: Unregister commands, save data, cleanup - super.onDisable(); - } - - @Override - @Nullable - public ModuleConfig getModuleConfig() { - return ConfigManager.get().announcements(); - } -} +package com.hyperessentials.module.announcements; + +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.ModuleConfig; +import com.hyperessentials.module.AbstractModule; +import com.hyperessentials.module.announcements.command.AnnounceCommand; +import com.hyperessentials.module.announcements.command.BroadcastCommand; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Announcements module for HyperEssentials. + * Provides scheduled auto-broadcast rotation and manual /broadcast. + */ +public class AnnouncementsModule extends AbstractModule { + + private AnnouncementScheduler scheduler; + + @Override + @NotNull + public String getName() { + return "announcements"; + } + + @Override + @NotNull + public String getDisplayName() { + return "Announcements"; + } + + @Override + public void onEnable() { + super.onEnable(); + + scheduler = new AnnouncementScheduler(); + scheduler.start(); + + // Register commands + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin != null) { + try { + plugin.getCommandRegistry().registerCommand(new BroadcastCommand(this)); + plugin.getCommandRegistry().registerCommand(new AnnounceCommand(this)); + Logger.info("[Announcements] Registered commands: /broadcast, /announce"); + } catch (Exception e) { + Logger.severe("[Announcements] Failed to register commands: %s", e.getMessage()); + } + } + } + + @Override + public void onDisable() { + if (scheduler != null) { + scheduler.shutdown(); + } + super.onDisable(); + } + + @NotNull + public AnnouncementScheduler getScheduler() { + return scheduler; + } + + @Override + @Nullable + public ModuleConfig getModuleConfig() { + return ConfigManager.get().announcements(); + } +} diff --git a/src/main/java/com/hyperessentials/module/announcements/command/AnnounceCommand.java b/src/main/java/com/hyperessentials/module/announcements/command/AnnounceCommand.java new file mode 100644 index 0000000..5f24740 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/announcements/command/AnnounceCommand.java @@ -0,0 +1,139 @@ +package com.hyperessentials.module.announcements.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.modules.AnnouncementsConfig; +import com.hyperessentials.module.announcements.AnnouncementsModule; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * /announce list|add|remove|reload - Manage announcement rotation. + */ +public class AnnounceCommand extends AbstractPlayerCommand { + + private final AnnouncementsModule module; + + public AnnounceCommand(@NotNull AnnouncementsModule module) { + super("announce", "Manage announcements"); + this.module = module; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.ANNOUNCE_MANAGE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to manage announcements.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + showHelp(ctx); + return; + } + + String subcommand = parts[1].toLowerCase(); + + switch (subcommand) { + case "list" -> handleList(ctx); + case "add" -> handleAdd(ctx, parts); + case "remove" -> handleRemove(ctx, parts); + case "reload" -> handleReload(ctx); + default -> showHelp(ctx); + } + } + + private void handleList(@NotNull CommandContext ctx) { + List messages = ConfigManager.get().announcements().getMessages(); + + if (messages.isEmpty()) { + ctx.sendMessage(CommandUtil.info("No announcements configured.")); + return; + } + + ctx.sendMessage(CommandUtil.info("Announcements (" + messages.size() + "):")); + for (int i = 0; i < messages.size(); i++) { + Message line = CommandUtil.prefix() + .insert(Message.raw(" " + (i + 1) + ". ").color(CommandUtil.COLOR_GOLD)) + .insert(Message.raw(messages.get(i)).color(CommandUtil.COLOR_WHITE)); + ctx.sendMessage(line); + } + } + + private void handleAdd(@NotNull CommandContext ctx, String[] parts) { + if (parts.length < 3) { + ctx.sendMessage(CommandUtil.error("Usage: /announce add ")); + return; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 2; i < parts.length; i++) { + if (i > 2) sb.append(' '); + sb.append(parts[i]); + } + String message = sb.toString(); + + AnnouncementsConfig config = ConfigManager.get().announcements(); + config.getMessages().add(message); + config.save(); + + ctx.sendMessage(CommandUtil.success("Added announcement: " + message)); + } + + private void handleRemove(@NotNull CommandContext ctx, String[] parts) { + if (parts.length < 3) { + ctx.sendMessage(CommandUtil.error("Usage: /announce remove ")); + return; + } + + int index; + try { + index = Integer.parseInt(parts[2]) - 1; + } catch (NumberFormatException e) { + ctx.sendMessage(CommandUtil.error("Invalid index.")); + return; + } + + List messages = ConfigManager.get().announcements().getMessages(); + if (index < 0 || index >= messages.size()) { + ctx.sendMessage(CommandUtil.error("Index out of range (1-" + messages.size() + ").")); + return; + } + + String removed = messages.remove(index); + ConfigManager.get().announcements().save(); + + ctx.sendMessage(CommandUtil.success("Removed announcement: " + removed)); + } + + private void handleReload(@NotNull CommandContext ctx) { + ConfigManager.get().announcements().reload(); + module.getScheduler().restart(); + ctx.sendMessage(CommandUtil.success("Announcements reloaded.")); + } + + private void showHelp(@NotNull CommandContext ctx) { + ctx.sendMessage(CommandUtil.info("Announcement Commands:")); + ctx.sendMessage(CommandUtil.msg(" /announce list - List announcements", CommandUtil.COLOR_GRAY)); + ctx.sendMessage(CommandUtil.msg(" /announce add - Add announcement", CommandUtil.COLOR_GRAY)); + ctx.sendMessage(CommandUtil.msg(" /announce remove - Remove announcement", CommandUtil.COLOR_GRAY)); + ctx.sendMessage(CommandUtil.msg(" /announce reload - Reload announcements", CommandUtil.COLOR_GRAY)); + } +} diff --git a/src/main/java/com/hyperessentials/module/announcements/command/BroadcastCommand.java b/src/main/java/com/hyperessentials/module/announcements/command/BroadcastCommand.java new file mode 100644 index 0000000..4d315e5 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/announcements/command/BroadcastCommand.java @@ -0,0 +1,63 @@ +package com.hyperessentials.module.announcements.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.announcements.AnnouncementsModule; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /broadcast - Send an immediate broadcast to all players. + */ +public class BroadcastCommand extends AbstractPlayerCommand { + + private final AnnouncementsModule module; + + public BroadcastCommand(@NotNull AnnouncementsModule module) { + super("broadcast", "Broadcast a message to all players"); + this.module = module; + addAliases("bc"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.ANNOUNCE_BROADCAST)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to broadcast.")); + return; + } + + String input = ctx.getInputString(); + if (input == null || input.isBlank()) { + ctx.sendMessage(CommandUtil.error("Usage: /broadcast ")); + return; + } + + // Remove command name from input + String message = input.trim(); + int spaceIdx = message.indexOf(' '); + if (spaceIdx < 0) { + ctx.sendMessage(CommandUtil.error("Usage: /broadcast ")); + return; + } + message = message.substring(spaceIdx + 1).trim(); + + if (message.isEmpty()) { + ctx.sendMessage(CommandUtil.error("Usage: /broadcast ")); + return; + } + + module.getScheduler().broadcastNow(message); + ctx.sendMessage(CommandUtil.success("Broadcast sent.")); + } +} diff --git a/src/main/java/com/hyperessentials/module/homes/HomeManager.java b/src/main/java/com/hyperessentials/module/homes/HomeManager.java new file mode 100644 index 0000000..9b03707 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/homes/HomeManager.java @@ -0,0 +1,203 @@ +package com.hyperessentials.module.homes; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.modules.HomesConfig; +import com.hyperessentials.data.Home; +import com.hyperessentials.data.PlayerHomes; +import com.hyperessentials.storage.HomeStorage; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages player homes - loading, saving, CRUD operations, and limit enforcement. + */ +public class HomeManager { + + private final HomeStorage storage; + private final Map cache = new ConcurrentHashMap<>(); + + public HomeManager(@NotNull HomeStorage storage) { + this.storage = storage; + } + + // ========== Lifecycle ========== + + /** + * Loads a player's homes from storage into the cache. + * Called on player connect. + */ + public CompletableFuture loadPlayer(@NotNull UUID uuid, @NotNull String username) { + return storage.loadPlayerHomes(uuid).thenAccept(opt -> { + PlayerHomes homes = opt.orElseGet(() -> new PlayerHomes(uuid, username)); + homes.setUsername(username); + cache.put(uuid, homes); + Logger.debug("[Homes] Loaded %d homes for %s", homes.count(), username); + }); + } + + /** + * Saves a player's homes and removes from cache. + * Called on player disconnect. + */ + public CompletableFuture unloadPlayer(@NotNull UUID uuid) { + PlayerHomes homes = cache.remove(uuid); + if (homes != null) { + return storage.savePlayerHomes(homes); + } + return CompletableFuture.completedFuture(null); + } + + /** + * Saves a player's homes without removing from cache. + */ + public CompletableFuture savePlayer(@NotNull UUID uuid) { + PlayerHomes homes = cache.get(uuid); + if (homes != null) { + return storage.savePlayerHomes(homes); + } + return CompletableFuture.completedFuture(null); + } + + /** + * Saves all cached players to storage. Called on shutdown. + */ + public CompletableFuture saveAll() { + CompletableFuture[] futures = cache.values().stream() + .map(storage::savePlayerHomes) + .toArray(CompletableFuture[]::new); + return CompletableFuture.allOf(futures); + } + + // ========== CRUD ========== + + /** + * Sets a home for a player. Checks home limit unless bypassed. + * + * @return true if the home was set, false if at limit + */ + public boolean setHome(@NotNull UUID uuid, @NotNull Home home) { + PlayerHomes homes = cache.get(uuid); + if (homes == null) return false; + + // If home already exists (overwrite), always allow + if (!homes.hasHome(home.name())) { + // New home — check limit + if (isAtLimit(uuid)) { + return false; + } + } + + homes.setHome(home); + savePlayer(uuid); + Logger.debug("[Homes] Set home '%s' for %s", home.name(), uuid); + return true; + } + + /** + * Deletes a home for a player. + * + * @return true if the home existed and was deleted + */ + public boolean deleteHome(@NotNull UUID uuid, @NotNull String name) { + PlayerHomes homes = cache.get(uuid); + if (homes == null) return false; + + boolean removed = homes.removeHome(name); + if (removed) { + savePlayer(uuid); + Logger.debug("[Homes] Deleted home '%s' for %s", name, uuid); + } + return removed; + } + + /** + * Gets a specific home (case-insensitive). + */ + @Nullable + public Home getHome(@NotNull UUID uuid, @NotNull String name) { + PlayerHomes homes = cache.get(uuid); + return homes != null ? homes.getHome(name) : null; + } + + /** + * Gets all homes for a player. + */ + @NotNull + public Collection getHomes(@NotNull UUID uuid) { + PlayerHomes homes = cache.get(uuid); + return homes != null ? homes.getHomes() : Collections.emptyList(); + } + + // ========== Limits ========== + + /** + * Gets the home limit for a player. + * Checks: unlimited perm → bypass.limit perm → home.limit.N perm → config default. + * + * @return the limit, or -1 for unlimited + */ + public int getHomeLimit(@NotNull UUID uuid) { + if (CommandUtil.hasPermission(uuid, Permissions.HOME_UNLIMITED)) { + return -1; + } + if (CommandUtil.hasPermission(uuid, Permissions.BYPASS_LIMIT)) { + return -1; + } + + int permLimit = CommandUtil.getPermissionValue(uuid, Permissions.HOME_LIMIT_PREFIX, -1); + if (permLimit > 0) { + return permLimit; + } + + return ConfigManager.get().homes().getDefaultHomeLimit(); + } + + /** + * Checks if a player is at their home limit. + */ + public boolean isAtLimit(@NotNull UUID uuid) { + int limit = getHomeLimit(uuid); + if (limit < 0) return false; // unlimited + PlayerHomes homes = cache.get(uuid); + return homes != null && homes.count() >= limit; + } + + /** + * Gets the current home count for a player. + */ + public int getHomeCount(@NotNull UUID uuid) { + PlayerHomes homes = cache.get(uuid); + return homes != null ? homes.count() : 0; + } + + // ========== Bed Sync ========== + + /** + * Syncs a bed location as a home, bypassing the home limit. + * Uses the configured bed home name. + */ + public void syncBedHome(@NotNull UUID uuid, @NotNull String world, + double x, double y, double z, float yaw, float pitch) { + PlayerHomes homes = cache.get(uuid); + if (homes == null) return; + + HomesConfig config = ConfigManager.get().homes(); + if (!config.isBedSyncEnabled()) return; + + String bedName = config.getBedHomeName(); + Home bedHome = Home.create(bedName, world, x, y, z, yaw, pitch); + homes.setHome(bedHome); + savePlayer(uuid); + Logger.debug("[Homes] Synced bed home for %s at %.0f, %.0f, %.0f", uuid, x, y, z); + } +} diff --git a/src/main/java/com/hyperessentials/module/homes/HomesModule.java b/src/main/java/com/hyperessentials/module/homes/HomesModule.java index 6f93da0..b4f8451 100644 --- a/src/main/java/com/hyperessentials/module/homes/HomesModule.java +++ b/src/main/java/com/hyperessentials/module/homes/HomesModule.java @@ -1,43 +1,62 @@ -package com.hyperessentials.module.homes; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.config.ModuleConfig; -import com.hyperessentials.module.AbstractModule; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Homes module for HyperEssentials. - */ -public class HomesModule extends AbstractModule { - - @Override - @NotNull - public String getName() { - return "homes"; - } - - @Override - @NotNull - public String getDisplayName() { - return "Homes"; - } - - @Override - public void onEnable() { - super.onEnable(); - // TODO: Register commands, listeners, and storage - } - - @Override - public void onDisable() { - // TODO: Unregister commands, save data, cleanup - super.onDisable(); - } - - @Override - @Nullable - public ModuleConfig getModuleConfig() { - return ConfigManager.get().homes(); - } -} +package com.hyperessentials.module.homes; + +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.ModuleConfig; +import com.hyperessentials.module.AbstractModule; +import com.hyperessentials.storage.HomeStorage; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Homes module for HyperEssentials. + */ +public class HomesModule extends AbstractModule { + + private HomeManager homeManager; + + @Override + @NotNull + public String getName() { + return "homes"; + } + + @Override + @NotNull + public String getDisplayName() { + return "Homes"; + } + + @Override + public void onEnable() { + super.onEnable(); + } + + /** + * Initializes the home manager with the given storage. + * Called by HyperEssentials after module is enabled. + */ + public void initManager(@NotNull HomeStorage storage) { + this.homeManager = new HomeManager(storage); + Logger.info("[Homes] HomeManager initialized"); + } + + @Override + public void onDisable() { + if (homeManager != null) { + homeManager.saveAll().join(); + } + super.onDisable(); + } + + @Nullable + public HomeManager getHomeManager() { + return homeManager; + } + + @Override + @Nullable + public ModuleConfig getModuleConfig() { + return ConfigManager.get().homes(); + } +} diff --git a/src/main/java/com/hyperessentials/module/homes/command/DelHomeCommand.java b/src/main/java/com/hyperessentials/module/homes/command/DelHomeCommand.java new file mode 100644 index 0000000..f08a82a --- /dev/null +++ b/src/main/java/com/hyperessentials/module/homes/command/DelHomeCommand.java @@ -0,0 +1,72 @@ +package com.hyperessentials.module.homes.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.Home; +import com.hyperessentials.module.homes.HomeManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.UUID; + +/** + * /delhome <name> - Delete a home. + */ +public class DelHomeCommand extends AbstractPlayerCommand { + + private final HomeManager homeManager; + + public DelHomeCommand(@NotNull HomeManager homeManager) { + super("delhome", "Delete a home"); + this.homeManager = homeManager; + addAliases("deletehome", "rmhome", "removehome"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.HOME_DELETE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to delete homes.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /delhome ")); + Collection homes = homeManager.getHomes(uuid); + if (!homes.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (Home h : homes) { + if (!sb.isEmpty()) sb.append(", "); + sb.append(h.name()); + } + ctx.sendMessage(CommandUtil.info("Your homes: " + sb)); + } + return; + } + + String homeName = parts[1]; + + if (homeManager.deleteHome(uuid, homeName)) { + ctx.sendMessage(CommandUtil.success("Home '" + homeName + "' has been deleted.")); + } else { + ctx.sendMessage(CommandUtil.error("Home '" + homeName + "' not found.")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/homes/command/HomeCommand.java b/src/main/java/com/hyperessentials/module/homes/command/HomeCommand.java new file mode 100644 index 0000000..d61b764 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/homes/command/HomeCommand.java @@ -0,0 +1,137 @@ +package com.hyperessentials.module.homes.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.Home; +import com.hyperessentials.data.Location; +import com.hyperessentials.integration.FactionTerritoryChecker; +import com.hyperessentials.module.homes.HomeManager; +import com.hyperessentials.module.warmup.WarmupManager; +import com.hyperessentials.module.warmup.WarmupTask; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.UUID; + +/** + * /home [name] - Teleport to a home. + */ +public class HomeCommand extends AbstractPlayerCommand { + + private final HomeManager homeManager; + private final WarmupManager warmupManager; + + public HomeCommand(@NotNull HomeManager homeManager, @NotNull WarmupManager warmupManager) { + super("home", "Teleport to a home"); + this.homeManager = homeManager; + this.warmupManager = warmupManager; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.HOME_TELEPORT)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to use homes.")); + return; + } + + // Parse home name (default: "home") + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + String homeName = parts.length >= 2 ? parts[1] : "home"; + + // No args and no homes — hint + if (parts.length < 2) { + Collection homes = homeManager.getHomes(uuid); + if (homes.isEmpty()) { + ctx.sendMessage(CommandUtil.info("You don't have any homes. Use /sethome to create one.")); + return; + } + } + + // Resolve home + Home home = homeManager.getHome(uuid, homeName); + if (home == null) { + ctx.sendMessage(CommandUtil.error("Home '" + homeName + "' not found.")); + Collection homes = homeManager.getHomes(uuid); + if (!homes.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (Home h : homes) { + if (!sb.isEmpty()) sb.append(", "); + sb.append(h.name()); + } + ctx.sendMessage(CommandUtil.info("Your homes: " + sb)); + } + return; + } + + // Faction territory check on DESTINATION (unless bypassed) + if (!CommandUtil.hasPermission(uuid, Permissions.BYPASS_FACTIONS_HOME) + && !CommandUtil.hasPermission(uuid, Permissions.BYPASS_FACTIONS)) { + FactionTerritoryChecker.Result result = FactionTerritoryChecker.canUseHome( + uuid, home.world(), home.x(), home.z()); + if (result != FactionTerritoryChecker.Result.ALLOWED) { + ctx.sendMessage(CommandUtil.error(result.getDenialMessage())); + return; + } + } + + // Check cooldown + if (warmupManager.isOnCooldown(uuid, "homes", "home")) { + int remaining = warmupManager.getRemainingCooldown(uuid, "homes", "home"); + ctx.sendMessage(CommandUtil.error("On cooldown. " + remaining + "s remaining.")); + return; + } + + Location destination = Location.fromHome(home); + + WarmupTask task = warmupManager.startWarmup(uuid, "homes", "home", () -> { + executeTeleport(ref, destination, () -> { + ctx.sendMessage(CommandUtil.success("Teleported to home '" + home.name() + "'!")); + }); + }); + + if (task != null) { + ctx.sendMessage(CommandUtil.info( + "Teleporting in " + task.warmupSeconds() + "s... Don't move!")); + } + } + + private void executeTeleport(Ref ref, Location dest, Runnable onComplete) { + World targetWorld = Universe.get().getWorld(dest.world()); + if (targetWorld == null) { + return; + } + targetWorld.execute(() -> { + if (!ref.isValid()) { + return; + } + Store store = ref.getStore(); + Vector3d position = new Vector3d(dest.x(), dest.y(), dest.z()); + Vector3f rotation = new Vector3f(dest.pitch(), dest.yaw(), 0); + Teleport teleport = Teleport.createForPlayer(targetWorld, position, rotation); + store.addComponent(ref, Teleport.getComponentType(), teleport); + if (onComplete != null) { + onComplete.run(); + } + }); + } +} diff --git a/src/main/java/com/hyperessentials/module/homes/command/HomesCommand.java b/src/main/java/com/hyperessentials/module/homes/command/HomesCommand.java new file mode 100644 index 0000000..4d147c3 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/homes/command/HomesCommand.java @@ -0,0 +1,91 @@ +package com.hyperessentials.module.homes.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.api.HyperEssentialsAPI; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.Home; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.module.homes.HomeManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.UUID; + +/** + * /homes - List all homes with count and limit. + * Opens GUI page when available, falls back to text list. + */ +public class HomesCommand extends AbstractPlayerCommand { + + private final HomeManager homeManager; + + public HomesCommand(@NotNull HomeManager homeManager) { + super("homes", "List your homes"); + this.homeManager = homeManager; + addAliases("listhomes", "homelist"); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.HOME_LIST)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to list homes.")); + return; + } + + // Try GUI page first + if (tryOpenGui(store, ref, playerRef)) { + return; + } + + // Text fallback + Collection homes = homeManager.getHomes(uuid); + int count = homes.size(); + int limit = homeManager.getHomeLimit(uuid); + + String limitStr = limit < 0 ? "unlimited" : String.valueOf(limit); + + if (homes.isEmpty()) { + ctx.sendMessage(CommandUtil.info( + "You don't have any homes (0/" + limitStr + "). Use /sethome to create one.")); + return; + } + + ctx.sendMessage(CommandUtil.info("Your homes (" + count + "/" + limitStr + "):")); + + StringBuilder sb = new StringBuilder(); + for (Home home : homes) { + if (!sb.isEmpty()) sb.append(", "); + sb.append(home.name()); + } + ctx.sendMessage(CommandUtil.msg(sb.toString(), CommandUtil.COLOR_GRAY)); + } + + private boolean tryOpenGui(@NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef) { + if (!HyperEssentialsAPI.isAvailable()) return false; + + GuiManager guiManager = HyperEssentialsAPI.getInstance().getGuiManager(); + if (guiManager.getPlayerRegistry().getEntry("homes") == null) return false; + + Player player = store.getComponent(ref, Player.getComponentType()); + if (player == null) return false; + + return guiManager.openPlayerPage("homes", player, ref, store, playerRef); + } +} diff --git a/src/main/java/com/hyperessentials/module/homes/command/SetHomeCommand.java b/src/main/java/com/hyperessentials/module/homes/command/SetHomeCommand.java new file mode 100644 index 0000000..6fbbceb --- /dev/null +++ b/src/main/java/com/hyperessentials/module/homes/command/SetHomeCommand.java @@ -0,0 +1,105 @@ +package com.hyperessentials.module.homes.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.Home; +import com.hyperessentials.integration.FactionTerritoryChecker; +import com.hyperessentials.module.homes.HomeManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; +import java.util.regex.Pattern; + +/** + * /sethome [name] - Set a home at your current location. + */ +public class SetHomeCommand extends AbstractPlayerCommand { + + private static final Pattern NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]{1,32}$"); + + private final HomeManager homeManager; + + public SetHomeCommand(@NotNull HomeManager homeManager) { + super("sethome", "Set a home at your current location"); + this.homeManager = homeManager; + addAliases("createhome"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.HOME_SET)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to set homes.")); + return; + } + + // Parse home name (default: "home") + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + String homeName = parts.length >= 2 ? parts[1] : "home"; + + // Validate name + if (!NAME_PATTERN.matcher(homeName).matches()) { + ctx.sendMessage(CommandUtil.error( + "Home name must be 1-32 characters (letters, numbers, _ and - only).")); + return; + } + + // Get player position + TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType()); + if (transform == null) { + ctx.sendMessage(CommandUtil.error("Could not get your position.")); + return; + } + + Vector3d pos = transform.getPosition(); + Vector3f rot = transform.getRotation(); + + // Faction territory check (unless bypassed) + if (!CommandUtil.hasPermission(uuid, Permissions.BYPASS_FACTIONS_SETHOME) + && !CommandUtil.hasPermission(uuid, Permissions.BYPASS_FACTIONS)) { + FactionTerritoryChecker.Result result = FactionTerritoryChecker.canUseHome( + uuid, currentWorld.getName(), pos.getX(), pos.getZ()); + if (result != FactionTerritoryChecker.Result.ALLOWED) { + ctx.sendMessage(CommandUtil.error(result.getDenialMessage())); + return; + } + } + + // Check limit (overwriting existing home is always allowed) + boolean isNew = homeManager.getHome(uuid, homeName) == null; + if (isNew && homeManager.isAtLimit(uuid)) { + int limit = homeManager.getHomeLimit(uuid); + ctx.sendMessage(CommandUtil.error("You have reached your home limit (" + limit + ").")); + return; + } + + Home home = Home.create(homeName, currentWorld.getName(), + pos.getX(), pos.getY(), pos.getZ(), + rot.getY(), rot.getX()); + + homeManager.setHome(uuid, home); + + String action = isNew ? "set" : "updated"; + ctx.sendMessage(CommandUtil.success("Home '" + homeName + "' has been " + action + "!")); + ctx.sendMessage(CommandUtil.info(String.format("Location: %.0f, %.0f, %.0f in %s", + pos.getX(), pos.getY(), pos.getZ(), currentWorld.getName()))); + } +} diff --git a/src/main/java/com/hyperessentials/module/kits/KitManager.java b/src/main/java/com/hyperessentials/module/kits/KitManager.java new file mode 100644 index 0000000..4eb31c6 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/kits/KitManager.java @@ -0,0 +1,374 @@ +package com.hyperessentials.module.kits; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.module.kits.data.Kit; +import com.hyperessentials.module.kits.data.KitItem; +import com.hyperessentials.module.kits.storage.KitStorage; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.inventory.container.ItemContainer; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages kit operations: CRUD, cooldowns, claiming. + * Supports hotbar, storage, armor, and utility inventory sections. + */ +public class KitManager { + + public enum ClaimResult { + SUCCESS, + ON_COOLDOWN, + ALREADY_CLAIMED, + NO_PERMISSION, + KIT_NOT_FOUND, + INSUFFICIENT_SPACE + } + + private final KitStorage storage; + private final Map cooldowns = new ConcurrentHashMap<>(); + private final Set oneTimeClaims = ConcurrentHashMap.newKeySet(); + + public KitManager(@NotNull KitStorage storage) { + this.storage = storage; + } + + @Nullable + public Kit getKit(@NotNull String name) { + return storage.getKit(name); + } + + @NotNull + public Collection getAllKits() { + return storage.getKits().values(); + } + + @NotNull + public List getAvailableKits(@NotNull UUID playerUuid) { + List available = new ArrayList<>(); + for (Kit kit : getAllKits()) { + if (canUseKit(playerUuid, kit)) { + available.add(kit); + } + } + return available; + } + + public boolean canUseKit(@NotNull UUID playerUuid, @NotNull Kit kit) { + return CommandUtil.hasPermission(playerUuid, Permissions.KIT_USE) + || CommandUtil.hasPermission(playerUuid, kit.getEffectivePermission()); + } + + @NotNull + public ClaimResult claimKit(@NotNull UUID playerUuid, @NotNull PlayerRef playerRef, + @NotNull Store store, @NotNull Ref ref, + @NotNull Kit kit) { + // Permission check + if (!canUseKit(playerUuid, kit)) { + return ClaimResult.NO_PERMISSION; + } + + // One-time check + if (kit.oneTime()) { + String key = playerUuid + ":" + kit.name(); + if (oneTimeClaims.contains(key)) { + return ClaimResult.ALREADY_CLAIMED; + } + } + + // Cooldown check + if (!CommandUtil.hasPermission(playerUuid, Permissions.BYPASS_KIT_COOLDOWN)) { + if (isOnCooldown(playerUuid, kit.name())) { + return ClaimResult.ON_COOLDOWN; + } + } + + // Space check before giving items + Player playerComponent = store.getComponent(ref, Player.getComponentType()); + if (playerComponent == null) { + Logger.warn("[KitManager] Player component not found"); + return ClaimResult.KIT_NOT_FOUND; + } + + if (!hasSpaceForKit(playerComponent.getInventory(), kit)) { + return ClaimResult.INSUFFICIENT_SPACE; + } + + // Give items + giveItems(playerComponent.getInventory(), kit); + + // Track cooldown + if (kit.cooldownSeconds() > 0) { + String cooldownKey = playerUuid + ":" + kit.name(); + cooldowns.put(cooldownKey, System.currentTimeMillis() + (kit.cooldownSeconds() * 1000L)); + } + + // Track one-time + if (kit.oneTime()) { + oneTimeClaims.add(playerUuid + ":" + kit.name()); + } + + return ClaimResult.SUCCESS; + } + + public boolean isOnCooldown(@NotNull UUID playerUuid, @NotNull String kitName) { + String key = playerUuid + ":" + kitName; + Long expiry = cooldowns.get(key); + if (expiry == null) return false; + if (System.currentTimeMillis() >= expiry) { + cooldowns.remove(key); + return false; + } + return true; + } + + public long getRemainingCooldown(@NotNull UUID playerUuid, @NotNull String kitName) { + String key = playerUuid + ":" + kitName; + Long expiry = cooldowns.get(key); + if (expiry == null) return 0; + long remaining = expiry - System.currentTimeMillis(); + return remaining > 0 ? remaining : 0; + } + + @NotNull + public Kit captureFromInventory(@NotNull PlayerRef playerRef, @NotNull Store store, + @NotNull Ref ref, @NotNull String kitName) { + List items = new ArrayList<>(); + + try { + Player playerComponent = store.getComponent(ref, Player.getComponentType()); + if (playerComponent != null) { + Inventory inventory = playerComponent.getInventory(); + + // Capture hotbar items + captureContainer(inventory.getHotbar(), items, KitItem.HOTBAR); + + // Capture storage items + captureContainer(inventory.getStorage(), items, KitItem.STORAGE); + + // Capture armor items + captureContainer(inventory.getArmor(), items, KitItem.ARMOR); + + // Capture utility items + captureContainer(inventory.getUtility(), items, KitItem.UTILITY); + } + } catch (Exception e) { + Logger.warn("[KitManager] Failed to capture inventory: %s", e.getMessage()); + } + + int defaultCooldown = ConfigManager.get().kits().getDefaultCooldownSeconds(); + boolean defaultOneTime = ConfigManager.get().kits().isOneTimeDefault(); + + Kit kit = new Kit(kitName.toLowerCase(), kitName, items, defaultCooldown, defaultOneTime, null); + storage.addKit(kit); + return kit; + } + + public boolean deleteKit(@NotNull String name) { + return storage.removeKit(name); + } + + public void clearPlayerCooldowns(@NotNull UUID playerUuid) { + String prefix = playerUuid + ":"; + cooldowns.keySet().removeIf(key -> key.startsWith(prefix)); + } + + public void shutdown() { + storage.save(); + cooldowns.clear(); + oneTimeClaims.clear(); + } + + /** + * Checks if the player has enough inventory space for the kit. + * Displaced armor/utility items need room in hotbar/storage. + */ + private boolean hasSpaceForKit(@NotNull Inventory inventory, @NotNull Kit kit) { + ItemContainer hotbar = inventory.getHotbar(); + ItemContainer storageContainer = inventory.getStorage(); + ItemContainer armor = inventory.getArmor(); + ItemContainer utility = inventory.getUtility(); + + int slotsNeeded = 0; + + for (KitItem kitItem : kit.items()) { + switch (kitItem.section()) { + case KitItem.ARMOR -> { + // If armor slot is occupied, existing item needs to move to hotbar/storage + if (kitItem.slot() >= 0 && kitItem.slot() < armor.getCapacity()) { + ItemStack existing = armor.getItemStack((short) kitItem.slot()); + if (!ItemStack.isEmpty(existing)) { + slotsNeeded++; + } + } + } + case KitItem.UTILITY -> { + // If utility slot is occupied, existing item needs to move to hotbar/storage + if (kitItem.slot() >= 0 && kitItem.slot() < utility.getCapacity()) { + ItemStack existing = utility.getItemStack((short) kitItem.slot()); + if (!ItemStack.isEmpty(existing)) { + slotsNeeded++; + } + } + } + case KitItem.HOTBAR, KitItem.STORAGE -> { + // Specific slot items overwrite — no extra space needed + // But slot=-1 items need a free slot + if (kitItem.slot() < 0) { + slotsNeeded++; + } + } + } + } + + if (slotsNeeded == 0) { + return true; + } + + // Count free slots in hotbar + storage + int freeSlots = countFreeSlots(hotbar) + countFreeSlots(storageContainer); + + // Subtract slots that will be taken by kit items with specific hotbar/storage slots + // going into currently-empty slots (those free slots will no longer be available) + for (KitItem kitItem : kit.items()) { + if (kitItem.slot() >= 0) { + if (kitItem.section().equals(KitItem.HOTBAR) + && kitItem.slot() < hotbar.getCapacity()) { + ItemStack existing = hotbar.getItemStack((short) kitItem.slot()); + if (ItemStack.isEmpty(existing)) { + freeSlots--; // This free slot will be used by the kit item + } + } else if (kitItem.section().equals(KitItem.STORAGE) + && kitItem.slot() < storageContainer.getCapacity()) { + ItemStack existing = storageContainer.getItemStack((short) kitItem.slot()); + if (ItemStack.isEmpty(existing)) { + freeSlots--; // This free slot will be used by the kit item + } + } + } + } + + return freeSlots >= slotsNeeded; + } + + private int countFreeSlots(@NotNull ItemContainer container) { + int free = 0; + for (short i = 0; i < container.getCapacity(); i++) { + if (ItemStack.isEmpty(container.getItemStack(i))) { + free++; + } + } + return free; + } + + private void captureContainer(@NotNull ItemContainer container, @NotNull List items, + @NotNull String section) { + short capacity = container.getCapacity(); + for (short i = 0; i < capacity; i++) { + try { + ItemStack stack = container.getItemStack(i); + if (!ItemStack.isEmpty(stack)) { + items.add(new KitItem( + stack.getItemId(), + stack.getQuantity(), + i, + section + )); + } + } catch (Exception e) { + // Skip slot on error + } + } + } + + private void giveItems(@NotNull Inventory inventory, @NotNull Kit kit) { + ItemContainer hotbar = inventory.getHotbar(); + ItemContainer storageContainer = inventory.getStorage(); + ItemContainer armor = inventory.getArmor(); + ItemContainer utility = inventory.getUtility(); + + // Collect displaced items that need to go to hotbar/storage + List displaced = new ArrayList<>(); + + for (KitItem kitItem : kit.items()) { + try { + ItemStack stack = new ItemStack(kitItem.itemId(), kitItem.quantity()); + int slot = kitItem.slot(); + + switch (kitItem.section()) { + case KitItem.ARMOR -> { + if (slot >= 0 && slot < armor.getCapacity()) { + ItemStack existing = armor.getItemStack((short) slot); + if (!ItemStack.isEmpty(existing)) { + displaced.add(existing); + } + armor.setItemStackForSlot((short) slot, stack); + } + } + case KitItem.UTILITY -> { + if (slot >= 0 && slot < utility.getCapacity()) { + ItemStack existing = utility.getItemStack((short) slot); + if (!ItemStack.isEmpty(existing)) { + displaced.add(existing); + } + utility.setItemStackForSlot((short) slot, stack); + } + } + case KitItem.HOTBAR -> { + if (slot >= 0 && slot < hotbar.getCapacity()) { + hotbar.setItemStackForSlot((short) slot, stack); + } else if (slot < 0) { + placeInFirstAvailable(hotbar, storageContainer, stack); + } + } + case KitItem.STORAGE -> { + if (slot >= 0 && slot < storageContainer.getCapacity()) { + storageContainer.setItemStackForSlot((short) slot, stack); + } else if (slot < 0) { + placeInFirstAvailable(hotbar, storageContainer, stack); + } + } + } + } catch (Exception e) { + Logger.warn("[KitManager] Failed to give item %s: %s", kitItem.itemId(), e.getMessage()); + } + } + + // Place displaced armor/utility items into hotbar/storage + for (ItemStack item : displaced) { + placeInFirstAvailable(hotbar, storageContainer, item); + } + } + + /** + * Places an item in the first available slot, checking hotbar then storage. + */ + private void placeInFirstAvailable(@NotNull ItemContainer hotbar, + @NotNull ItemContainer storage, + @NotNull ItemStack stack) { + for (short h = 0; h < hotbar.getCapacity(); h++) { + if (ItemStack.isEmpty(hotbar.getItemStack(h))) { + hotbar.setItemStackForSlot(h, stack); + return; + } + } + for (short s = 0; s < storage.getCapacity(); s++) { + if (ItemStack.isEmpty(storage.getItemStack(s))) { + storage.setItemStackForSlot(s, stack); + return; + } + } + Logger.warn("[KitManager] No space for displaced item: %s", stack.getItemId()); + } +} diff --git a/src/main/java/com/hyperessentials/module/kits/KitsModule.java b/src/main/java/com/hyperessentials/module/kits/KitsModule.java index 429a437..462760d 100644 --- a/src/main/java/com/hyperessentials/module/kits/KitsModule.java +++ b/src/main/java/com/hyperessentials/module/kits/KitsModule.java @@ -1,43 +1,82 @@ -package com.hyperessentials.module.kits; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.config.ModuleConfig; -import com.hyperessentials.module.AbstractModule; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Kits module for HyperEssentials. - */ -public class KitsModule extends AbstractModule { - - @Override - @NotNull - public String getName() { - return "kits"; - } - - @Override - @NotNull - public String getDisplayName() { - return "Kits"; - } - - @Override - public void onEnable() { - super.onEnable(); - // TODO: Register commands, listeners, and storage - } - - @Override - public void onDisable() { - // TODO: Unregister commands, save data, cleanup - super.onDisable(); - } - - @Override - @Nullable - public ModuleConfig getModuleConfig() { - return ConfigManager.get().kits(); - } -} +package com.hyperessentials.module.kits; + +import com.hyperessentials.HyperEssentials; +import com.hyperessentials.api.HyperEssentialsAPI; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.ModuleConfig; +import com.hyperessentials.module.AbstractModule; +import com.hyperessentials.module.kits.command.*; +import com.hyperessentials.module.kits.storage.KitStorage; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Kits module for HyperEssentials. + * Provides admin-defined item kits with permission checks and cooldowns. + */ +public class KitsModule extends AbstractModule { + + private KitStorage kitStorage; + private KitManager kitManager; + + @Override + @NotNull + public String getName() { + return "kits"; + } + + @Override + @NotNull + public String getDisplayName() { + return "Kits"; + } + + @Override + public void onEnable() { + super.onEnable(); + + HyperEssentials core = HyperEssentialsAPI.getInstance(); + if (core == null) return; + + // Initialize storage and manager + kitStorage = new KitStorage(core.getDataDir()); + kitStorage.load(); + kitManager = new KitManager(kitStorage); + + // Register commands + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin != null) { + try { + plugin.getCommandRegistry().registerCommand(new KitCommand(this)); + plugin.getCommandRegistry().registerCommand(new KitsCommand(this)); + plugin.getCommandRegistry().registerCommand(new CreateKitCommand(this)); + plugin.getCommandRegistry().registerCommand(new DeleteKitCommand(this)); + plugin.getCommandRegistry().registerCommand(new PreviewKitCommand(this)); + Logger.info("[Kits] Registered commands: /kit, /kits, /createkit, /deletekit, /previewkit"); + } catch (Exception e) { + Logger.severe("[Kits] Failed to register commands: %s", e.getMessage()); + } + } + } + + @Override + public void onDisable() { + if (kitManager != null) { + kitManager.shutdown(); + } + super.onDisable(); + } + + @NotNull + public KitManager getKitManager() { + return kitManager; + } + + @Override + @Nullable + public ModuleConfig getModuleConfig() { + return ConfigManager.get().kits(); + } +} diff --git a/src/main/java/com/hyperessentials/module/kits/command/CreateKitCommand.java b/src/main/java/com/hyperessentials/module/kits/command/CreateKitCommand.java new file mode 100644 index 0000000..c7e1d0c --- /dev/null +++ b/src/main/java/com/hyperessentials/module/kits/command/CreateKitCommand.java @@ -0,0 +1,66 @@ +package com.hyperessentials.module.kits.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.kits.KitsModule; +import com.hyperessentials.module.kits.data.Kit; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /createkit - Create a kit from current inventory. + */ +public class CreateKitCommand extends AbstractPlayerCommand { + + private final KitsModule module; + + public CreateKitCommand(@NotNull KitsModule module) { + super("createkit", "Create a kit from your inventory"); + this.module = module; + addAliases("ckit"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.KIT_CREATE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to create kits.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /createkit ")); + return; + } + + String kitName = parts[1].toLowerCase(); + + // Check if name is valid + if (!kitName.matches("[a-z0-9_-]+")) { + ctx.sendMessage(CommandUtil.error("Kit name must contain only lowercase letters, numbers, hyphens, and underscores.")); + return; + } + + // Check if already exists + if (module.getKitManager().getKit(kitName) != null) { + ctx.sendMessage(CommandUtil.error("Kit '" + kitName + "' already exists. Delete it first.")); + return; + } + + Kit kit = module.getKitManager().captureFromInventory(playerRef, store, ref, kitName); + ctx.sendMessage(CommandUtil.success("Kit '" + kit.displayName() + "' created with " + kit.items().size() + " item(s).")); + } +} diff --git a/src/main/java/com/hyperessentials/module/kits/command/DeleteKitCommand.java b/src/main/java/com/hyperessentials/module/kits/command/DeleteKitCommand.java new file mode 100644 index 0000000..97b5f06 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/kits/command/DeleteKitCommand.java @@ -0,0 +1,56 @@ +package com.hyperessentials.module.kits.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.kits.KitsModule; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /deletekit - Delete a kit. + */ +public class DeleteKitCommand extends AbstractPlayerCommand { + + private final KitsModule module; + + public DeleteKitCommand(@NotNull KitsModule module) { + super("deletekit", "Delete a kit"); + this.module = module; + addAliases("dkit"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.KIT_DELETE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to delete kits.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /deletekit ")); + return; + } + + String kitName = parts[1].toLowerCase(); + + if (module.getKitManager().deleteKit(kitName)) { + ctx.sendMessage(CommandUtil.success("Kit '" + kitName + "' deleted.")); + } else { + ctx.sendMessage(CommandUtil.error("Kit '" + kitName + "' not found.")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/kits/command/KitCommand.java b/src/main/java/com/hyperessentials/module/kits/command/KitCommand.java new file mode 100644 index 0000000..cdcbbe3 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/kits/command/KitCommand.java @@ -0,0 +1,75 @@ +package com.hyperessentials.module.kits.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.kits.KitManager; +import com.hyperessentials.module.kits.KitsModule; +import com.hyperessentials.module.kits.data.Kit; +import com.hyperessentials.util.DurationParser; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /kit - Claim a kit. + */ +public class KitCommand extends AbstractPlayerCommand { + + private final KitsModule module; + + public KitCommand(@NotNull KitsModule module) { + super("kit", "Claim a kit"); + this.module = module; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.KIT_USE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to use kits.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /kit ")); + return; + } + + String kitName = parts[1].toLowerCase(); + KitManager manager = module.getKitManager(); + Kit kit = manager.getKit(kitName); + + if (kit == null) { + ctx.sendMessage(CommandUtil.error("Kit '" + kitName + "' not found.")); + return; + } + + KitManager.ClaimResult result = manager.claimKit( + playerRef.getUuid(), playerRef, store, ref, kit + ); + + switch (result) { + case SUCCESS -> ctx.sendMessage(CommandUtil.success("Kit '" + kit.displayName() + "' claimed!")); + case ON_COOLDOWN -> { + long remaining = manager.getRemainingCooldown(playerRef.getUuid(), kitName); + ctx.sendMessage(CommandUtil.error("Kit on cooldown. " + DurationParser.formatHuman(remaining) + " remaining.")); + } + case ALREADY_CLAIMED -> ctx.sendMessage(CommandUtil.error("You have already claimed this one-time kit.")); + case NO_PERMISSION -> ctx.sendMessage(CommandUtil.error("You don't have permission to use this kit.")); + case KIT_NOT_FOUND -> ctx.sendMessage(CommandUtil.error("Kit '" + kitName + "' not found.")); + case INSUFFICIENT_SPACE -> ctx.sendMessage(CommandUtil.error("Not enough inventory space to claim this kit.")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/kits/command/KitsCommand.java b/src/main/java/com/hyperessentials/module/kits/command/KitsCommand.java new file mode 100644 index 0000000..033b9c8 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/kits/command/KitsCommand.java @@ -0,0 +1,86 @@ +package com.hyperessentials.module.kits.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.api.HyperEssentialsAPI; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.module.kits.KitsModule; +import com.hyperessentials.module.kits.data.Kit; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * /kits - List available kits. + * Opens GUI page when available, falls back to text list. + */ +public class KitsCommand extends AbstractPlayerCommand { + + private final KitsModule module; + + public KitsCommand(@NotNull KitsModule module) { + super("kits", "List available kits"); + this.module = module; + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.KIT_LIST)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to list kits.")); + return; + } + + // Try GUI page first + if (tryOpenGui(store, ref, playerRef)) { + return; + } + + // Text fallback + List available = module.getKitManager().getAvailableKits(playerRef.getUuid()); + + if (available.isEmpty()) { + ctx.sendMessage(CommandUtil.info("No kits available.")); + return; + } + + ctx.sendMessage(CommandUtil.info("Available Kits (" + available.size() + "):")); + for (Kit kit : available) { + String cooldownInfo = kit.cooldownSeconds() > 0 + ? " (cooldown: " + kit.cooldownSeconds() + "s)" + : ""; + String oneTimeInfo = kit.oneTime() ? " [one-time]" : ""; + + Message line = CommandUtil.prefix() + .insert(Message.raw(" " + kit.displayName()).color(CommandUtil.COLOR_GREEN)) + .insert(Message.raw(cooldownInfo + oneTimeInfo).color(CommandUtil.COLOR_GRAY)); + ctx.sendMessage(line); + } + } + + private boolean tryOpenGui(@NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef) { + if (!HyperEssentialsAPI.isAvailable()) return false; + + GuiManager guiManager = HyperEssentialsAPI.getInstance().getGuiManager(); + if (guiManager.getPlayerRegistry().getEntry("kits") == null) return false; + + Player player = store.getComponent(ref, Player.getComponentType()); + if (player == null) return false; + + return guiManager.openPlayerPage("kits", player, ref, store, playerRef); + } +} diff --git a/src/main/java/com/hyperessentials/module/kits/command/PreviewKitCommand.java b/src/main/java/com/hyperessentials/module/kits/command/PreviewKitCommand.java new file mode 100644 index 0000000..f7fed2a --- /dev/null +++ b/src/main/java/com/hyperessentials/module/kits/command/PreviewKitCommand.java @@ -0,0 +1,82 @@ +package com.hyperessentials.module.kits.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.kits.KitManager; +import com.hyperessentials.module.kits.KitsModule; +import com.hyperessentials.module.kits.data.Kit; +import com.hyperessentials.module.kits.data.KitItem; +import com.hyperessentials.util.DurationParser; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /previewkit - Preview the contents of a kit. + */ +public class PreviewKitCommand extends AbstractPlayerCommand { + + private final KitsModule module; + + public PreviewKitCommand(@NotNull KitsModule module) { + super("previewkit", "Preview the contents of a kit"); + this.module = module; + addAliases("vkit", "viewkit"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.KIT_USE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to preview kits.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /previewkit ")); + return; + } + + String kitName = parts[1].toLowerCase(); + KitManager manager = module.getKitManager(); + Kit kit = manager.getKit(kitName); + + if (kit == null) { + ctx.sendMessage(CommandUtil.error("Kit '" + kitName + "' not found.")); + return; + } + + ctx.sendMessage(CommandUtil.msg("--- Kit: " + kit.displayName() + " ---", CommandUtil.COLOR_GOLD)); + + if (kit.items().isEmpty()) { + ctx.sendMessage(CommandUtil.msg(" (empty)", CommandUtil.COLOR_GRAY)); + } else { + for (KitItem item : kit.items()) { + String line = " " + item.itemId() + " x" + item.quantity() + " [" + item.section() + "]"; + ctx.sendMessage(CommandUtil.msg(line, CommandUtil.COLOR_GRAY)); + } + } + + if (kit.cooldownSeconds() > 0) { + ctx.sendMessage(CommandUtil.msg("Cooldown: " + DurationParser.formatHuman(kit.cooldownSeconds() * 1000L), CommandUtil.COLOR_GRAY)); + } + if (kit.oneTime()) { + ctx.sendMessage(CommandUtil.msg("One-time use only", CommandUtil.COLOR_GRAY)); + } + if (kit.permission() != null) { + ctx.sendMessage(CommandUtil.msg("Permission: " + kit.permission(), CommandUtil.COLOR_GRAY)); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/kits/data/Kit.java b/src/main/java/com/hyperessentials/module/kits/data/Kit.java new file mode 100644 index 0000000..1433ab8 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/kits/data/Kit.java @@ -0,0 +1,39 @@ +package com.hyperessentials.module.kits.data; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * Represents a kit definition. + * + * @param name unique lowercase identifier + * @param displayName display name shown to players + * @param items items included in the kit + * @param cooldownSeconds cooldown between claims (0 = no cooldown) + * @param oneTime whether the kit can only be claimed once per player + * @param permission custom permission override, or null for default + */ +public record Kit( + @NotNull String name, + @NotNull String displayName, + @NotNull List items, + int cooldownSeconds, + boolean oneTime, + @Nullable String permission +) { + public Kit { + if (cooldownSeconds < 0) cooldownSeconds = 0; + items = List.copyOf(items); + } + + /** + * Returns the effective permission node for this kit. + * Uses custom permission if set, otherwise derives from kit name. + */ + @NotNull + public String getEffectivePermission() { + return permission != null ? permission : "hyperessentials.kit.use." + name; + } +} diff --git a/src/main/java/com/hyperessentials/module/kits/data/KitItem.java b/src/main/java/com/hyperessentials/module/kits/data/KitItem.java new file mode 100644 index 0000000..746dbef --- /dev/null +++ b/src/main/java/com/hyperessentials/module/kits/data/KitItem.java @@ -0,0 +1,27 @@ +package com.hyperessentials.module.kits.data; + +import org.jetbrains.annotations.NotNull; + +/** + * Represents an item within a kit. + * + * @param itemId the item type ID (e.g., "Weapon_Sword_Adamantite") + * @param quantity the number of items + * @param slot the target slot index within the section, or -1 for first available + * @param section the inventory section: "hotbar", "storage", "armor", or "utility" + */ +public record KitItem( + @NotNull String itemId, + int quantity, + int slot, + @NotNull String section +) { + public static final String HOTBAR = "hotbar"; + public static final String STORAGE = "storage"; + public static final String ARMOR = "armor"; + public static final String UTILITY = "utility"; + + public KitItem { + if (quantity < 1) quantity = 1; + } +} diff --git a/src/main/java/com/hyperessentials/module/kits/storage/KitStorage.java b/src/main/java/com/hyperessentials/module/kits/storage/KitStorage.java new file mode 100644 index 0000000..4727b9c --- /dev/null +++ b/src/main/java/com/hyperessentials/module/kits/storage/KitStorage.java @@ -0,0 +1,164 @@ +package com.hyperessentials.module.kits.storage; + +import com.google.gson.*; +import com.hyperessentials.module.kits.data.Kit; +import com.hyperessentials.module.kits.data.KitItem; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Loads and saves kit definitions from data/kits.json. + */ +public class KitStorage { + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + + private final Path filePath; + private final Map kits = new ConcurrentHashMap<>(); + + public KitStorage(@NotNull Path dataDir) { + this.filePath = dataDir.resolve("data").resolve("kits.json"); + } + + public void load() { + kits.clear(); + + if (!Files.exists(filePath)) { + Logger.info("[KitStorage] No kits data file found, starting fresh"); + return; + } + + try { + String json = Files.readString(filePath); + JsonObject root = JsonParser.parseString(json).getAsJsonObject(); + + if (root.has("kits") && root.get("kits").isJsonArray()) { + for (JsonElement el : root.getAsJsonArray("kits")) { + Kit kit = deserializeKit(el.getAsJsonObject()); + if (kit != null) { + kits.put(kit.name(), kit); + } + } + } + + Logger.info("[KitStorage] Loaded %d kit(s)", kits.size()); + } catch (Exception e) { + Logger.severe("[KitStorage] Failed to load kits: %s", e.getMessage()); + } + } + + public void save() { + try { + Files.createDirectories(filePath.getParent()); + + JsonObject root = new JsonObject(); + JsonArray kitsArray = new JsonArray(); + + for (Kit kit : kits.values()) { + kitsArray.add(serializeKit(kit)); + } + root.add("kits", kitsArray); + + Files.writeString(filePath, GSON.toJson(root)); + Logger.debug("[KitStorage] Saved %d kit(s)", kits.size()); + } catch (IOException e) { + Logger.severe("[KitStorage] Failed to save kits: %s", e.getMessage()); + } + } + + @NotNull + public Map getKits() { + return Collections.unmodifiableMap(kits); + } + + @Nullable + public Kit getKit(@NotNull String name) { + return kits.get(name.toLowerCase()); + } + + public void addKit(@NotNull Kit kit) { + kits.put(kit.name(), kit); + save(); + } + + public boolean removeKit(@NotNull String name) { + Kit removed = kits.remove(name.toLowerCase()); + if (removed != null) { + save(); + return true; + } + return false; + } + + private JsonObject serializeKit(@NotNull Kit kit) { + JsonObject obj = new JsonObject(); + obj.addProperty("name", kit.name()); + obj.addProperty("displayName", kit.displayName()); + obj.addProperty("cooldownSeconds", kit.cooldownSeconds()); + obj.addProperty("oneTime", kit.oneTime()); + if (kit.permission() != null) { + obj.addProperty("permission", kit.permission()); + } + + JsonArray items = new JsonArray(); + for (KitItem item : kit.items()) { + JsonObject itemObj = new JsonObject(); + itemObj.addProperty("itemId", item.itemId()); + itemObj.addProperty("quantity", item.quantity()); + itemObj.addProperty("slot", item.slot()); + itemObj.addProperty("section", item.section()); + items.add(itemObj); + } + obj.add("items", items); + + return obj; + } + + @Nullable + private Kit deserializeKit(@NotNull JsonObject obj) { + try { + String name = obj.get("name").getAsString(); + String displayName = obj.has("displayName") ? obj.get("displayName").getAsString() : name; + int cooldown = obj.has("cooldownSeconds") ? obj.get("cooldownSeconds").getAsInt() : 0; + boolean oneTime = obj.has("oneTime") && obj.get("oneTime").getAsBoolean(); + String permission = obj.has("permission") ? obj.get("permission").getAsString() : null; + + List items = new ArrayList<>(); + if (obj.has("items") && obj.get("items").isJsonArray()) { + for (JsonElement el : obj.getAsJsonArray("items")) { + JsonObject itemObj = el.getAsJsonObject(); + int slot = itemObj.has("slot") ? itemObj.get("slot").getAsInt() : -1; + String section; + if (itemObj.has("section")) { + section = itemObj.get("section").getAsString(); + } else { + // Backward compat: infer section from old flat-slot numbering + // 0-8 = hotbar, 9+ = storage + section = (slot >= 0 && slot < 9) ? KitItem.HOTBAR : KitItem.STORAGE; + if (section.equals(KitItem.STORAGE) && slot >= 9) { + slot = slot - 9; // Convert flat slot to storage-local slot + } + } + items.add(new KitItem( + itemObj.get("itemId").getAsString(), + itemObj.has("quantity") ? itemObj.get("quantity").getAsInt() : 1, + slot, + section + )); + } + } + + return new Kit(name.toLowerCase(), displayName, items, cooldown, oneTime, permission); + } catch (Exception e) { + Logger.warn("[KitStorage] Failed to parse kit: %s", e.getMessage()); + return null; + } + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/FreezeManager.java b/src/main/java/com/hyperessentials/module/moderation/FreezeManager.java new file mode 100644 index 0000000..05543da --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/FreezeManager.java @@ -0,0 +1,113 @@ +package com.hyperessentials.module.moderation; + +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.data.Location; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Transform; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.server.core.HytaleServer; +import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * Manages freeze state for players. Session-only (not persisted). + * Frozen players are teleported back to their freeze location if they move. + */ +public class FreezeManager { + + private final Map frozenPlayers = new ConcurrentHashMap<>(); + private ScheduledFuture checkerTask; + + public void start() { + int intervalMs = ConfigManager.get().moderation().getFreezeCheckIntervalMs(); + checkerTask = HytaleServer.SCHEDULED_EXECUTOR.scheduleAtFixedRate( + this::checkFrozenPlayers, intervalMs, intervalMs, TimeUnit.MILLISECONDS + ); + Logger.debug("[FreezeManager] Movement checker started (interval: %dms)", intervalMs); + } + + public void shutdown() { + if (checkerTask != null) { + checkerTask.cancel(false); + checkerTask = null; + } + frozenPlayers.clear(); + } + + public void freeze(@NotNull UUID playerUuid, @NotNull Location location) { + frozenPlayers.put(playerUuid, location); + } + + public void unfreeze(@NotNull UUID playerUuid) { + frozenPlayers.remove(playerUuid); + } + + public boolean isFrozen(@NotNull UUID playerUuid) { + return frozenPlayers.containsKey(playerUuid); + } + + public void onPlayerDisconnect(@NotNull UUID playerUuid) { + frozenPlayers.remove(playerUuid); + } + + private void checkFrozenPlayers() { + if (frozenPlayers.isEmpty()) return; + + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null) return; + + for (Map.Entry entry : frozenPlayers.entrySet()) { + try { + PlayerRef playerRef = plugin.getTrackedPlayer(entry.getKey()); + if (playerRef == null) { + frozenPlayers.remove(entry.getKey()); + continue; + } + + Location frozenLoc = entry.getValue(); + Transform transform = playerRef.getTransform(); + Vector3d pos = transform.getPosition(); + double px = pos.getX(); + double py = pos.getY(); + double pz = pos.getZ(); + + double distSq = Math.pow(px - frozenLoc.x(), 2) + + Math.pow(py - frozenLoc.y(), 2) + + Math.pow(pz - frozenLoc.z(), 2); + + if (distSq > 0.25) { + // Teleport player back to frozen location using Teleport ECS component + try { + Ref ref = playerRef.getReference(); + if (ref != null) { + Store store = ref.getStore(); + if (store != null) { + Vector3d targetPos = new Vector3d(frozenLoc.x(), frozenLoc.y(), frozenLoc.z()); + Vector3f targetRot = new Vector3f(frozenLoc.pitch(), frozenLoc.yaw(), 0); + // Use same-world Teleport constructor (no World param) + store.addComponent(ref, Teleport.getComponentType(), + new Teleport(targetPos, targetRot)); + } + } + } catch (Exception e) { + Logger.debug("[FreezeManager] Teleport failed for frozen player: %s", e.getMessage()); + } + } + } catch (Exception e) { + Logger.debug("[FreezeManager] Error checking frozen player: %s", e.getMessage()); + } + } + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/ModerationManager.java b/src/main/java/com/hyperessentials/module/moderation/ModerationManager.java new file mode 100644 index 0000000..542c563 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/ModerationManager.java @@ -0,0 +1,352 @@ +package com.hyperessentials.module.moderation; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.module.moderation.data.IpBan; +import com.hyperessentials.module.moderation.data.Punishment; +import com.hyperessentials.module.moderation.data.PunishmentType; +import com.hyperessentials.module.moderation.storage.ModerationStorage; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hyperessentials.util.DurationParser; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.net.InetSocketAddress; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages ban, mute, and kick operations. + */ +public class ModerationManager { + + private final ModerationStorage storage; + private final Map playerIps = new ConcurrentHashMap<>(); + + public ModerationManager(@NotNull ModerationStorage storage) { + this.storage = storage; + } + + // === IP Tracking === + + /** + * Captures a player's IP address on connect. + */ + public void onPlayerConnect(@NotNull PlayerRef playerRef) { + try { + InetSocketAddress addr = (InetSocketAddress) playerRef.getPacketHandler() + .getChannel().remoteAddress(); + if (addr != null) { + String ip = addr.getAddress().getHostAddress(); + playerIps.put(playerRef.getUuid(), ip); + } + } catch (Exception e) { + Logger.debug("[Moderation] Failed to capture IP for %s", playerRef.getUsername()); + } + } + + @Nullable + public String getPlayerIp(@NotNull UUID uuid) { + return playerIps.get(uuid); + } + + /** + * Checks if a player's IP is banned. Auto-removes expired bans. + */ + public boolean isIpBanned(@NotNull String ip) { + IpBan ban = storage.getIpBan(ip); + if (ban != null && ban.hasExpired()) { + storage.removeIpBan(ip); + return false; + } + return ban != null && ban.isEffective(); + } + + @NotNull + public IpBan ipBan(@NotNull String ip, @Nullable UUID issuerUuid, @NotNull String issuerName, + @Nullable String reason, @Nullable Long durationMs) { + Instant expiresAt = durationMs != null ? Instant.now().plusMillis(durationMs) : null; + IpBan ban = new IpBan(ip, reason, issuerUuid, issuerName, Instant.now(), expiresAt); + storage.addIpBan(ban); + + notifyStaff(Permissions.NOTIFY_BAN, issuerName + " IP banned " + ip + + (ban.isPermanent() ? " permanently" : " for " + DurationParser.formatHuman(durationMs))); + + return ban; + } + + public boolean ipUnban(@NotNull String ip) { + boolean removed = storage.removeIpBan(ip); + if (removed) { + notifyStaff(Permissions.NOTIFY_BAN, "IP " + ip + " was unbanned"); + } + return removed; + } + + /** + * Kicks all online players that share the given IP. + */ + public void kickPlayersWithIp(@NotNull String ip, @NotNull String reason) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null) return; + + for (Map.Entry entry : playerIps.entrySet()) { + if (ip.equals(entry.getValue())) { + kickOnlinePlayer(entry.getKey(), reason); + } + } + } + + // === Ban Operations === + + @NotNull + public Punishment ban(@NotNull UUID playerUuid, @NotNull String playerName, + @Nullable UUID issuerUuid, @NotNull String issuerName, + @Nullable String reason, @Nullable Long durationMs) { + // Revoke any existing active ban first + Punishment existing = storage.getActiveBan(playerUuid); + if (existing != null) { + storage.updatePunishment(existing.revoke(issuerUuid, issuerName)); + } + + String effectiveReason = reason != null ? reason + : ConfigManager.get().moderation().getDefaultBanReason(); + + Instant expiresAt = durationMs != null ? Instant.now().plusMillis(durationMs) : null; + + Punishment punishment = new Punishment( + UUID.randomUUID(), PunishmentType.BAN, playerUuid, playerName, + issuerUuid, issuerName, effectiveReason, Instant.now(), + expiresAt, true, null, null + ); + + storage.addPunishment(punishment); + + // Kick the player if online + kickOnlinePlayer(playerUuid, buildBanMessage(punishment)); + + // Broadcast and notify + if (ConfigManager.get().moderation().isBroadcastBans()) { + String durationText = punishment.isPermanent() ? "permanently" : "for " + DurationParser.formatHuman(durationMs); + broadcastToAll(CommandUtil.msg(playerName + " was banned " + durationText + ".", CommandUtil.COLOR_RED)); + } + notifyStaff(Permissions.NOTIFY_BAN, + issuerName + " banned " + playerName + (punishment.isPermanent() ? " permanently" : " for " + DurationParser.formatHuman(durationMs))); + + return punishment; + } + + public boolean unban(@NotNull UUID playerUuid, @Nullable UUID revokerUuid, @NotNull String revokerName) { + Punishment ban = storage.getActiveBan(playerUuid); + if (ban == null) return false; + + storage.updatePunishment(ban.revoke(revokerUuid, revokerName)); + + notifyStaff(Permissions.NOTIFY_BAN, revokerName + " unbanned " + ban.playerName()); + return true; + } + + public boolean isBanned(@NotNull UUID playerUuid) { + Punishment ban = storage.getActiveBan(playerUuid); + if (ban != null && ban.hasExpired()) { + storage.updatePunishment(ban.revoke(null, "System")); + return false; + } + return ban != null; + } + + @Nullable + public Punishment getActiveBan(@NotNull UUID playerUuid) { + return storage.getActiveBan(playerUuid); + } + + // === Mute Operations === + + @NotNull + public Punishment mute(@NotNull UUID playerUuid, @NotNull String playerName, + @Nullable UUID issuerUuid, @NotNull String issuerName, + @Nullable String reason, @Nullable Long durationMs) { + Punishment existing = storage.getActiveMute(playerUuid); + if (existing != null) { + storage.updatePunishment(existing.revoke(issuerUuid, issuerName)); + } + + String effectiveReason = reason != null ? reason + : ConfigManager.get().moderation().getDefaultMuteReason(); + + Instant expiresAt = durationMs != null ? Instant.now().plusMillis(durationMs) : null; + + Punishment punishment = new Punishment( + UUID.randomUUID(), PunishmentType.MUTE, playerUuid, playerName, + issuerUuid, issuerName, effectiveReason, Instant.now(), + expiresAt, true, null, null + ); + + storage.addPunishment(punishment); + + // Notify the muted player if online + PlayerRef target = findOnlinePlayer(playerUuid); + if (target != null) { + String durationText = punishment.isPermanent() ? "permanently" : "for " + DurationParser.formatHuman(durationMs); + target.sendMessage(CommandUtil.error("You have been muted " + durationText + ".")); + } + + if (ConfigManager.get().moderation().isBroadcastMutes()) { + String durationText = punishment.isPermanent() ? "permanently" : "for " + DurationParser.formatHuman(durationMs); + broadcastToAll(CommandUtil.msg(playerName + " was muted " + durationText + ".", CommandUtil.COLOR_RED)); + } + notifyStaff(Permissions.NOTIFY_MUTE, + issuerName + " muted " + playerName + (punishment.isPermanent() ? " permanently" : " for " + DurationParser.formatHuman(durationMs))); + + return punishment; + } + + public boolean unmute(@NotNull UUID playerUuid, @Nullable UUID revokerUuid, @NotNull String revokerName) { + Punishment mute = storage.getActiveMute(playerUuid); + if (mute == null) return false; + + storage.updatePunishment(mute.revoke(revokerUuid, revokerName)); + + PlayerRef target = findOnlinePlayer(playerUuid); + if (target != null) { + target.sendMessage(CommandUtil.success("You have been unmuted.")); + } + + notifyStaff(Permissions.NOTIFY_MUTE, revokerName + " unmuted " + mute.playerName()); + return true; + } + + public boolean isMuted(@NotNull UUID playerUuid) { + Punishment mute = storage.getActiveMute(playerUuid); + if (mute != null && mute.hasExpired()) { + storage.updatePunishment(mute.revoke(null, "System")); + return false; + } + return mute != null; + } + + // === Kick Operations === + + @NotNull + public Punishment kick(@NotNull UUID playerUuid, @NotNull String playerName, + @Nullable UUID issuerUuid, @NotNull String issuerName, + @Nullable String reason) { + String effectiveReason = reason != null ? reason + : ConfigManager.get().moderation().getDefaultKickReason(); + + Punishment punishment = new Punishment( + UUID.randomUUID(), PunishmentType.KICK, playerUuid, playerName, + issuerUuid, issuerName, effectiveReason, Instant.now(), + null, false, null, null + ); + + storage.addPunishment(punishment); + + kickOnlinePlayer(playerUuid, effectiveReason); + + if (ConfigManager.get().moderation().isBroadcastKicks()) { + broadcastToAll(CommandUtil.msg(playerName + " was kicked.", CommandUtil.COLOR_RED)); + } + notifyStaff(Permissions.NOTIFY_KICK, issuerName + " kicked " + playerName); + + return punishment; + } + + // === History === + + @NotNull + public List getHistory(@NotNull UUID playerUuid) { + return storage.getPunishments(playerUuid); + } + + /** + * Gets all punishments across all players, optionally filtered by active status. + */ + @NotNull + public List getAllPunishments(boolean activeOnly) { + return storage.getAllPunishments(activeOnly); + } + + // === Offline Resolution === + + @Nullable + public UUID findPlayerUuid(@NotNull String name) { + // Check online first + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin != null) { + PlayerRef online = plugin.findOnlinePlayer(name); + if (online != null) return online.getUuid(); + } + // Check stored records + return storage.findPlayerUuid(name); + } + + @Nullable + public String getStoredPlayerName(@NotNull UUID uuid) { + List list = storage.getPunishments(uuid); + return list.isEmpty() ? null : list.getFirst().playerName(); + } + + public void shutdown() { + storage.save(); + } + + // === Helpers === + + private void kickOnlinePlayer(@NotNull UUID playerUuid, @NotNull String reason) { + PlayerRef player = findOnlinePlayer(playerUuid); + if (player != null) { + try { + player.getPacketHandler().disconnect(reason); + } catch (Exception e) { + Logger.warn("[Moderation] Failed to kick player: %s", e.getMessage()); + } + } + } + + @Nullable + private PlayerRef findOnlinePlayer(@NotNull UUID playerUuid) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + return plugin != null ? plugin.getTrackedPlayer(playerUuid) : null; + } + + private void broadcastToAll(@NotNull Message message) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null) return; + + for (PlayerRef player : plugin.getTrackedPlayers().values()) { + player.sendMessage(message); + } + } + + private void notifyStaff(@NotNull String permission, @NotNull String message) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null) return; + + Message msg = CommandUtil.msg("[Staff] " + message, CommandUtil.COLOR_AQUA); + for (Map.Entry entry : plugin.getTrackedPlayers().entrySet()) { + if (CommandUtil.hasPermission(entry.getKey(), permission)) { + entry.getValue().sendMessage(msg); + } + } + } + + @NotNull + private String buildBanMessage(@NotNull Punishment ban) { + StringBuilder sb = new StringBuilder("You are banned from this server."); + if (ban.reason() != null) { + sb.append("\nReason: ").append(ban.reason()); + } + if (!ban.isPermanent()) { + sb.append("\nExpires in: ").append(DurationParser.formatHuman(ban.getRemainingMillis())); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/ModerationModule.java b/src/main/java/com/hyperessentials/module/moderation/ModerationModule.java index ce78400..cdcef9c 100644 --- a/src/main/java/com/hyperessentials/module/moderation/ModerationModule.java +++ b/src/main/java/com/hyperessentials/module/moderation/ModerationModule.java @@ -1,43 +1,150 @@ -package com.hyperessentials.module.moderation; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.config.ModuleConfig; -import com.hyperessentials.module.AbstractModule; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Moderation module for HyperEssentials. - */ -public class ModerationModule extends AbstractModule { - - @Override - @NotNull - public String getName() { - return "moderation"; - } - - @Override - @NotNull - public String getDisplayName() { - return "Moderation"; - } - - @Override - public void onEnable() { - super.onEnable(); - // TODO: Register commands, listeners, and storage - } - - @Override - public void onDisable() { - // TODO: Unregister commands, save data, cleanup - super.onDisable(); - } - - @Override - @Nullable - public ModuleConfig getModuleConfig() { - return ConfigManager.get().moderation(); - } -} +package com.hyperessentials.module.moderation; + +import com.hyperessentials.HyperEssentials; +import com.hyperessentials.api.HyperEssentialsAPI; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.ModuleConfig; +import com.hyperessentials.module.AbstractModule; +import com.hyperessentials.module.moderation.command.*; +import com.hyperessentials.module.moderation.listener.ModerationListener; +import com.hyperessentials.module.moderation.storage.ModerationStorage; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.server.core.event.events.player.PlayerChatEvent; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Moderation module for HyperEssentials. + * Provides ban, mute, kick, freeze, vanish, punishment history, and IP bans. + */ +public class ModerationModule extends AbstractModule { + + private ModerationStorage storage; + private ModerationManager moderationManager; + private FreezeManager freezeManager; + private VanishManager vanishManager; + private ModerationListener listener; + private Consumer disconnectHandler; + private BiConsumer connectHandler; + + @Override + @NotNull + public String getName() { + return "moderation"; + } + + @Override + @NotNull + public String getDisplayName() { + return "Moderation"; + } + + @Override + public void onEnable() { + super.onEnable(); + + HyperEssentials core = HyperEssentialsAPI.getInstance(); + if (core == null) return; + + // Initialize storage + storage = new ModerationStorage(core.getDataDir()); + storage.load(); + + // Initialize managers + moderationManager = new ModerationManager(storage); + freezeManager = new FreezeManager(); + vanishManager = new VanishManager(); + + // Start freeze movement checker + freezeManager.start(); + + // Register listener + listener = new ModerationListener(this); + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin != null) { + plugin.getEventRegistry().register(PlayerConnectEvent.class, listener::onPlayerConnect); + plugin.getEventRegistry().registerGlobal(PlayerChatEvent.class, listener::onPlayerChat); + + // Register disconnect handler + disconnectHandler = uuid -> { + freezeManager.onPlayerDisconnect(uuid); + vanishManager.onPlayerDisconnect(uuid); + }; + core.registerDisconnectHandler(disconnectHandler); + + // Register connect handler for IP tracking + connectHandler = (uuid, username) -> { + PlayerRef ref = plugin.getTrackedPlayer(uuid); + if (ref != null) { + moderationManager.onPlayerConnect(ref); + } + }; + core.registerConnectHandler(connectHandler); + + // Register commands + try { + plugin.getCommandRegistry().registerCommand(new BanCommand(this)); + plugin.getCommandRegistry().registerCommand(new UnbanCommand(this)); + plugin.getCommandRegistry().registerCommand(new MuteCommand(this)); + plugin.getCommandRegistry().registerCommand(new UnmuteCommand(this)); + plugin.getCommandRegistry().registerCommand(new KickCommand(this)); + plugin.getCommandRegistry().registerCommand(new FreezeCommand(this)); + plugin.getCommandRegistry().registerCommand(new VanishCommand(this)); + plugin.getCommandRegistry().registerCommand(new PunishmentsCommand(this)); + plugin.getCommandRegistry().registerCommand(new IpBanCommand(this)); + plugin.getCommandRegistry().registerCommand(new IpUnbanCommand(this)); + Logger.info("[Moderation] Registered 10 commands"); + } catch (Exception e) { + Logger.severe("[Moderation] Failed to register commands: %s", e.getMessage()); + } + } + } + + @Override + public void onDisable() { + if (freezeManager != null) freezeManager.shutdown(); + if (vanishManager != null) vanishManager.shutdown(); + if (moderationManager != null) moderationManager.shutdown(); + + // Unregister handlers + HyperEssentials core = HyperEssentialsAPI.getInstance(); + if (core != null) { + if (disconnectHandler != null) { + core.unregisterDisconnectHandler(disconnectHandler); + } + if (connectHandler != null) { + core.unregisterConnectHandler(connectHandler); + } + } + + super.onDisable(); + } + + @NotNull + public ModerationManager getModerationManager() { + return moderationManager; + } + + @NotNull + public FreezeManager getFreezeManager() { + return freezeManager; + } + + @NotNull + public VanishManager getVanishManager() { + return vanishManager; + } + + @Override + @Nullable + public ModuleConfig getModuleConfig() { + return ConfigManager.get().moderation(); + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/VanishManager.java b/src/main/java/com/hyperessentials/module/moderation/VanishManager.java new file mode 100644 index 0000000..a5a6165 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/VanishManager.java @@ -0,0 +1,140 @@ +package com.hyperessentials.module.moderation; + +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages vanish state for players. Session-only (not persisted). + * Uses HiddenPlayersManager to hide vanished players from others. + */ +public class VanishManager { + + private final Set vanishedPlayers = ConcurrentHashMap.newKeySet(); + + public boolean isVanished(@NotNull UUID playerUuid) { + return vanishedPlayers.contains(playerUuid); + } + + /** + * Toggles vanish for a player. + * + * @return true if now vanished, false if un-vanished + */ + public boolean toggleVanish(@NotNull UUID playerUuid, @NotNull PlayerRef playerRef) { + if (vanishedPlayers.contains(playerUuid)) { + unvanish(playerUuid, playerRef); + return false; + } else { + vanish(playerUuid, playerRef); + return true; + } + } + + public void vanish(@NotNull UUID playerUuid, @NotNull PlayerRef playerRef) { + vanishedPlayers.add(playerUuid); + hideFromAll(playerUuid); + + if (ConfigManager.get().vanish().isFakeLeaveMessage()) { + broadcastFakeMessage(playerRef.getUsername() + " left the game."); + } + + Logger.debug("[VanishManager] %s vanished", playerRef.getUsername()); + } + + public void unvanish(@NotNull UUID playerUuid, @NotNull PlayerRef playerRef) { + vanishedPlayers.remove(playerUuid); + showToAll(playerUuid); + + if (ConfigManager.get().vanish().isFakeJoinMessage()) { + broadcastFakeMessage(playerRef.getUsername() + " joined the game."); + } + + Logger.debug("[VanishManager] %s un-vanished", playerRef.getUsername()); + } + + /** + * Called when a new player connects. Hides all vanished players from them. + */ + public void onPlayerConnect(@NotNull UUID newPlayerUuid, @NotNull PlayerRef newPlayerRef) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null) return; + + for (UUID vanishedUuid : vanishedPlayers) { + try { + newPlayerRef.getHiddenPlayersManager().hidePlayer(vanishedUuid); + } catch (Exception e) { + Logger.debug("[VanishManager] Failed to hide %s from new player: %s", vanishedUuid, e.getMessage()); + } + } + } + + /** + * Called when a player disconnects. Removes them from vanish if vanished. + */ + public void onPlayerDisconnect(@NotNull UUID playerUuid) { + vanishedPlayers.remove(playerUuid); + } + + @NotNull + public Set getVanishedPlayers() { + return Set.copyOf(vanishedPlayers); + } + + public void shutdown() { + // Show all vanished players before shutdown + for (UUID uuid : vanishedPlayers) { + showToAll(uuid); + } + vanishedPlayers.clear(); + } + + private void hideFromAll(@NotNull UUID vanishedUuid) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null) return; + + for (Map.Entry entry : plugin.getTrackedPlayers().entrySet()) { + if (!entry.getKey().equals(vanishedUuid)) { + try { + entry.getValue().getHiddenPlayersManager().hidePlayer(vanishedUuid); + } catch (Exception e) { + Logger.debug("[VanishManager] Failed to hide from player: %s", e.getMessage()); + } + } + } + } + + private void showToAll(@NotNull UUID vanishedUuid) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null) return; + + for (Map.Entry entry : plugin.getTrackedPlayers().entrySet()) { + if (!entry.getKey().equals(vanishedUuid)) { + try { + entry.getValue().getHiddenPlayersManager().showPlayer(vanishedUuid); + } catch (Exception e) { + Logger.debug("[VanishManager] Failed to show to player: %s", e.getMessage()); + } + } + } + } + + private void broadcastFakeMessage(@NotNull String text) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null) return; + + Message msg = Message.raw(text).color(CommandUtil.COLOR_YELLOW); + for (PlayerRef player : plugin.getTrackedPlayers().values()) { + player.sendMessage(msg); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/command/BanCommand.java b/src/main/java/com/hyperessentials/module/moderation/command/BanCommand.java new file mode 100644 index 0000000..b3b2131 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/command/BanCommand.java @@ -0,0 +1,99 @@ +package com.hyperessentials.module.moderation.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.moderation.ModerationModule; +import com.hyperessentials.util.DurationParser; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /ban [duration] [reason...] - Ban a player (permanent or temporary). + * If the second argument parses as a duration, it's a temp ban; otherwise treat as reason. + * Aliases: /tempban + */ +public class BanCommand extends AbstractPlayerCommand { + + private final ModerationModule module; + + public BanCommand(@NotNull ModerationModule module) { + super("ban", "Ban a player"); + addAliases("tempban"); + this.module = module; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.MODERATION_BAN)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to ban players.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /ban [duration] [reason...]")); + return; + } + + String targetName = parts[1]; + Long durationMs = null; + String reason = null; + + if (parts.length > 2) { + // Try to parse second arg as duration + long parsed = DurationParser.parse(parts[2]); + if (parsed > 0) { + durationMs = parsed; + reason = parts.length > 3 ? joinArgs(parts, 3) : null; + } else { + // Not a duration, treat as start of reason + reason = joinArgs(parts, 2); + } + } + + // Resolve target + UUID targetUuid = module.getModerationManager().findPlayerUuid(targetName); + if (targetUuid == null) { + ctx.sendMessage(CommandUtil.error("Player '" + targetName + "' not found.")); + return; + } + + // Check bypass + if (CommandUtil.hasPermission(targetUuid, Permissions.BYPASS_BAN)) { + ctx.sendMessage(CommandUtil.error("That player cannot be banned.")); + return; + } + + module.getModerationManager().ban(targetUuid, targetName, playerRef.getUuid(), playerRef.getUsername(), reason, durationMs); + + if (durationMs != null) { + ctx.sendMessage(CommandUtil.success("Banned " + targetName + " for " + DurationParser.formatHuman(durationMs) + ".")); + } else { + ctx.sendMessage(CommandUtil.success("Permanently banned " + targetName + ".")); + } + } + + private String joinArgs(String[] parts, int start) { + StringBuilder sb = new StringBuilder(); + for (int i = start; i < parts.length; i++) { + if (i > start) sb.append(' '); + sb.append(parts[i]); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/command/FreezeCommand.java b/src/main/java/com/hyperessentials/module/moderation/command/FreezeCommand.java new file mode 100644 index 0000000..8978baf --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/command/FreezeCommand.java @@ -0,0 +1,84 @@ +package com.hyperessentials.module.moderation.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.data.Location; +import com.hyperessentials.module.moderation.ModerationModule; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Transform; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /freeze - Toggle freeze on a player. + */ +public class FreezeCommand extends AbstractPlayerCommand { + + private final ModerationModule module; + + public FreezeCommand(@NotNull ModerationModule module) { + super("freeze", "Toggle freeze on a player"); + this.module = module; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.MODERATION_FREEZE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to freeze players.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /freeze ")); + return; + } + + String targetName = parts[1]; + PlayerRef target = CommandUtil.findOnlinePlayer(targetName); + if (target == null) { + ctx.sendMessage(CommandUtil.error("Player '" + targetName + "' is not online.")); + return; + } + + if (CommandUtil.hasPermission(target.getUuid(), Permissions.BYPASS_FREEZE)) { + ctx.sendMessage(CommandUtil.error("That player cannot be frozen.")); + return; + } + + if (module.getFreezeManager().isFrozen(target.getUuid())) { + module.getFreezeManager().unfreeze(target.getUuid()); + target.sendMessage(CommandUtil.success("You have been unfrozen.")); + ctx.sendMessage(CommandUtil.success("Unfroze " + target.getUsername() + ".")); + } else { + Transform transform = target.getTransform(); + Vector3d pos = transform.getPosition(); + Location loc = new Location( + world.getName(), + pos.getX(), + pos.getY(), + pos.getZ(), + 0, 0 + ); + module.getFreezeManager().freeze(target.getUuid(), loc); + + String freezeMsg = ConfigManager.get().moderation().getFreezeMessage(); + target.sendMessage(CommandUtil.error(freezeMsg)); + ctx.sendMessage(CommandUtil.success("Froze " + target.getUsername() + ".")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/command/IpBanCommand.java b/src/main/java/com/hyperessentials/module/moderation/command/IpBanCommand.java new file mode 100644 index 0000000..fe66120 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/command/IpBanCommand.java @@ -0,0 +1,95 @@ +package com.hyperessentials.module.moderation.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.moderation.ModerationModule; +import com.hyperessentials.util.DurationParser; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /ipban [duration] [reason...] - Ban a player's IP address. + */ +public class IpBanCommand extends AbstractPlayerCommand { + + private final ModerationModule module; + + public IpBanCommand(@NotNull ModerationModule module) { + super("ipban", "Ban a player's IP address"); + this.module = module; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.MODERATION_IPBAN)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to IP ban players.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /ipban [duration] [reason...]")); + return; + } + + String targetName = parts[1]; + Long durationMs = null; + String reason = null; + + if (parts.length > 2) { + long parsed = DurationParser.parse(parts[2]); + if (parsed > 0) { + durationMs = parsed; + reason = parts.length > 3 ? joinArgs(parts, 3) : null; + } else { + reason = joinArgs(parts, 2); + } + } + + // Target must be online to get their IP + PlayerRef target = CommandUtil.findOnlinePlayer(targetName); + if (target == null) { + ctx.sendMessage(CommandUtil.error("Player '" + targetName + "' must be online to IP ban.")); + return; + } + + String ip = module.getModerationManager().getPlayerIp(target.getUuid()); + if (ip == null) { + ctx.sendMessage(CommandUtil.error("Could not resolve IP for " + targetName + ".")); + return; + } + + module.getModerationManager().ipBan(ip, playerRef.getUuid(), playerRef.getUsername(), reason, durationMs); + + // Kick the target and anyone else on the same IP + module.getModerationManager().kickPlayersWithIp(ip, "Your IP has been banned."); + + if (durationMs != null) { + ctx.sendMessage(CommandUtil.success("IP banned " + targetName + " (" + ip + ") for " + DurationParser.formatHuman(durationMs) + ".")); + } else { + ctx.sendMessage(CommandUtil.success("Permanently IP banned " + targetName + " (" + ip + ").")); + } + } + + private String joinArgs(String[] parts, int start) { + StringBuilder sb = new StringBuilder(); + for (int i = start; i < parts.length; i++) { + if (i > start) sb.append(' '); + sb.append(parts[i]); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/command/IpUnbanCommand.java b/src/main/java/com/hyperessentials/module/moderation/command/IpUnbanCommand.java new file mode 100644 index 0000000..c00baff --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/command/IpUnbanCommand.java @@ -0,0 +1,55 @@ +package com.hyperessentials.module.moderation.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.moderation.ModerationModule; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /ipunban - Unban an IP address. + */ +public class IpUnbanCommand extends AbstractPlayerCommand { + + private final ModerationModule module; + + public IpUnbanCommand(@NotNull ModerationModule module) { + super("ipunban", "Unban an IP address"); + this.module = module; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.MODERATION_IPBAN)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to manage IP bans.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /ipunban ")); + return; + } + + String ip = parts[1]; + + if (module.getModerationManager().ipUnban(ip)) { + ctx.sendMessage(CommandUtil.success("Unbanned IP: " + ip)); + } else { + ctx.sendMessage(CommandUtil.error("IP '" + ip + "' is not banned.")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/command/KickCommand.java b/src/main/java/com/hyperessentials/module/moderation/command/KickCommand.java new file mode 100644 index 0000000..76efb2c --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/command/KickCommand.java @@ -0,0 +1,71 @@ +package com.hyperessentials.module.moderation.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.moderation.ModerationModule; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /kick [reason...] - Kick a player from the server. + */ +public class KickCommand extends AbstractPlayerCommand { + + private final ModerationModule module; + + public KickCommand(@NotNull ModerationModule module) { + super("kick", "Kick a player"); + this.module = module; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.MODERATION_KICK)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to kick players.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /kick [reason...]")); + return; + } + + String targetName = parts[1]; + String reason = parts.length > 2 ? joinArgs(parts, 2) : null; + + PlayerRef target = CommandUtil.findOnlinePlayer(targetName); + if (target == null) { + ctx.sendMessage(CommandUtil.error("Player '" + targetName + "' is not online.")); + return; + } + + module.getModerationManager().kick( + target.getUuid(), target.getUsername(), + playerRef.getUuid(), playerRef.getUsername(), reason + ); + ctx.sendMessage(CommandUtil.success("Kicked " + target.getUsername() + ".")); + } + + private String joinArgs(String[] parts, int start) { + StringBuilder sb = new StringBuilder(); + for (int i = start; i < parts.length; i++) { + if (i > start) sb.append(' '); + sb.append(parts[i]); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/command/MuteCommand.java b/src/main/java/com/hyperessentials/module/moderation/command/MuteCommand.java new file mode 100644 index 0000000..90adfc4 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/command/MuteCommand.java @@ -0,0 +1,95 @@ +package com.hyperessentials.module.moderation.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.moderation.ModerationModule; +import com.hyperessentials.util.DurationParser; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /mute [duration] [reason...] - Mute a player (permanent or temporary). + * If the second argument parses as a duration, it's a temp mute; otherwise treat as reason. + * Aliases: /tempmute, /tmute + */ +public class MuteCommand extends AbstractPlayerCommand { + + private final ModerationModule module; + + public MuteCommand(@NotNull ModerationModule module) { + super("mute", "Mute a player"); + addAliases("tempmute", "tmute"); + this.module = module; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.MODERATION_MUTE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to mute players.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /mute [duration] [reason...]")); + return; + } + + String targetName = parts[1]; + Long durationMs = null; + String reason = null; + + if (parts.length > 2) { + long parsed = DurationParser.parse(parts[2]); + if (parsed > 0) { + durationMs = parsed; + reason = parts.length > 3 ? joinArgs(parts, 3) : null; + } else { + reason = joinArgs(parts, 2); + } + } + + UUID targetUuid = module.getModerationManager().findPlayerUuid(targetName); + if (targetUuid == null) { + ctx.sendMessage(CommandUtil.error("Player '" + targetName + "' not found.")); + return; + } + + if (CommandUtil.hasPermission(targetUuid, Permissions.BYPASS_MUTE)) { + ctx.sendMessage(CommandUtil.error("That player cannot be muted.")); + return; + } + + module.getModerationManager().mute(targetUuid, targetName, playerRef.getUuid(), playerRef.getUsername(), reason, durationMs); + + if (durationMs != null) { + ctx.sendMessage(CommandUtil.success("Muted " + targetName + " for " + DurationParser.formatHuman(durationMs) + ".")); + } else { + ctx.sendMessage(CommandUtil.success("Permanently muted " + targetName + ".")); + } + } + + private String joinArgs(String[] parts, int start) { + StringBuilder sb = new StringBuilder(); + for (int i = start; i < parts.length; i++) { + if (i > start) sb.append(' '); + sb.append(parts[i]); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/command/PunishmentsCommand.java b/src/main/java/com/hyperessentials/module/moderation/command/PunishmentsCommand.java new file mode 100644 index 0000000..a61bbaa --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/command/PunishmentsCommand.java @@ -0,0 +1,101 @@ +package com.hyperessentials.module.moderation.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.moderation.ModerationModule; +import com.hyperessentials.module.moderation.data.Punishment; +import com.hyperessentials.util.DurationParser; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.UUID; + +/** + * /punishments - View a player's punishment history. + */ +public class PunishmentsCommand extends AbstractPlayerCommand { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + .withZone(ZoneId.systemDefault()); + + private final ModerationModule module; + + public PunishmentsCommand(@NotNull ModerationModule module) { + super("punishments", "View punishment history"); + this.module = module; + addAliases("pun"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.MODERATION_HISTORY)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to view punishment history.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /punishments ")); + return; + } + + String targetName = parts[1]; + UUID targetUuid = module.getModerationManager().findPlayerUuid(targetName); + if (targetUuid == null) { + ctx.sendMessage(CommandUtil.error("Player '" + targetName + "' not found.")); + return; + } + + List history = module.getModerationManager().getHistory(targetUuid); + if (history.isEmpty()) { + ctx.sendMessage(CommandUtil.info("No punishments found for " + targetName + ".")); + return; + } + + ctx.sendMessage(CommandUtil.info("Punishment History for " + targetName + " (" + history.size() + "):")); + + int shown = 0; + for (int i = history.size() - 1; i >= 0 && shown < 10; i--, shown++) { + Punishment p = history.get(i); + String status = p.isEffective() ? "[ACTIVE]" : (p.active() ? "[EXPIRED]" : "[REVOKED]"); + String statusColor = p.isEffective() ? CommandUtil.COLOR_RED : CommandUtil.COLOR_GRAY; + String duration = p.isPermanent() ? "permanent" : DurationParser.formatCompact( + p.expiresAt().toEpochMilli() - p.issuedAt().toEpochMilli()); + + Message line = CommandUtil.prefix() + .insert(Message.raw(" " + status + " ").color(statusColor)) + .insert(Message.raw(p.type().name() + " ").color(CommandUtil.COLOR_YELLOW)) + .insert(Message.raw("by " + p.issuerName() + " ").color(CommandUtil.COLOR_WHITE)) + .insert(Message.raw("(" + duration + ") ").color(CommandUtil.COLOR_GRAY)) + .insert(Message.raw(DATE_FMT.format(p.issuedAt())).color(CommandUtil.COLOR_DARK_GRAY)); + + ctx.sendMessage(line); + + if (p.reason() != null) { + ctx.sendMessage(CommandUtil.msg(" Reason: " + p.reason(), CommandUtil.COLOR_GRAY)); + } + } + + if (history.size() > 10) { + ctx.sendMessage(CommandUtil.msg(" ... and " + (history.size() - 10) + " more", CommandUtil.COLOR_DARK_GRAY)); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/command/UnbanCommand.java b/src/main/java/com/hyperessentials/module/moderation/command/UnbanCommand.java new file mode 100644 index 0000000..e69ef07 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/command/UnbanCommand.java @@ -0,0 +1,62 @@ +package com.hyperessentials.module.moderation.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.moderation.ModerationModule; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /unban - Revoke a player's ban. + */ +public class UnbanCommand extends AbstractPlayerCommand { + + private final ModerationModule module; + + public UnbanCommand(@NotNull ModerationModule module) { + super("unban", "Unban a player"); + this.module = module; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.MODERATION_BAN)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to unban players.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /unban ")); + return; + } + + String targetName = parts[1]; + UUID targetUuid = module.getModerationManager().findPlayerUuid(targetName); + if (targetUuid == null) { + ctx.sendMessage(CommandUtil.error("Player '" + targetName + "' not found.")); + return; + } + + if (module.getModerationManager().unban(targetUuid, playerRef.getUuid(), playerRef.getUsername())) { + ctx.sendMessage(CommandUtil.success("Unbanned " + targetName + ".")); + } else { + ctx.sendMessage(CommandUtil.error(targetName + " is not banned.")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/command/UnmuteCommand.java b/src/main/java/com/hyperessentials/module/moderation/command/UnmuteCommand.java new file mode 100644 index 0000000..6a3c189 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/command/UnmuteCommand.java @@ -0,0 +1,62 @@ +package com.hyperessentials.module.moderation.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.moderation.ModerationModule; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /unmute - Revoke a player's mute. + */ +public class UnmuteCommand extends AbstractPlayerCommand { + + private final ModerationModule module; + + public UnmuteCommand(@NotNull ModerationModule module) { + super("unmute", "Unmute a player"); + this.module = module; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.MODERATION_MUTE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to unmute players.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /unmute ")); + return; + } + + String targetName = parts[1]; + UUID targetUuid = module.getModerationManager().findPlayerUuid(targetName); + if (targetUuid == null) { + ctx.sendMessage(CommandUtil.error("Player '" + targetName + "' not found.")); + return; + } + + if (module.getModerationManager().unmute(targetUuid, playerRef.getUuid(), playerRef.getUsername())) { + ctx.sendMessage(CommandUtil.success("Unmuted " + targetName + ".")); + } else { + ctx.sendMessage(CommandUtil.error(targetName + " is not muted.")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/command/VanishCommand.java b/src/main/java/com/hyperessentials/module/moderation/command/VanishCommand.java new file mode 100644 index 0000000..25ad5d4 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/command/VanishCommand.java @@ -0,0 +1,47 @@ +package com.hyperessentials.module.moderation.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.module.moderation.ModerationModule; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /vanish - Toggle vanish mode (self only). + */ +public class VanishCommand extends AbstractPlayerCommand { + + private final ModerationModule module; + + public VanishCommand(@NotNull ModerationModule module) { + super("vanish", "Toggle vanish mode"); + this.module = module; + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.MODERATION_VANISH)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to vanish.")); + return; + } + + boolean nowVanished = module.getVanishManager().toggleVanish(playerRef.getUuid(), playerRef); + + if (nowVanished) { + ctx.sendMessage(CommandUtil.success(ConfigManager.get().vanish().getVanishEnableMessage())); + } else { + ctx.sendMessage(CommandUtil.success(ConfigManager.get().vanish().getVanishDisableMessage())); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/data/IpBan.java b/src/main/java/com/hyperessentials/module/moderation/data/IpBan.java new file mode 100644 index 0000000..955d7ed --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/data/IpBan.java @@ -0,0 +1,29 @@ +package com.hyperessentials.module.moderation.data; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; +import java.util.UUID; + +/** + * Represents an IP ban record. + */ +public record IpBan( + @NotNull String ip, + @Nullable String reason, + @Nullable UUID issuerUuid, + @NotNull String issuerName, + @NotNull Instant issuedAt, + @Nullable Instant expiresAt +) { + public boolean isPermanent() { return expiresAt == null; } + public boolean hasExpired() { return expiresAt != null && Instant.now().isAfter(expiresAt); } + public boolean isEffective() { return !hasExpired(); } + + public long getRemainingMillis() { + if (expiresAt == null) return Long.MAX_VALUE; + long remaining = expiresAt.toEpochMilli() - System.currentTimeMillis(); + return Math.max(0, remaining); + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/data/Punishment.java b/src/main/java/com/hyperessentials/module/moderation/data/Punishment.java new file mode 100644 index 0000000..068b5b0 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/data/Punishment.java @@ -0,0 +1,60 @@ +package com.hyperessentials.module.moderation.data; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; +import java.util.UUID; + +/** + * Represents a punishment record (ban, mute, or kick). + */ +public record Punishment( + @NotNull UUID id, + @NotNull PunishmentType type, + @NotNull UUID playerUuid, + @NotNull String playerName, + @Nullable UUID issuerUuid, + @NotNull String issuerName, + @Nullable String reason, + @NotNull Instant issuedAt, + @Nullable Instant expiresAt, + boolean active, + @Nullable UUID revokedBy, + @Nullable Instant revokedAt +) { + public boolean isPermanent() { + return expiresAt == null; + } + + public boolean hasExpired() { + return expiresAt != null && Instant.now().isAfter(expiresAt); + } + + /** + * Returns whether this punishment is currently in effect. + */ + public boolean isEffective() { + return active && !hasExpired(); + } + + /** + * Creates a revoked copy of this punishment. + */ + @NotNull + public Punishment revoke(@Nullable UUID revokerUuid, @NotNull String revokerName) { + return new Punishment( + id, type, playerUuid, playerName, issuerUuid, issuerName, + reason, issuedAt, expiresAt, false, revokerUuid, Instant.now() + ); + } + + /** + * Returns the remaining time in milliseconds, or 0 if expired/permanent. + */ + public long getRemainingMillis() { + if (expiresAt == null) return Long.MAX_VALUE; + long remaining = expiresAt.toEpochMilli() - System.currentTimeMillis(); + return Math.max(0, remaining); + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/data/PunishmentType.java b/src/main/java/com/hyperessentials/module/moderation/data/PunishmentType.java new file mode 100644 index 0000000..4c7dccb --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/data/PunishmentType.java @@ -0,0 +1,10 @@ +package com.hyperessentials.module.moderation.data; + +/** + * Types of punishments tracked by the moderation system. + */ +public enum PunishmentType { + BAN, + MUTE, + KICK +} diff --git a/src/main/java/com/hyperessentials/module/moderation/listener/ModerationListener.java b/src/main/java/com/hyperessentials/module/moderation/listener/ModerationListener.java new file mode 100644 index 0000000..40003be --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/listener/ModerationListener.java @@ -0,0 +1,100 @@ +package com.hyperessentials.module.moderation.listener; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.module.moderation.ModerationManager; +import com.hyperessentials.module.moderation.ModerationModule; +import com.hyperessentials.module.moderation.VanishManager; +import com.hyperessentials.module.moderation.data.IpBan; +import com.hyperessentials.module.moderation.data.Punishment; +import com.hyperessentials.util.DurationParser; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.server.core.event.events.player.PlayerChatEvent; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import org.jetbrains.annotations.NotNull; + +import java.net.InetSocketAddress; + +/** + * Handles player connect and chat events for ban/mute enforcement, IP bans, and vanish. + */ +public class ModerationListener { + + private final ModerationModule module; + + public ModerationListener(@NotNull ModerationModule module) { + this.module = module; + } + + /** + * Called on player connect. Checks ban and IP ban status, applies vanish hiding. + */ + public void onPlayerConnect(@NotNull PlayerConnectEvent event) { + PlayerRef playerRef = event.getPlayerRef(); + ModerationManager modManager = module.getModerationManager(); + + // Check for active ban + Punishment ban = modManager.getActiveBan(playerRef.getUuid()); + if (ban != null && ban.isEffective()) { + StringBuilder message = new StringBuilder("You are banned from this server."); + if (ban.reason() != null) { + message.append("\nReason: ").append(ban.reason()); + } + if (!ban.isPermanent()) { + message.append("\nExpires in: ").append(DurationParser.formatHuman(ban.getRemainingMillis())); + } + + try { + playerRef.getPacketHandler().disconnect(message.toString()); + } catch (Exception e) { + Logger.warn("[Moderation] Failed to disconnect banned player: %s", e.getMessage()); + } + return; + } + + // Check for IP ban + try { + InetSocketAddress addr = (InetSocketAddress) playerRef.getPacketHandler() + .getChannel().remoteAddress(); + if (addr != null) { + String ip = addr.getAddress().getHostAddress(); + if (modManager.isIpBanned(ip)) { + try { + playerRef.getPacketHandler().disconnect("Your IP address has been banned."); + } catch (Exception e) { + Logger.warn("[Moderation] Failed to disconnect IP-banned player: %s", e.getMessage()); + } + return; + } + } + } catch (Exception e) { + Logger.debug("[Moderation] Failed to check IP ban for %s: %s", playerRef.getUsername(), e.getMessage()); + } + + // Hide vanished players from the new player + VanishManager vanishManager = module.getVanishManager(); + vanishManager.onPlayerConnect(playerRef.getUuid(), playerRef); + } + + /** + * Called on player chat. Checks mute status. + */ + public void onPlayerChat(@NotNull PlayerChatEvent event) { + PlayerRef playerRef = event.getSender(); + + // Check bypass + if (CommandUtil.hasPermission(playerRef.getUuid(), Permissions.BYPASS_MUTE)) { + return; + } + + // Check mute + ModerationManager modManager = module.getModerationManager(); + if (modManager.isMuted(playerRef.getUuid())) { + event.setCancelled(true); + String muteMsg = ConfigManager.get().moderation().getMutedChatMessage(); + playerRef.sendMessage(CommandUtil.error(muteMsg)); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/moderation/storage/ModerationStorage.java b/src/main/java/com/hyperessentials/module/moderation/storage/ModerationStorage.java new file mode 100644 index 0000000..c5fcb4f --- /dev/null +++ b/src/main/java/com/hyperessentials/module/moderation/storage/ModerationStorage.java @@ -0,0 +1,330 @@ +package com.hyperessentials.module.moderation.storage; + +import com.google.gson.*; +import com.hyperessentials.module.moderation.data.IpBan; +import com.hyperessentials.module.moderation.data.Punishment; +import com.hyperessentials.module.moderation.data.PunishmentType; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * JSON persistence for punishment records. + * File: data/punishments.json + */ +public class ModerationStorage { + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + + private final Path filePath; + private final Map> punishments = new ConcurrentHashMap<>(); + private final Map ipBans = new ConcurrentHashMap<>(); + + public ModerationStorage(@NotNull Path dataDir) { + this.filePath = dataDir.resolve("data").resolve("punishments.json"); + } + + public void load() { + punishments.clear(); + ipBans.clear(); + + if (!Files.exists(filePath)) { + Logger.info("[ModerationStorage] No punishments file found, starting fresh"); + return; + } + + try { + String json = Files.readString(filePath); + JsonObject root = JsonParser.parseString(json).getAsJsonObject(); + + if (root.has("players") && root.get("players").isJsonObject()) { + JsonObject players = root.getAsJsonObject("players"); + for (Map.Entry entry : players.entrySet()) { + UUID playerUuid = UUID.fromString(entry.getKey()); + JsonObject playerObj = entry.getValue().getAsJsonObject(); + + if (playerObj.has("punishments") && playerObj.get("punishments").isJsonArray()) { + List list = new ArrayList<>(); + for (JsonElement el : playerObj.getAsJsonArray("punishments")) { + Punishment p = deserialize(el.getAsJsonObject()); + if (p != null) list.add(p); + } + if (!list.isEmpty()) { + punishments.put(playerUuid, Collections.synchronizedList(list)); + } + } + } + } + + // Load IP bans + if (root.has("ipBans") && root.get("ipBans").isJsonObject()) { + JsonObject bans = root.getAsJsonObject("ipBans"); + for (Map.Entry entry : bans.entrySet()) { + try { + IpBan ban = deserializeIpBan(entry.getKey(), entry.getValue().getAsJsonObject()); + if (ban != null) ipBans.put(entry.getKey(), ban); + } catch (Exception e) { + Logger.warn("[ModerationStorage] Failed to parse IP ban for %s: %s", entry.getKey(), e.getMessage()); + } + } + } + + Logger.info("[ModerationStorage] Loaded punishments for %d player(s), %d IP ban(s)", punishments.size(), ipBans.size()); + } catch (Exception e) { + Logger.severe("[ModerationStorage] Failed to load punishments: %s", e.getMessage()); + } + } + + public void save() { + try { + Files.createDirectories(filePath.getParent()); + + JsonObject root = new JsonObject(); + JsonObject players = new JsonObject(); + + for (Map.Entry> entry : punishments.entrySet()) { + JsonObject playerObj = new JsonObject(); + List list = entry.getValue(); + + if (!list.isEmpty()) { + playerObj.addProperty("playerName", list.getFirst().playerName()); + } + + JsonArray arr = new JsonArray(); + synchronized (list) { + for (Punishment p : list) { + arr.add(serialize(p)); + } + } + playerObj.add("punishments", arr); + + players.add(entry.getKey().toString(), playerObj); + } + + root.add("players", players); + + // Save IP bans + JsonObject ipBansObj = new JsonObject(); + for (Map.Entry entry : ipBans.entrySet()) { + ipBansObj.add(entry.getKey(), serializeIpBan(entry.getValue())); + } + root.add("ipBans", ipBansObj); + + Files.writeString(filePath, GSON.toJson(root)); + Logger.debug("[ModerationStorage] Saved punishments"); + } catch (IOException e) { + Logger.severe("[ModerationStorage] Failed to save punishments: %s", e.getMessage()); + } + } + + public void addPunishment(@NotNull Punishment punishment) { + punishments.computeIfAbsent(punishment.playerUuid(), + k -> Collections.synchronizedList(new ArrayList<>())).add(punishment); + save(); + } + + /** + * Updates a punishment in-place (e.g., revoking). + */ + public void updatePunishment(@NotNull Punishment updated) { + List list = punishments.get(updated.playerUuid()); + if (list == null) return; + + synchronized (list) { + for (int i = 0; i < list.size(); i++) { + if (list.get(i).id().equals(updated.id())) { + list.set(i, updated); + break; + } + } + } + save(); + } + + /** + * Gets all punishments for a player. + */ + @NotNull + public List getPunishments(@NotNull UUID playerUuid) { + List list = punishments.get(playerUuid); + if (list == null) return List.of(); + synchronized (list) { + return new ArrayList<>(list); + } + } + + /** + * Gets the active ban for a player, if any. + */ + @Nullable + public Punishment getActiveBan(@NotNull UUID playerUuid) { + return getActivePunishment(playerUuid, PunishmentType.BAN); + } + + /** + * Gets the active mute for a player, if any. + */ + @Nullable + public Punishment getActiveMute(@NotNull UUID playerUuid) { + return getActivePunishment(playerUuid, PunishmentType.MUTE); + } + + @Nullable + private Punishment getActivePunishment(@NotNull UUID playerUuid, @NotNull PunishmentType type) { + List list = punishments.get(playerUuid); + if (list == null) return null; + + synchronized (list) { + for (Punishment p : list) { + if (p.type() == type && p.isEffective()) { + return p; + } + } + } + return null; + } + + /** + * Gets all punishments across all players. + * @param activeOnly if true, only returns punishments that are currently effective + */ + @NotNull + public List getAllPunishments(boolean activeOnly) { + List result = new ArrayList<>(); + for (List list : punishments.values()) { + synchronized (list) { + for (Punishment p : list) { + if (!activeOnly || p.isEffective()) { + result.add(p); + } + } + } + } + return result; + } + + /** + * Finds a player UUID by name from stored punishment records. + */ + @Nullable + public UUID findPlayerUuid(@NotNull String name) { + for (Map.Entry> entry : punishments.entrySet()) { + List list = entry.getValue(); + synchronized (list) { + for (Punishment p : list) { + if (p.playerName().equalsIgnoreCase(name)) { + return entry.getKey(); + } + } + } + } + return null; + } + + private JsonObject serialize(@NotNull Punishment p) { + JsonObject obj = new JsonObject(); + obj.addProperty("id", p.id().toString()); + obj.addProperty("type", p.type().name()); + obj.addProperty("playerUuid", p.playerUuid().toString()); + obj.addProperty("playerName", p.playerName()); + obj.addProperty("issuerUuid", p.issuerUuid() != null ? p.issuerUuid().toString() : null); + obj.addProperty("issuerName", p.issuerName()); + obj.addProperty("reason", p.reason()); + obj.addProperty("issuedAt", p.issuedAt().toEpochMilli()); + obj.addProperty("expiresAt", p.expiresAt() != null ? p.expiresAt().toEpochMilli() : null); + obj.addProperty("active", p.active()); + obj.addProperty("revokedBy", p.revokedBy() != null ? p.revokedBy().toString() : null); + obj.addProperty("revokedAt", p.revokedAt() != null ? p.revokedAt().toEpochMilli() : null); + return obj; + } + + // === IP Ban Operations === + + public void addIpBan(@NotNull IpBan ban) { + ipBans.put(ban.ip(), ban); + save(); + } + + public boolean removeIpBan(@NotNull String ip) { + IpBan removed = ipBans.remove(ip); + if (removed != null) { + save(); + return true; + } + return false; + } + + @Nullable + public IpBan getIpBan(@NotNull String ip) { + return ipBans.get(ip); + } + + @NotNull + public Map getAllIpBans() { + return Collections.unmodifiableMap(ipBans); + } + + private JsonObject serializeIpBan(@NotNull IpBan ban) { + JsonObject obj = new JsonObject(); + obj.addProperty("reason", ban.reason()); + obj.addProperty("issuerUuid", ban.issuerUuid() != null ? ban.issuerUuid().toString() : null); + obj.addProperty("issuerName", ban.issuerName()); + obj.addProperty("issuedAt", ban.issuedAt().toEpochMilli()); + obj.addProperty("expiresAt", ban.expiresAt() != null ? ban.expiresAt().toEpochMilli() : null); + return obj; + } + + @Nullable + private IpBan deserializeIpBan(@NotNull String ip, @NotNull JsonObject obj) { + try { + return new IpBan( + ip, + obj.has("reason") && !obj.get("reason").isJsonNull() ? obj.get("reason").getAsString() : null, + obj.has("issuerUuid") && !obj.get("issuerUuid").isJsonNull() + ? UUID.fromString(obj.get("issuerUuid").getAsString()) : null, + obj.get("issuerName").getAsString(), + Instant.ofEpochMilli(obj.get("issuedAt").getAsLong()), + obj.has("expiresAt") && !obj.get("expiresAt").isJsonNull() + ? Instant.ofEpochMilli(obj.get("expiresAt").getAsLong()) : null + ); + } catch (Exception e) { + Logger.warn("[ModerationStorage] Failed to parse IP ban: %s", e.getMessage()); + return null; + } + } + + @Nullable + private Punishment deserialize(@NotNull JsonObject obj) { + try { + return new Punishment( + UUID.fromString(obj.get("id").getAsString()), + PunishmentType.valueOf(obj.get("type").getAsString()), + UUID.fromString(obj.get("playerUuid").getAsString()), + obj.get("playerName").getAsString(), + obj.has("issuerUuid") && !obj.get("issuerUuid").isJsonNull() + ? UUID.fromString(obj.get("issuerUuid").getAsString()) : null, + obj.get("issuerName").getAsString(), + obj.has("reason") && !obj.get("reason").isJsonNull() + ? obj.get("reason").getAsString() : null, + Instant.ofEpochMilli(obj.get("issuedAt").getAsLong()), + obj.has("expiresAt") && !obj.get("expiresAt").isJsonNull() + ? Instant.ofEpochMilli(obj.get("expiresAt").getAsLong()) : null, + obj.get("active").getAsBoolean(), + obj.has("revokedBy") && !obj.get("revokedBy").isJsonNull() + ? UUID.fromString(obj.get("revokedBy").getAsString()) : null, + obj.has("revokedAt") && !obj.get("revokedAt").isJsonNull() + ? Instant.ofEpochMilli(obj.get("revokedAt").getAsLong()) : null + ); + } catch (Exception e) { + Logger.warn("[ModerationStorage] Failed to parse punishment: %s", e.getMessage()); + return null; + } + } +} diff --git a/src/main/java/com/hyperessentials/module/rtp/RtpModule.java b/src/main/java/com/hyperessentials/module/rtp/RtpModule.java deleted file mode 100644 index a47f63c..0000000 --- a/src/main/java/com/hyperessentials/module/rtp/RtpModule.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.hyperessentials.module.rtp; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.config.ModuleConfig; -import com.hyperessentials.module.AbstractModule; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Random Teleport module for HyperEssentials. - */ -public class RtpModule extends AbstractModule { - - @Override - @NotNull - public String getName() { - return "rtp"; - } - - @Override - @NotNull - public String getDisplayName() { - return "Random Teleport"; - } - - @Override - public void onEnable() { - super.onEnable(); - // TODO: Register commands, listeners, and storage - } - - @Override - public void onDisable() { - // TODO: Unregister commands, save data, cleanup - super.onDisable(); - } - - @Override - @Nullable - public ModuleConfig getModuleConfig() { - return ConfigManager.get().rtp(); - } -} diff --git a/src/main/java/com/hyperessentials/module/spawns/SpawnManager.java b/src/main/java/com/hyperessentials/module/spawns/SpawnManager.java new file mode 100644 index 0000000..7d8f87e --- /dev/null +++ b/src/main/java/com/hyperessentials/module/spawns/SpawnManager.java @@ -0,0 +1,258 @@ +package com.hyperessentials.module.spawns; + +import com.hyperessentials.config.modules.SpawnsConfig; +import com.hyperessentials.data.Spawn; +import com.hyperessentials.integration.PermissionManager; +import com.hyperessentials.storage.SpawnStorage; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.math.vector.Transform; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.spawn.ISpawnProvider; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Manages server spawns - loading, saving, and CRUD operations. + */ +public class SpawnManager { + + private final SpawnStorage storage; + private final SpawnsConfig config; + private final Map spawns; + + public SpawnManager(@NotNull SpawnStorage storage, @NotNull SpawnsConfig config) { + this.storage = storage; + this.config = config; + this.spawns = new ConcurrentHashMap<>(); + } + + public CompletableFuture loadSpawns() { + return storage.loadSpawns().thenAccept(loaded -> { + spawns.clear(); + spawns.putAll(loaded); + Logger.info("[Spawns] Loaded %d spawns", spawns.size()); + }); + } + + /** + * Auto-detects world spawn points from the server on first startup. + * Only runs if no spawns are configured (fresh install). + */ + public void autoDetectWorldSpawns() { + if (!spawns.isEmpty()) { + Logger.debug("[Spawns] Spawns already configured, skipping auto-detection"); + return; + } + importWorldSpawns(); + } + + /** + * Imports world spawn points from the Hytale server's WorldConfig spawn providers. + * Creates or updates spawn records for each world that has a configured spawn point. + * Existing user-created spawns that don't match a world name are preserved. + * The default world's spawn is marked as default if no default exists yet. + * + * @return the number of spawns imported + */ + public int importWorldSpawns() { + try { + Universe universe = Universe.get(); + if (universe == null) { + Logger.warn("[Spawns] Universe not available, cannot import spawns"); + return 0; + } + + Map worlds = universe.getWorlds(); + if (worlds.isEmpty()) { + Logger.warn("[Spawns] No worlds found, cannot import spawns"); + return 0; + } + + World defaultWorld = universe.getDefaultWorld(); + String defaultWorldName = defaultWorld != null ? defaultWorld.getName() : null; + boolean hasExistingDefault = getDefaultSpawn() != null; + int imported = 0; + + for (World world : worlds.values()) { + ISpawnProvider provider = world.getWorldConfig().getSpawnProvider(); + if (provider == null) { + Logger.debug("[Spawns] World '%s' has no spawn provider, skipping", world.getName()); + continue; + } + + @SuppressWarnings("deprecation") + Transform[] spawnPoints = provider.getSpawnPoints(); + if (spawnPoints == null || spawnPoints.length == 0) { + Logger.debug("[Spawns] World '%s' spawn provider returned no spawn points, skipping", world.getName()); + continue; + } + + Transform transform = spawnPoints[0]; + Vector3d pos = transform.getPosition(); + Vector3f rot = transform.getRotation(); + + String spawnName = world.getName().toLowerCase(); + boolean isDefault = !hasExistingDefault + && defaultWorldName != null + && world.getName().equalsIgnoreCase(defaultWorldName); + + Spawn spawn = new Spawn( + spawnName, + world.getName(), + pos.getX(), pos.getY(), pos.getZ(), + rot.getY(), rot.getX(), // yaw = rotation.y, pitch = rotation.x + null, // no permission + null, // no group permission + isDefault, + System.currentTimeMillis(), + "server" // created by server import + ); + + boolean isUpdate = spawns.containsKey(spawnName); + spawns.put(spawnName, spawn); + if (isDefault) hasExistingDefault = true; + imported++; + + Logger.info("[Spawns] %s spawn for world '%s' at %.1f, %.1f, %.1f%s", + isUpdate ? "Updated" : "Imported", + world.getName(), pos.getX(), pos.getY(), pos.getZ(), + isDefault ? " (default)" : ""); + } + + if (imported > 0) { + saveSpawns(); + Logger.info("[Spawns] Imported %d world spawn(s)", imported); + } + return imported; + } catch (Exception e) { + Logger.warn("[Spawns] Failed to import world spawns: %s", e.getMessage()); + return 0; + } + } + + public CompletableFuture saveSpawns() { + return storage.saveSpawns(new ConcurrentHashMap<>(spawns)); + } + + public boolean setSpawn(@NotNull Spawn spawn) { + if (spawn.isDefault()) { + for (Map.Entry entry : spawns.entrySet()) { + if (entry.getValue().isDefault() && !entry.getKey().equals(spawn.name())) { + spawns.put(entry.getKey(), entry.getValue().withDefault(false)); + } + } + } + + boolean isNew = !spawns.containsKey(spawn.name()); + spawns.put(spawn.name(), spawn); + saveSpawns(); + Logger.info("[Spawns] Spawn '%s' %s%s", spawn.name(), isNew ? "created" : "updated", + spawn.isDefault() ? " (default)" : ""); + return isNew; + } + + @Nullable + public Spawn getSpawn(@NotNull String name) { + return spawns.get(name.toLowerCase()); + } + + public boolean deleteSpawn(@NotNull String name) { + Spawn removed = spawns.remove(name.toLowerCase()); + if (removed != null) { + saveSpawns(); + Logger.info("[Spawns] Spawn '%s' deleted", name); + return true; + } + return false; + } + + @Nullable + public Spawn getDefaultSpawn() { + for (Spawn spawn : spawns.values()) { + if (spawn.isDefault()) { + return spawn; + } + } + String defaultName = config.getDefaultSpawnName(); + return spawns.get(defaultName.toLowerCase()); + } + + @Nullable + public Spawn getSpawnForPlayer(@NotNull UUID playerUuid) { + for (Spawn spawn : spawns.values()) { + if (spawn.isGroupRestricted()) { + if (PermissionManager.get().hasPermission(playerUuid, spawn.groupPermission())) { + return spawn; + } + } + } + return getDefaultSpawn(); + } + + @Nullable + public Spawn getSpawnForWorld(@NotNull String worldName) { + for (Spawn spawn : spawns.values()) { + if (spawn.world().equalsIgnoreCase(worldName)) { + return spawn; + } + } + return null; + } + + @NotNull + public Collection getAllSpawns() { + return Collections.unmodifiableCollection(spawns.values()); + } + + @NotNull + public List getAccessibleSpawns(@NotNull UUID playerUuid) { + return spawns.values().stream() + .filter(spawn -> canAccess(playerUuid, spawn)) + .collect(Collectors.toList()); + } + + public boolean canAccess(@NotNull UUID playerUuid, @NotNull Spawn spawn) { + if (!spawn.requiresPermission()) { + return true; + } + return PermissionManager.get().hasPermission(playerUuid, spawn.permission()); + } + + public boolean spawnExists(@NotNull String name) { + return spawns.containsKey(name.toLowerCase()); + } + + public boolean setDefaultSpawn(@NotNull String name) { + Spawn spawn = spawns.get(name.toLowerCase()); + if (spawn == null) { + return false; + } + + for (Map.Entry entry : spawns.entrySet()) { + if (entry.getValue().isDefault()) { + spawns.put(entry.getKey(), entry.getValue().withDefault(false)); + } + } + + spawns.put(spawn.name(), spawn.withDefault(true)); + saveSpawns(); + return true; + } + + @NotNull + public List getSpawnNames() { + return new ArrayList<>(spawns.keySet()); + } + + public int getSpawnCount() { + return spawns.size(); + } +} diff --git a/src/main/java/com/hyperessentials/module/spawns/SpawnsModule.java b/src/main/java/com/hyperessentials/module/spawns/SpawnsModule.java index 3a73618..1d0b346 100644 --- a/src/main/java/com/hyperessentials/module/spawns/SpawnsModule.java +++ b/src/main/java/com/hyperessentials/module/spawns/SpawnsModule.java @@ -1,43 +1,65 @@ -package com.hyperessentials.module.spawns; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.config.ModuleConfig; -import com.hyperessentials.module.AbstractModule; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Spawns module for HyperEssentials. - */ -public class SpawnsModule extends AbstractModule { - - @Override - @NotNull - public String getName() { - return "spawns"; - } - - @Override - @NotNull - public String getDisplayName() { - return "Spawns"; - } - - @Override - public void onEnable() { - super.onEnable(); - // TODO: Register commands, listeners, and storage - } - - @Override - public void onDisable() { - // TODO: Unregister commands, save data, cleanup - super.onDisable(); - } - - @Override - @Nullable - public ModuleConfig getModuleConfig() { - return ConfigManager.get().spawns(); - } -} +package com.hyperessentials.module.spawns; + +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.ModuleConfig; +import com.hyperessentials.config.modules.SpawnsConfig; +import com.hyperessentials.module.AbstractModule; +import com.hyperessentials.storage.SpawnStorage; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Spawns module for HyperEssentials. + */ +public class SpawnsModule extends AbstractModule { + + private SpawnManager spawnManager; + + @Override + @NotNull + public String getName() { + return "spawns"; + } + + @Override + @NotNull + public String getDisplayName() { + return "Spawns"; + } + + @Override + public void onEnable() { + super.onEnable(); + } + + public void initManager(@NotNull SpawnStorage storage) { + SpawnsConfig config = ConfigManager.get().spawns(); + this.spawnManager = new SpawnManager(storage, config); + spawnManager.loadSpawns().join(); + + // On first startup (no spawns configured), auto-detect from server world configs + spawnManager.autoDetectWorldSpawns(); + + Logger.info("[Spawns] SpawnManager initialized"); + } + + @Override + public void onDisable() { + if (spawnManager != null) { + spawnManager.saveSpawns().join(); + } + super.onDisable(); + } + + @Nullable + public SpawnManager getSpawnManager() { + return spawnManager; + } + + @Override + @Nullable + public ModuleConfig getModuleConfig() { + return ConfigManager.get().spawns(); + } +} diff --git a/src/main/java/com/hyperessentials/module/spawns/command/DelSpawnCommand.java b/src/main/java/com/hyperessentials/module/spawns/command/DelSpawnCommand.java new file mode 100644 index 0000000..ca44ed0 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/spawns/command/DelSpawnCommand.java @@ -0,0 +1,61 @@ +package com.hyperessentials.module.spawns.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.spawns.SpawnManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /delspawn <name> - Delete a spawn. + */ +public class DelSpawnCommand extends AbstractPlayerCommand { + + private final SpawnManager spawnManager; + + public DelSpawnCommand(@NotNull SpawnManager spawnManager) { + super("delspawn", "Delete a spawn"); + this.spawnManager = spawnManager; + addAliases("deletespawn", "rmspawn", "removespawn"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.SPAWN_DELETE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to delete spawns.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /delspawn ")); + return; + } + + String spawnName = parts[1].toLowerCase(); + + if (spawnManager.deleteSpawn(spawnName)) { + ctx.sendMessage(CommandUtil.success("Spawn '" + spawnName + "' has been deleted.")); + } else { + ctx.sendMessage(CommandUtil.error("Spawn '" + spawnName + "' not found.")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/spawns/command/SetSpawnCommand.java b/src/main/java/com/hyperessentials/module/spawns/command/SetSpawnCommand.java new file mode 100644 index 0000000..e1bdee5 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/spawns/command/SetSpawnCommand.java @@ -0,0 +1,122 @@ +package com.hyperessentials.module.spawns.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.modules.SpawnsConfig; +import com.hyperessentials.data.Spawn; +import com.hyperessentials.module.spawns.SpawnManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /setspawn [name] [--default] - Create or update a spawn at your location. + */ +public class SetSpawnCommand extends AbstractPlayerCommand { + + private final SpawnManager spawnManager; + private final SpawnsConfig config; + + public SetSpawnCommand(@NotNull SpawnManager spawnManager, @NotNull SpawnsConfig config) { + super("setspawn", "Create a spawn at your location"); + this.spawnManager = spawnManager; + this.config = config; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.SPAWN_SET)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to create spawns.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + String spawnName = parts.length > 1 ? parts[1].toLowerCase() : config.getDefaultSpawnName(); + + boolean setAsDefault = false; + for (String part : parts) { + if (part.equalsIgnoreCase("--default") || part.equalsIgnoreCase("-d")) { + setAsDefault = true; + break; + } + } + + if (spawnName.equals("--default") || spawnName.equals("-d")) { + spawnName = config.getDefaultSpawnName(); + setAsDefault = true; + } + + if (spawnName.length() < 1 || spawnName.length() > 32) { + ctx.sendMessage(CommandUtil.error("Spawn name must be 1-32 characters.")); + return; + } + + if (!spawnName.matches("[a-z0-9_-]+")) { + ctx.sendMessage(CommandUtil.error("Spawn name can only contain letters, numbers, underscore, and dash.")); + return; + } + + TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType()); + if (transform == null) { + ctx.sendMessage(CommandUtil.error("Could not get your position.")); + return; + } + + Vector3d pos = transform.getPosition(); + Vector3f rot = transform.getRotation(); + + boolean isUpdate = spawnManager.spawnExists(spawnName); + + Spawn spawn; + if (isUpdate) { + Spawn existing = spawnManager.getSpawn(spawnName); + spawn = existing.withLocation( + currentWorld.getName(), + pos.getX(), pos.getY(), pos.getZ(), + rot.getY(), rot.getX() + ); + if (setAsDefault) { + spawn = spawn.withDefault(true); + } + } else { + spawn = Spawn.create( + spawnName, + currentWorld.getName(), + pos.getX(), pos.getY(), pos.getZ(), + rot.getY(), rot.getX(), + uuid.toString() + ); + if (spawnManager.getSpawnCount() == 0 || setAsDefault) { + spawn = spawn.withDefault(true); + } + } + + spawnManager.setSpawn(spawn); + + ctx.sendMessage(CommandUtil.success("Spawn '" + spawnName + "' has been set!")); + ctx.sendMessage(CommandUtil.info(String.format("Location: %.0f, %.0f, %.0f in %s", + pos.getX(), pos.getY(), pos.getZ(), currentWorld.getName()))); + if (spawn.isDefault()) { + ctx.sendMessage(CommandUtil.info("(Set as default spawn)")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/spawns/command/SpawnCommand.java b/src/main/java/com/hyperessentials/module/spawns/command/SpawnCommand.java new file mode 100644 index 0000000..fcfabb6 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/spawns/command/SpawnCommand.java @@ -0,0 +1,126 @@ +package com.hyperessentials.module.spawns.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.modules.SpawnsConfig; +import com.hyperessentials.data.Location; +import com.hyperessentials.data.Spawn; +import com.hyperessentials.module.spawns.SpawnManager; +import com.hyperessentials.module.warmup.WarmupManager; +import com.hyperessentials.module.warmup.WarmupTask; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /spawn [name] - Teleport to spawn. + */ +public class SpawnCommand extends AbstractPlayerCommand { + + private final SpawnManager spawnManager; + private final SpawnsConfig config; + private final WarmupManager warmupManager; + + public SpawnCommand(@NotNull SpawnManager spawnManager, @NotNull SpawnsConfig config, + @NotNull WarmupManager warmupManager) { + super("spawn", "Teleport to spawn"); + this.spawnManager = spawnManager; + this.config = config; + this.warmupManager = warmupManager; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.SPAWN)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to use spawn.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + String spawnName = parts.length > 1 ? parts[1].toLowerCase() : null; + + Spawn spawn; + if (spawnName != null) { + spawn = spawnManager.getSpawn(spawnName); + if (spawn == null) { + ctx.sendMessage(CommandUtil.error("Spawn '" + spawnName + "' not found.")); + return; + } + if (!spawnManager.canAccess(uuid, spawn)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to use this spawn.")); + return; + } + } else if (config.isPerWorldSpawns()) { + spawn = spawnManager.getSpawnForWorld(currentWorld.getName()); + if (spawn == null) { + spawn = spawnManager.getSpawnForPlayer(uuid); + } + } else { + spawn = spawnManager.getSpawnForPlayer(uuid); + } + + if (spawn == null) { + ctx.sendMessage(CommandUtil.error("No spawn point has been set.")); + return; + } + + if (warmupManager.isOnCooldown(uuid, "spawns", "spawn")) { + int remaining = warmupManager.getRemainingCooldown(uuid, "spawns", "spawn"); + ctx.sendMessage(CommandUtil.error("On cooldown. " + remaining + "s remaining.")); + return; + } + + Location destination = Location.fromSpawn(spawn); + + WarmupTask task = warmupManager.startWarmup(uuid, "spawns", "spawn", () -> { + executeTeleport(ref, destination, () -> { + ctx.sendMessage(CommandUtil.success("Teleported to spawn!")); + }); + }); + + if (task != null) { + ctx.sendMessage(CommandUtil.info("Teleporting in " + task.warmupSeconds() + "s... Don't move!")); + } + } + + private void executeTeleport(Ref ref, Location dest, Runnable onComplete) { + World targetWorld = Universe.get().getWorld(dest.world()); + if (targetWorld == null) { + return; + } + targetWorld.execute(() -> { + if (!ref.isValid()) { + return; + } + Store store = ref.getStore(); + Vector3d position = new Vector3d(dest.x(), dest.y(), dest.z()); + Vector3f rotation = new Vector3f(dest.pitch(), dest.yaw(), 0); + Teleport teleport = Teleport.createForPlayer(targetWorld, position, rotation); + store.addComponent(ref, Teleport.getComponentType(), teleport); + if (onComplete != null) { + onComplete.run(); + } + }); + } +} diff --git a/src/main/java/com/hyperessentials/module/spawns/command/SpawnInfoCommand.java b/src/main/java/com/hyperessentials/module/spawns/command/SpawnInfoCommand.java new file mode 100644 index 0000000..f68a747 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/spawns/command/SpawnInfoCommand.java @@ -0,0 +1,80 @@ +package com.hyperessentials.module.spawns.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.Spawn; +import com.hyperessentials.module.spawns.SpawnManager; +import com.hyperessentials.util.TimeUtil; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /spawninfo <name> - Display detailed information about a spawn. + */ +public class SpawnInfoCommand extends AbstractPlayerCommand { + + private final SpawnManager spawnManager; + + public SpawnInfoCommand(@NotNull SpawnManager spawnManager) { + super("spawninfo", "Display spawn information"); + this.spawnManager = spawnManager; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.SPAWN_INFO)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to view spawn info.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /spawninfo ")); + return; + } + + String spawnName = parts[1].toLowerCase(); + + Spawn spawn = spawnManager.getSpawn(spawnName); + if (spawn == null) { + ctx.sendMessage(CommandUtil.error("Spawn '" + spawnName + "' not found.")); + return; + } + + ctx.sendMessage(CommandUtil.msg("--- Spawn: " + spawn.name() + " ---", CommandUtil.COLOR_GOLD)); + ctx.sendMessage(CommandUtil.msg("World: " + spawn.world(), CommandUtil.COLOR_GRAY)); + ctx.sendMessage(CommandUtil.msg(String.format("Location: %.1f, %.1f, %.1f", + spawn.x(), spawn.y(), spawn.z()), CommandUtil.COLOR_GRAY)); + ctx.sendMessage(CommandUtil.msg("Default: " + (spawn.isDefault() ? "Yes" : "No"), CommandUtil.COLOR_GRAY)); + + if (spawn.permission() != null && !spawn.permission().isEmpty()) { + ctx.sendMessage(CommandUtil.msg("Permission: " + spawn.permission(), CommandUtil.COLOR_GRAY)); + boolean hasAccess = spawnManager.canAccess(uuid, spawn); + ctx.sendMessage(CommandUtil.msg("You have access: " + (hasAccess ? "Yes" : "No"), CommandUtil.COLOR_GRAY)); + } + + if (spawn.groupPermission() != null && !spawn.groupPermission().isEmpty()) { + ctx.sendMessage(CommandUtil.msg("Group Permission: " + spawn.groupPermission(), CommandUtil.COLOR_GRAY)); + } + + ctx.sendMessage(CommandUtil.msg("Created: " + TimeUtil.formatRelativeTime(spawn.createdAt()), CommandUtil.COLOR_GRAY)); + } +} diff --git a/src/main/java/com/hyperessentials/module/spawns/command/SpawnsCommand.java b/src/main/java/com/hyperessentials/module/spawns/command/SpawnsCommand.java new file mode 100644 index 0000000..cabf1cf --- /dev/null +++ b/src/main/java/com/hyperessentials/module/spawns/command/SpawnsCommand.java @@ -0,0 +1,76 @@ +package com.hyperessentials.module.spawns.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.Spawn; +import com.hyperessentials.module.spawns.SpawnManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.UUID; + +/** + * /spawns - List all spawns. + */ +public class SpawnsCommand extends AbstractPlayerCommand { + + private final SpawnManager spawnManager; + + public SpawnsCommand(@NotNull SpawnManager spawnManager) { + super("spawns", "List server spawns"); + this.spawnManager = spawnManager; + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.SPAWN_LIST)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to list spawns.")); + return; + } + + Collection spawns = spawnManager.getAllSpawns(); + + if (spawns.isEmpty()) { + ctx.sendMessage(CommandUtil.info("No spawns have been set.")); + ctx.sendMessage(CommandUtil.msg("Use /setspawn [name] to create one.", CommandUtil.COLOR_GRAY)); + return; + } + + ctx.sendMessage(CommandUtil.msg("--- Server Spawns ---", CommandUtil.COLOR_GOLD)); + + for (Spawn spawn : spawns) { + StringBuilder sb = new StringBuilder(); + sb.append(spawn.name()); + if (spawn.isDefault()) { + sb.append(" (default)"); + } + sb.append(" - ").append(spawn.world()); + sb.append(String.format(" (%.0f, %.0f, %.0f)", spawn.x(), spawn.y(), spawn.z())); + + if (spawn.requiresPermission()) { + boolean hasAccess = spawnManager.canAccess(uuid, spawn); + if (!hasAccess) { + sb.append(" [no access]"); + } + } + + ctx.sendMessage(CommandUtil.msg(" " + sb, CommandUtil.COLOR_GRAY)); + } + + ctx.sendMessage(CommandUtil.msg("Use /spawn [name] to teleport.", CommandUtil.COLOR_GRAY)); + } +} diff --git a/src/main/java/com/hyperessentials/module/teleport/BackManager.java b/src/main/java/com/hyperessentials/module/teleport/BackManager.java new file mode 100644 index 0000000..3595528 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/teleport/BackManager.java @@ -0,0 +1,95 @@ +package com.hyperessentials.module.teleport; + +import com.hyperessentials.config.modules.TeleportConfig; +import com.hyperessentials.data.Location; +import com.hyperessentials.data.PlayerTeleportData; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * Manages /back location history for players. + * Works in conjunction with TpaManager for player data storage. + */ +public class BackManager { + + private final TpaManager tpaManager; + private final TeleportConfig config; + + public BackManager(@NotNull TpaManager tpaManager, @NotNull TeleportConfig config) { + this.tpaManager = tpaManager; + this.config = config; + } + + public void saveBackLocation(@NotNull UUID uuid, @NotNull Location location) { + PlayerTeleportData data = tpaManager.getPlayerData(uuid); + if (data == null) { + Logger.debug("Cannot save back location - player %s not loaded", uuid); + return; + } + + int maxSize = config.getBackHistorySize(); + data.addBackLocation(location, maxSize); + tpaManager.savePlayer(uuid); + + Logger.debug("Saved back location for %s: %s %.0f, %.0f, %.0f", + uuid, location.world(), location.x(), location.y(), location.z()); + } + + public void onTeleport(@NotNull UUID uuid, @NotNull Location location) { + if (config.isSaveBackOnTeleport()) { + saveBackLocation(uuid, location); + } + } + + public void onDeath(@NotNull UUID uuid, @NotNull Location location) { + if (config.isSaveBackOnDeath()) { + saveBackLocation(uuid, location); + } + } + + @Nullable + public Location getBackLocation(@NotNull UUID uuid) { + PlayerTeleportData data = tpaManager.getPlayerData(uuid); + if (data == null) { + return null; + } + return data.getLastBackLocation(); + } + + @Nullable + public Location popBackLocation(@NotNull UUID uuid) { + PlayerTeleportData data = tpaManager.getPlayerData(uuid); + if (data == null) { + return null; + } + + Location location = data.popBackLocation(); + if (location != null) { + tpaManager.savePlayer(uuid); + Logger.debug("Popped back location for %s", uuid); + } + return location; + } + + public boolean hasBackHistory(@NotNull UUID uuid) { + PlayerTeleportData data = tpaManager.getPlayerData(uuid); + return data != null && !data.getBackHistory().isEmpty(); + } + + public void clearHistory(@NotNull UUID uuid) { + PlayerTeleportData data = tpaManager.getPlayerData(uuid); + if (data != null) { + data.clearBackHistory(); + tpaManager.savePlayer(uuid); + Logger.debug("Cleared back history for %s", uuid); + } + } + + public int getHistorySize(@NotNull UUID uuid) { + PlayerTeleportData data = tpaManager.getPlayerData(uuid); + return data != null ? data.getBackHistory().size() : 0; + } +} diff --git a/src/main/java/com/hyperessentials/module/teleport/RtpManager.java b/src/main/java/com/hyperessentials/module/teleport/RtpManager.java new file mode 100644 index 0000000..18d430f --- /dev/null +++ b/src/main/java/com/hyperessentials/module/teleport/RtpManager.java @@ -0,0 +1,246 @@ +package com.hyperessentials.module.teleport; + +import com.hyperessentials.config.modules.TeleportConfig; +import com.hyperessentials.data.Location; +import com.hyperessentials.integration.BorderHook; +import com.hyperessentials.integration.HyperFactionsIntegration; +import com.hyperessentials.util.Logger; +import com.hyperessentials.util.RtpChunkUtil; +import com.hypixel.hytale.math.util.ChunkUtil; +import com.hypixel.hytale.math.util.MathUtil; +import com.hypixel.hytale.protocol.BlockMaterial; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; +import com.hypixel.hytale.server.core.asset.type.fluid.Fluid; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * Manages random teleport location generation with chunk-based safety verification, + * faction claim avoidance, and world border enforcement. + *

+ * Must be called on the world thread (requires chunk/block access). + */ +public class RtpManager { + + private final TeleportConfig config; + private BorderHook borderHook = (world, x, z) -> true; + + public RtpManager(@NotNull TeleportConfig config) { + this.config = config; + } + + /** + * Sets the border hook for world border enforcement. + * + * @param hook the border hook implementation + */ + public void setBorderHook(@NotNull BorderHook hook) { + this.borderHook = hook; + } + + /** + * Result of an RTP search attempt. + */ + public sealed interface RtpResult { + record Success(@NotNull Location location) implements RtpResult {} + record Failure(@NotNull String reason) implements RtpResult {} + } + + /** + * Finds a safe random location. Must be called on the world thread. + * + * @param world the world instance (for chunk/block access) + * @param worldName the world name + * @param playerX player's current X coordinate (used when playerRelative=true) + * @param playerZ player's current Z coordinate (used when playerRelative=true) + * @param bypassFactions whether to skip faction claim checks + * @return the result of the search + */ + @NotNull + public RtpResult findSafeRandomLocation(@NotNull World world, @NotNull String worldName, + double playerX, double playerZ, + boolean bypassFactions) { + int maxAttempts = config.getRtpMaxAttempts(); + int minRadius = config.getRtpMinRadius(); + int maxRadius = config.getRtpMaxRadius(); + boolean checkFactions = config.isRtpFactionAvoidanceEnabled() && !bypassFactions; + int factionBuffer = config.getRtpFactionAvoidanceBufferRadius(); + + // Center: player position or configured center + double centerX = config.isRtpPlayerRelative() ? playerX : config.getRtpCenterX(); + double centerZ = config.isRtpPlayerRelative() ? playerZ : config.getRtpCenterZ(); + + Logger.debugRtp("Starting RTP search: center=(%.0f, %.0f), radius=[%d, %d], maxAttempts=%d, factions=%s", + centerX, centerZ, minRadius, maxRadius, maxAttempts, checkFactions ? "check" : "bypass"); + + ThreadLocalRandom random = ThreadLocalRandom.current(); + + for (int attempt = 1; attempt <= maxAttempts; attempt++) { + // Step 1: Random direction (continuous angle) + double angle = random.nextDouble() * 2 * Math.PI; + + // Step 2: Random distance within ring + double distance = minRadius + random.nextDouble() * (maxRadius - minRadius); + + // Step 3: Compute target world coordinates + double targetX = centerX + distance * Math.cos(angle); + double targetZ = centerZ + distance * Math.sin(angle); + + Logger.debugRtp("Attempt %d: angle=%.1f°, dist=%.0f, target=(%.0f, %.0f)", + attempt, Math.toDegrees(angle), distance, targetX, targetZ); + + // Step 4: Border check + if (!borderHook.isWithinBorder(worldName, targetX, targetZ)) { + Logger.debugRtp("Attempt %d: outside world border, retrying", attempt); + continue; + } + + // Step 5: Faction claim check (buffer radius around target chunk) + int targetChunkX = RtpChunkUtil.toChunkCoord(targetX); + int targetChunkZ = RtpChunkUtil.toChunkCoord(targetZ); + + if (checkFactions && isAnyClaimNearby(worldName, targetChunkX, targetChunkZ, factionBuffer)) { + Logger.debugRtp("Attempt %d: faction claim within buffer radius %d, retrying", attempt, factionBuffer); + continue; + } + + // Step 6: Random position within the target chunk + int blockX = RtpChunkUtil.chunkToBlockMin(targetChunkX) + random.nextInt(RtpChunkUtil.CHUNK_SIZE); + int blockZ = RtpChunkUtil.chunkToBlockMin(targetChunkZ) + random.nextInt(RtpChunkUtil.CHUNK_SIZE); + + // Step 7: Load chunk + safety scan + long chunkIndex = ChunkUtil.indexChunkFromBlock(blockX, blockZ); + WorldChunk chunk = world.getNonTickingChunk(chunkIndex); + if (chunk == null) { + Logger.debugRtp("Attempt %d: chunk not loaded at (%d, %d), retrying", attempt, blockX, blockZ); + continue; + } + + int safeY = findSafeY(world, chunk, blockX, blockZ); + if (safeY < 0) { + Logger.debugRtp("Attempt %d: no safe Y found at (%d, %d), retrying", attempt, blockX, blockZ); + continue; + } + + // Center player on block + double finalX = blockX + 0.5; + double finalZ = blockZ + 0.5; + Location location = new Location(worldName, finalX, safeY, finalZ, 0, 0); + + Logger.debugRtp("RTP success on attempt %d: (%.1f, %d, %.1f)", attempt, finalX, safeY, finalZ); + return new RtpResult.Success(location); + } + + Logger.debugRtp("RTP search FAILED after %d attempts", maxAttempts); + return new RtpResult.Failure("Could not find a safe random location after " + maxAttempts + " attempts."); + } + + /** + * Scans from heightmap downward to find a safe Y coordinate. + * Checks for solid ground with 2 empty non-fluid blocks for the player body, + * plus additional air blocks above to ensure surface placement (not caves). + * The number of air blocks above head is configurable via safety.airAboveHead. + * + * @return safe Y coordinate (feet level), or -1 if none found + */ + private int findSafeY(@NotNull World world, @NotNull WorldChunk chunk, int blockX, int blockZ) { + int topY = chunk.getHeight(blockX, blockZ); + int minY = config.getRtpSafetyMinY(); + int maxY = config.getRtpSafetyMaxY(); + int airAboveHead = config.getRtpSafetyAirAboveHead(); + boolean avoidWater = config.isRtpSafetyAvoidWater(); + boolean avoidDangerous = config.isRtpSafetyAvoidDangerousFluids(); + + // Clamp scan start to maxY + int scanStart = Math.min(topY, maxY); + + for (int y = scanStart; y >= minY; y--) { + BlockType ground = world.getBlockType(blockX, y, blockZ); + BlockType feet = world.getBlockType(blockX, y + 1, blockZ); + BlockType head = world.getBlockType(blockX, y + 2, blockZ); + + // Ground must be solid + boolean groundSolid = ground != null && ground.getMaterial() == BlockMaterial.Solid; + if (!groundSolid) continue; + + // Feet and head must be empty + boolean feetClear = feet == null || feet.getMaterial() == BlockMaterial.Empty; + boolean headClear = head == null || head.getMaterial() == BlockMaterial.Empty; + if (!feetClear || !headClear) continue; + + // Check additional air blocks above head to avoid caves + boolean aboveClear = true; + for (int above = 3; above <= 2 + airAboveHead; above++) { + BlockType aboveBlock = world.getBlockType(blockX, y + above, blockZ); + if (aboveBlock != null && aboveBlock.getMaterial() == BlockMaterial.Solid) { + aboveClear = false; + break; + } + } + if (!aboveClear) { + Logger.debugRtp("Rejected y=%d at (%d, %d): solid block within %d blocks above head (cave)", + y, blockX, blockZ, airAboveHead); + continue; + } + + // Fluid checks + if (avoidWater) { + int feetFluid = chunk.getFluidId(blockX, y + 1, blockZ); + int headFluid = chunk.getFluidId(blockX, y + 2, blockZ); + if (feetFluid != Fluid.EMPTY_ID || headFluid != Fluid.EMPTY_ID) continue; + } + + // Dangerous fluid check below ground (prevents thin-crust-over-lava) + if (avoidDangerous && y > minY) { + int belowFluid = chunk.getFluidId(blockX, y - 1, blockZ); + if (belowFluid != Fluid.EMPTY_ID) { + Fluid fluid = Fluid.getAssetMap().getAsset(belowFluid); + if (fluid != null && fluid.getDamageToEntities() > 0) continue; + } + } + + return y + 1; // Feet level + } + + return -1; // No safe position found + } + + /** + * Checks whether any faction claim exists within the buffer radius of the target chunk. + * Uses world-coordinate lookups via HyperFactionsIntegration. + * + * @param worldName the world name + * @param chunkX target chunk X + * @param chunkZ target chunk Z + * @param radiusChunks buffer radius in chunks (e.g. 2 = 5x5 check area) + * @return true if any claim is nearby + */ + private boolean isAnyClaimNearby(@NotNull String worldName, int chunkX, int chunkZ, int radiusChunks) { + for (int dx = -radiusChunks; dx <= radiusChunks; dx++) { + for (int dz = -radiusChunks; dz <= radiusChunks; dz++) { + // Convert chunk center to world coordinates for the integration lookup + int worldX = RtpChunkUtil.chunkToBlockMin(chunkX + dx) + RtpChunkUtil.CHUNK_SIZE / 2; + int worldZ = RtpChunkUtil.chunkToBlockMin(chunkZ + dz) + RtpChunkUtil.CHUNK_SIZE / 2; + + String faction = HyperFactionsIntegration.getFactionAtLocation(worldName, worldX, worldZ); + if (faction != null) { + Logger.debugRtp("Claim found at chunk (%d, %d): %s", chunkX + dx, chunkZ + dz, faction); + return true; + } + } + } + return false; + } + + public boolean isWorldBlacklisted(@NotNull String worldName) { + return config.getRtpBlacklistedWorlds().contains(worldName.toLowerCase()); + } + + public int getMaxAttempts() { + return config.getRtpMaxAttempts(); + } +} diff --git a/src/main/java/com/hyperessentials/module/teleport/TeleportModule.java b/src/main/java/com/hyperessentials/module/teleport/TeleportModule.java index 34163ab..1d86942 100644 --- a/src/main/java/com/hyperessentials/module/teleport/TeleportModule.java +++ b/src/main/java/com/hyperessentials/module/teleport/TeleportModule.java @@ -1,43 +1,77 @@ -package com.hyperessentials.module.teleport; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.config.ModuleConfig; -import com.hyperessentials.module.AbstractModule; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Teleport module for HyperEssentials. - */ -public class TeleportModule extends AbstractModule { - - @Override - @NotNull - public String getName() { - return "teleport"; - } - - @Override - @NotNull - public String getDisplayName() { - return "Teleport"; - } - - @Override - public void onEnable() { - super.onEnable(); - // TODO: Register commands, listeners, and storage - } - - @Override - public void onDisable() { - // TODO: Unregister commands, save data, cleanup - super.onDisable(); - } - - @Override - @Nullable - public ModuleConfig getModuleConfig() { - return ConfigManager.get().teleport(); - } -} +package com.hyperessentials.module.teleport; + +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.ModuleConfig; +import com.hyperessentials.config.modules.TeleportConfig; +import com.hyperessentials.module.AbstractModule; +import com.hyperessentials.storage.PlayerDataStorage; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Teleport module for HyperEssentials. + * Handles TPA requests, /back history, and random teleport (RTP). + */ +public class TeleportModule extends AbstractModule { + + private TpaManager tpaManager; + private BackManager backManager; + private RtpManager rtpManager; + + @Override + @NotNull + public String getName() { + return "teleport"; + } + + @Override + @NotNull + public String getDisplayName() { + return "Teleport"; + } + + @Override + public void onEnable() { + super.onEnable(); + TeleportConfig config = ConfigManager.get().teleport(); + this.rtpManager = new RtpManager(config); + Logger.info("[Teleport] RtpManager initialized"); + } + + public void initManagers(@NotNull PlayerDataStorage storage) { + TeleportConfig config = ConfigManager.get().teleport(); + this.tpaManager = new TpaManager(storage, config); + this.backManager = new BackManager(tpaManager, config); + Logger.info("[Teleport] TpaManager and BackManager initialized"); + } + + @Override + public void onDisable() { + if (tpaManager != null) { + tpaManager.saveAll().join(); + } + super.onDisable(); + } + + @Nullable + public TpaManager getTpaManager() { + return tpaManager; + } + + @Nullable + public BackManager getBackManager() { + return backManager; + } + + @Nullable + public RtpManager getRtpManager() { + return rtpManager; + } + + @Override + @Nullable + public ModuleConfig getModuleConfig() { + return ConfigManager.get().teleport(); + } +} diff --git a/src/main/java/com/hyperessentials/module/teleport/TpaManager.java b/src/main/java/com/hyperessentials/module/teleport/TpaManager.java new file mode 100644 index 0000000..d155623 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/teleport/TpaManager.java @@ -0,0 +1,262 @@ +package com.hyperessentials.module.teleport; + +import com.hyperessentials.Permissions; +import com.hyperessentials.config.modules.TeleportConfig; +import com.hyperessentials.data.PlayerTeleportData; +import com.hyperessentials.data.TeleportRequest; +import com.hyperessentials.integration.PermissionManager; +import com.hyperessentials.storage.PlayerDataStorage; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages TPA (teleport ask) requests between players. + */ +public class TpaManager { + + private final PlayerDataStorage storage; + private final TeleportConfig config; + private final Map playerCache; + private final Map> incomingRequests; // target -> list of requests + private final Map outgoingRequests; // requester -> their pending request + + public TpaManager(@NotNull PlayerDataStorage storage, @NotNull TeleportConfig config) { + this.storage = storage; + this.config = config; + this.playerCache = new ConcurrentHashMap<>(); + this.incomingRequests = new ConcurrentHashMap<>(); + this.outgoingRequests = new ConcurrentHashMap<>(); + } + + public CompletableFuture loadPlayer(@NotNull UUID uuid, @NotNull String username) { + return storage.loadPlayerData(uuid).thenApply(opt -> { + PlayerTeleportData data = opt.orElseGet(() -> new PlayerTeleportData(uuid, username)); + data.setUsername(username); + playerCache.put(uuid, data); + Logger.debug("Loaded teleport data for %s", username); + return data; + }); + } + + public CompletableFuture savePlayer(@NotNull UUID uuid) { + PlayerTeleportData data = playerCache.get(uuid); + if (data == null) { + return CompletableFuture.completedFuture(null); + } + return storage.savePlayerData(data); + } + + public CompletableFuture unloadPlayer(@NotNull UUID uuid) { + cancelOutgoingRequest(uuid); + incomingRequests.remove(uuid); + + for (List requests : incomingRequests.values()) { + requests.removeIf(req -> req.requester().equals(uuid)); + } + + return savePlayer(uuid).thenRun(() -> { + playerCache.remove(uuid); + Logger.debug("Unloaded player %s from TPA cache", uuid); + }); + } + + @Nullable + public PlayerTeleportData getPlayerData(@NotNull UUID uuid) { + return playerCache.get(uuid); + } + + @NotNull + public PlayerTeleportData getOrCreatePlayerData(@NotNull UUID uuid, @NotNull String username) { + return playerCache.computeIfAbsent(uuid, k -> new PlayerTeleportData(uuid, username)); + } + + // ========== TPA Toggle ========== + + public boolean isAcceptingRequests(@NotNull UUID uuid) { + PlayerTeleportData data = playerCache.get(uuid); + return data == null || data.isTpToggle(); + } + + public boolean toggleTpToggle(@NotNull UUID uuid) { + PlayerTeleportData data = playerCache.get(uuid); + if (data == null) { + return true; + } + boolean newState = data.toggleTpToggle(); + savePlayer(uuid); + return newState; + } + + // ========== TPA Requests ========== + + @Nullable + public TeleportRequest createRequest(@NotNull UUID requesterUuid, @NotNull UUID targetUuid, + @NotNull TeleportRequest.Type type) { + if (!isAcceptingRequests(targetUuid)) { + if (!PermissionManager.get().hasPermission(requesterUuid, Permissions.BYPASS_TOGGLE)) { + return null; + } + } + + cancelOutgoingRequest(requesterUuid); + + List targetIncoming = incomingRequests.computeIfAbsent(targetUuid, k -> new ArrayList<>()); + cleanupExpiredRequests(targetIncoming); + + if (targetIncoming.size() >= config.getMaxPendingTpa()) { + return null; + } + + PlayerTeleportData requesterData = playerCache.get(requesterUuid); + if (requesterData != null) { + long lastRequest = requesterData.getLastTpaRequest(); + long cooldownMs = config.getTpaCooldown() * 1000L; + if (System.currentTimeMillis() - lastRequest < cooldownMs) { + return null; + } + } + + TeleportRequest request = TeleportRequest.create(requesterUuid, targetUuid, type, config.getTpaTimeout()); + + targetIncoming.add(request); + outgoingRequests.put(requesterUuid, request); + + if (requesterData != null) { + requesterData.setLastTpaRequest(System.currentTimeMillis()); + savePlayer(requesterUuid); + } + + Logger.debug("TPA request created: %s -> %s (%s)", requesterUuid, targetUuid, type); + return request; + } + + @NotNull + public List getIncomingRequests(@NotNull UUID uuid) { + List requests = incomingRequests.get(uuid); + if (requests == null) { + return new ArrayList<>(); + } + cleanupExpiredRequests(requests); + return new ArrayList<>(requests); + } + + @Nullable + public TeleportRequest getIncomingRequest(@NotNull UUID targetUuid, @NotNull UUID requesterUuid) { + List requests = incomingRequests.get(targetUuid); + if (requests == null) { + return null; + } + cleanupExpiredRequests(requests); + for (TeleportRequest req : requests) { + if (req.requester().equals(requesterUuid) && !req.isExpired()) { + return req; + } + } + return null; + } + + @Nullable + public TeleportRequest getMostRecentIncomingRequest(@NotNull UUID uuid) { + List requests = incomingRequests.get(uuid); + if (requests == null || requests.isEmpty()) { + return null; + } + cleanupExpiredRequests(requests); + if (requests.isEmpty()) { + return null; + } + return requests.get(requests.size() - 1); + } + + @Nullable + public TeleportRequest getOutgoingRequest(@NotNull UUID uuid) { + TeleportRequest request = outgoingRequests.get(uuid); + if (request != null && request.isExpired()) { + outgoingRequests.remove(uuid); + return null; + } + return request; + } + + public void acceptRequest(@NotNull TeleportRequest request) { + removeRequest(request); + Logger.debug("TPA request accepted: %s -> %s", request.requester(), request.target()); + } + + public void denyRequest(@NotNull TeleportRequest request) { + removeRequest(request); + Logger.debug("TPA request denied: %s -> %s", request.requester(), request.target()); + } + + @Nullable + public TeleportRequest cancelOutgoingRequest(@NotNull UUID uuid) { + TeleportRequest request = outgoingRequests.remove(uuid); + if (request != null) { + List targetIncoming = incomingRequests.get(request.target()); + if (targetIncoming != null) { + targetIncoming.remove(request); + } + Logger.debug("TPA request cancelled: %s -> %s", request.requester(), request.target()); + } + return request; + } + + private void removeRequest(@NotNull TeleportRequest request) { + outgoingRequests.remove(request.requester()); + List targetIncoming = incomingRequests.get(request.target()); + if (targetIncoming != null) { + targetIncoming.remove(request); + } + } + + private void cleanupExpiredRequests(@NotNull List requests) { + Iterator iter = requests.iterator(); + while (iter.hasNext()) { + TeleportRequest req = iter.next(); + if (req.isExpired()) { + iter.remove(); + outgoingRequests.remove(req.requester()); + } + } + } + + public long getRemainingTpaCooldown(@NotNull UUID uuid) { + PlayerTeleportData data = playerCache.get(uuid); + if (data == null) { + return 0; + } + long lastRequest = data.getLastTpaRequest(); + if (lastRequest == 0) { + return 0; + } + long cooldownMs = config.getTpaCooldown() * 1000L; + long elapsed = System.currentTimeMillis() - lastRequest; + return Math.max(0, cooldownMs - elapsed); + } + + public boolean hasPendingIncoming(@NotNull UUID uuid) { + List requests = incomingRequests.get(uuid); + if (requests == null || requests.isEmpty()) { + return false; + } + cleanupExpiredRequests(requests); + return !requests.isEmpty(); + } + + public CompletableFuture saveAll() { + List> futures = new ArrayList<>(); + for (UUID uuid : playerCache.keySet()) { + futures.add(savePlayer(uuid)); + } + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + } +} diff --git a/src/main/java/com/hyperessentials/module/teleport/command/BackCommand.java b/src/main/java/com/hyperessentials/module/teleport/command/BackCommand.java new file mode 100644 index 0000000..95cbb21 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/teleport/command/BackCommand.java @@ -0,0 +1,99 @@ +package com.hyperessentials.module.teleport.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.Location; +import com.hyperessentials.module.teleport.BackManager; +import com.hyperessentials.module.warmup.WarmupManager; +import com.hyperessentials.module.warmup.WarmupTask; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /back - Return to your previous location. + */ +public class BackCommand extends AbstractPlayerCommand { + + private final BackManager backManager; + private final WarmupManager warmupManager; + + public BackCommand(@NotNull BackManager backManager, @NotNull WarmupManager warmupManager) { + super("back", "Return to your previous location"); + this.backManager = backManager; + this.warmupManager = warmupManager; + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.BACK)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to use /back.")); + return; + } + + Location backLocation = backManager.getBackLocation(uuid); + if (backLocation == null) { + ctx.sendMessage(CommandUtil.error("No back location found.")); + return; + } + + if (warmupManager.isOnCooldown(uuid, "teleport", "back")) { + int remaining = warmupManager.getRemainingCooldown(uuid, "teleport", "back"); + ctx.sendMessage(CommandUtil.error("On cooldown. " + remaining + "s remaining.")); + return; + } + + // Pop the back location (remove from history since we're using it) + backManager.popBackLocation(uuid); + + Location destination = backLocation; + + WarmupTask task = warmupManager.startWarmup(uuid, "teleport", "back", () -> { + executeTeleport(ref, destination, () -> { + ctx.sendMessage(CommandUtil.success("Teleported to previous location!")); + }); + }); + + if (task != null) { + ctx.sendMessage(CommandUtil.info("Teleporting in " + task.warmupSeconds() + "s... Don't move!")); + } + } + + private void executeTeleport(Ref ref, Location dest, Runnable onComplete) { + World targetWorld = Universe.get().getWorld(dest.world()); + if (targetWorld == null) { + return; + } + targetWorld.execute(() -> { + if (!ref.isValid()) { + return; + } + Store store = ref.getStore(); + Vector3d position = new Vector3d(dest.x(), dest.y(), dest.z()); + Vector3f rotation = new Vector3f(dest.pitch(), dest.yaw(), 0); + Teleport teleport = Teleport.createForPlayer(targetWorld, position, rotation); + store.addComponent(ref, Teleport.getComponentType(), teleport); + if (onComplete != null) { + onComplete.run(); + } + }); + } +} diff --git a/src/main/java/com/hyperessentials/module/teleport/command/RtpCommand.java b/src/main/java/com/hyperessentials/module/teleport/command/RtpCommand.java new file mode 100644 index 0000000..ae87a50 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/teleport/command/RtpCommand.java @@ -0,0 +1,130 @@ +package com.hyperessentials.module.teleport.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.Location; +import com.hyperessentials.module.teleport.RtpManager; +import com.hyperessentials.module.teleport.RtpManager.RtpResult; +import com.hyperessentials.module.warmup.WarmupManager; +import com.hyperessentials.module.warmup.WarmupTask; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /rtp - Teleport to a random location. + */ +public class RtpCommand extends AbstractPlayerCommand { + + private final RtpManager rtpManager; + private final WarmupManager warmupManager; + + public RtpCommand(@NotNull RtpManager rtpManager, @NotNull WarmupManager warmupManager) { + super("rtp", "Teleport to a random location"); + this.rtpManager = rtpManager; + this.warmupManager = warmupManager; + addAliases("randomtp", "randomteleport"); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.RTP)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to use random teleport.")); + return; + } + + String worldName = currentWorld.getName(); + + if (rtpManager.isWorldBlacklisted(worldName)) { + ctx.sendMessage(CommandUtil.error("Random teleport is not allowed in this world.")); + return; + } + + if (warmupManager.isOnCooldown(uuid, "rtp", "rtp")) { + int remaining = warmupManager.getRemainingCooldown(uuid, "rtp", "rtp"); + ctx.sendMessage(CommandUtil.error("On cooldown. " + remaining + "s remaining.")); + return; + } + + // Get player position for player-relative RTP + TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType()); + if (transform == null) { + ctx.sendMessage(CommandUtil.error("Could not determine your position.")); + return; + } + + Vector3d pos = transform.getPosition(); + double playerX = pos.x; + double playerZ = pos.z; + + boolean bypassFactions = CommandUtil.hasPermission(uuid, Permissions.RTP_BYPASS_FACTIONS); + + ctx.sendMessage(CommandUtil.info("Searching for a safe random location...")); + + // Run search on world thread (chunk/block access required) + currentWorld.execute(() -> { + RtpResult result = rtpManager.findSafeRandomLocation( + currentWorld, worldName, playerX, playerZ, bypassFactions); + + if (result instanceof RtpResult.Failure failure) { + ctx.sendMessage(CommandUtil.error(failure.reason())); + return; + } + + Location destination = ((RtpResult.Success) result).location(); + + WarmupTask task = warmupManager.startWarmup(uuid, "rtp", "rtp", () -> { + executeTeleport(ref, destination, () -> { + ctx.sendMessage(CommandUtil.success(String.format( + "Teleported to random location! (%.0f, %.0f, %.0f)", + destination.x(), destination.y(), destination.z()))); + }); + }); + + if (task != null) { + ctx.sendMessage(CommandUtil.info(String.format( + "Found location at (%.0f, %.0f, %.0f). Teleporting in %ds... Don't move!", + destination.x(), destination.y(), destination.z(), task.warmupSeconds()))); + } + }); + } + + private void executeTeleport(Ref ref, Location dest, Runnable onComplete) { + World targetWorld = Universe.get().getWorld(dest.world()); + if (targetWorld == null) { + return; + } + targetWorld.execute(() -> { + if (!ref.isValid()) { + return; + } + Store store = ref.getStore(); + Vector3d position = new Vector3d(dest.x(), dest.y(), dest.z()); + Vector3f rotation = new Vector3f(dest.pitch(), dest.yaw(), 0); + Teleport teleport = Teleport.createForPlayer(targetWorld, position, rotation); + store.addComponent(ref, Teleport.getComponentType(), teleport); + if (onComplete != null) { + onComplete.run(); + } + }); + } +} diff --git a/src/main/java/com/hyperessentials/module/teleport/command/TpAcceptCommand.java b/src/main/java/com/hyperessentials/module/teleport/command/TpAcceptCommand.java new file mode 100644 index 0000000..e9c85a0 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/teleport/command/TpAcceptCommand.java @@ -0,0 +1,164 @@ +package com.hyperessentials.module.teleport.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.Location; +import com.hyperessentials.data.TeleportRequest; +import com.hyperessentials.module.teleport.BackManager; +import com.hyperessentials.module.teleport.TpaManager; +import com.hyperessentials.module.warmup.WarmupManager; +import com.hyperessentials.module.warmup.WarmupTask; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /tpaccept [player] - Accept a teleport request. + */ +public class TpAcceptCommand extends AbstractPlayerCommand { + + private final TpaManager tpaManager; + private final BackManager backManager; + private final WarmupManager warmupManager; + + public TpAcceptCommand(@NotNull TpaManager tpaManager, @NotNull BackManager backManager, + @NotNull WarmupManager warmupManager) { + super("tpaccept", "Accept a teleport request"); + this.tpaManager = tpaManager; + this.backManager = backManager; + this.warmupManager = warmupManager; + addAliases("tpyes", "tpy"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.TPACCEPT)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to accept requests.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + String requesterName = parts.length > 1 ? parts[1] : null; + + TeleportRequest request; + if (requesterName != null) { + PlayerRef requesterRef = findPlayer(requesterName); + if (requesterRef == null) { + ctx.sendMessage(CommandUtil.error("Player '" + requesterName + "' not found or offline.")); + return; + } + request = tpaManager.getIncomingRequest(uuid, requesterRef.getUuid()); + if (request == null) { + ctx.sendMessage(CommandUtil.error("No pending request from " + requesterName + ".")); + return; + } + } else { + request = tpaManager.getMostRecentIncomingRequest(uuid); + if (request == null) { + ctx.sendMessage(CommandUtil.error("You have no pending teleport requests.")); + return; + } + } + + if (request.isExpired()) { + tpaManager.denyRequest(request); + ctx.sendMessage(CommandUtil.error("That teleport request has expired.")); + return; + } + + tpaManager.acceptRequest(request); + + PlayerRef teleportingRef = findPlayerByUuid(request.getTeleportingPlayer()); + PlayerRef destinationRef = findPlayerByUuid(request.getDestinationPlayer()); + + if (teleportingRef == null || destinationRef == null) { + ctx.sendMessage(CommandUtil.error("The other player is no longer online.")); + return; + } + + // Get the destination (the player being teleported TO) + // For TPA: requester teleports to target (us), so destination is our location + // For TPAHERE: target (us) teleports to requester, so destination is requester's location + // In both cases the accepting player is us, but the destination depends on type. + // The actual teleport location must come from the destination player's current position. + // Since we only have our own store/ref, we send the accept message and + // the actual teleport is executed via the warmup system on the teleporting player's side. + + TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType()); + if (transform == null) { + ctx.sendMessage(CommandUtil.error("Could not determine position.")); + return; + } + + Vector3d pos = transform.getPosition(); + Location ourLocation = new Location(currentWorld.getName(), pos.getX(), pos.getY(), pos.getZ(), 0, 0); + + ctx.sendMessage(CommandUtil.success("Teleport request accepted.")); + teleportingRef.sendMessage(CommandUtil.success("Request accepted! Teleporting...")); + + if (request.type() == TeleportRequest.Type.TPA) { + // Requester teleports to us - we use our location as destination + // Save requester's back location (done via their player data, best effort) + backManager.onTeleport(request.requester(), ourLocation); + + // Execute teleport on the teleporting player + // Note: We don't have access to requester's store/ref, so we use the + // destination world thread to perform the teleport via Universe + executeTeleportToLocation(teleportingRef, ourLocation); + } else { + // TPAHERE: We (target) teleport to requester + // Our current location is saved as back + backManager.onTeleport(uuid, ourLocation); + + // We need the requester's current location, but we don't have their store. + // Since the requester is the destination, we inform them and handle via message. + // For now, we send a message - actual cross-player teleport requires platform support. + // The teleporting player (us) will be teleported via the store/ref we have. + destinationRef.sendMessage(CommandUtil.info("Teleporting " + playerRef.getUsername() + " to you...")); + } + } + + private void executeTeleportToLocation(PlayerRef teleportingRef, Location dest) { + World targetWorld = Universe.get().getWorld(dest.world()); + if (targetWorld == null) { + return; + } + // Queue teleport on the target world thread + // Note: Full cross-player teleport is wired in Task 8 via platform events + targetWorld.execute(() -> { + // The teleport will be handled by the platform layer + }); + } + + private PlayerRef findPlayer(String name) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + return plugin != null ? plugin.findOnlinePlayer(name) : null; + } + + private PlayerRef findPlayerByUuid(UUID uuid) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + return plugin != null ? plugin.getTrackedPlayer(uuid) : null; + } +} diff --git a/src/main/java/com/hyperessentials/module/teleport/command/TpCancelCommand.java b/src/main/java/com/hyperessentials/module/teleport/command/TpCancelCommand.java new file mode 100644 index 0000000..2c2f505 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/teleport/command/TpCancelCommand.java @@ -0,0 +1,65 @@ +package com.hyperessentials.module.teleport.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.TeleportRequest; +import com.hyperessentials.module.teleport.TpaManager; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /tpcancel - Cancel your outgoing teleport request. + */ +public class TpCancelCommand extends AbstractPlayerCommand { + + private final TpaManager tpaManager; + + public TpCancelCommand(@NotNull TpaManager tpaManager) { + super("tpcancel", "Cancel your teleport request"); + this.tpaManager = tpaManager; + addAliases("tpc"); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.TPCANCEL)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to cancel requests.")); + return; + } + + TeleportRequest request = tpaManager.cancelOutgoingRequest(uuid); + + if (request == null) { + ctx.sendMessage(CommandUtil.error("You have no pending teleport request to cancel.")); + return; + } + + ctx.sendMessage(CommandUtil.success("Teleport request cancelled.")); + + PlayerRef targetRef = findPlayerByUuid(request.target()); + if (targetRef != null) { + targetRef.sendMessage(CommandUtil.info(playerRef.getUsername() + " cancelled their teleport request.")); + } + } + + private PlayerRef findPlayerByUuid(UUID uuid) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + return plugin != null ? plugin.getTrackedPlayer(uuid) : null; + } +} diff --git a/src/main/java/com/hyperessentials/module/teleport/command/TpDenyCommand.java b/src/main/java/com/hyperessentials/module/teleport/command/TpDenyCommand.java new file mode 100644 index 0000000..ccb24e6 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/teleport/command/TpDenyCommand.java @@ -0,0 +1,90 @@ +package com.hyperessentials.module.teleport.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.TeleportRequest; +import com.hyperessentials.module.teleport.TpaManager; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /tpdeny [player] - Deny a teleport request. + */ +public class TpDenyCommand extends AbstractPlayerCommand { + + private final TpaManager tpaManager; + + public TpDenyCommand(@NotNull TpaManager tpaManager) { + super("tpdeny", "Deny a teleport request"); + this.tpaManager = tpaManager; + addAliases("tpno", "tpn"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.TPDENY)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to deny requests.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + String requesterName = parts.length > 1 ? parts[1] : null; + + TeleportRequest request; + if (requesterName != null) { + PlayerRef requesterRef = findPlayer(requesterName); + if (requesterRef == null) { + ctx.sendMessage(CommandUtil.error("Player '" + requesterName + "' not found or offline.")); + return; + } + request = tpaManager.getIncomingRequest(uuid, requesterRef.getUuid()); + if (request == null) { + ctx.sendMessage(CommandUtil.error("No pending request from " + requesterName + ".")); + return; + } + } else { + request = tpaManager.getMostRecentIncomingRequest(uuid); + if (request == null) { + ctx.sendMessage(CommandUtil.error("You have no pending teleport requests.")); + return; + } + } + + tpaManager.denyRequest(request); + + ctx.sendMessage(CommandUtil.success("Teleport request denied.")); + + PlayerRef requesterRef = findPlayerByUuid(request.requester()); + if (requesterRef != null) { + requesterRef.sendMessage(CommandUtil.error(playerRef.getUsername() + " denied your teleport request.")); + } + } + + private PlayerRef findPlayer(String name) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + return plugin != null ? plugin.findOnlinePlayer(name) : null; + } + + private PlayerRef findPlayerByUuid(UUID uuid) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + return plugin != null ? plugin.getTrackedPlayer(uuid) : null; + } +} diff --git a/src/main/java/com/hyperessentials/module/teleport/command/TpToggleCommand.java b/src/main/java/com/hyperessentials/module/teleport/command/TpToggleCommand.java new file mode 100644 index 0000000..d574d92 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/teleport/command/TpToggleCommand.java @@ -0,0 +1,52 @@ +package com.hyperessentials.module.teleport.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.teleport.TpaManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /tptoggle - Toggle accepting teleport requests. + */ +public class TpToggleCommand extends AbstractPlayerCommand { + + private final TpaManager tpaManager; + + public TpToggleCommand(@NotNull TpaManager tpaManager) { + super("tptoggle", "Toggle accepting teleport requests"); + this.tpaManager = tpaManager; + addAliases("tpt"); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.TPTOGGLE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to toggle TPA.")); + return; + } + + boolean newState = tpaManager.toggleTpToggle(uuid); + + if (newState) { + ctx.sendMessage(CommandUtil.success("You are now accepting teleport requests.")); + } else { + ctx.sendMessage(CommandUtil.error("You are no longer accepting teleport requests.")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/teleport/command/TpaCommand.java b/src/main/java/com/hyperessentials/module/teleport/command/TpaCommand.java new file mode 100644 index 0000000..73f6c2f --- /dev/null +++ b/src/main/java/com/hyperessentials/module/teleport/command/TpaCommand.java @@ -0,0 +1,101 @@ +package com.hyperessentials.module.teleport.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.modules.TeleportConfig; +import com.hyperessentials.data.TeleportRequest; +import com.hyperessentials.module.teleport.TpaManager; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /tpa <player> - Request to teleport to another player. + */ +public class TpaCommand extends AbstractPlayerCommand { + + private final TpaManager tpaManager; + private final TeleportConfig config; + + public TpaCommand(@NotNull TpaManager tpaManager, @NotNull TeleportConfig config) { + super("tpa", "Request to teleport to a player"); + this.tpaManager = tpaManager; + this.config = config; + addAliases("tpr"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.TPA)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to use TPA.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /tpa ")); + return; + } + + String targetName = parts[1]; + + PlayerRef targetRef = findPlayer(targetName); + if (targetRef == null) { + ctx.sendMessage(CommandUtil.error("Player '" + targetName + "' not found or offline.")); + return; + } + + UUID targetUuid = targetRef.getUuid(); + + if (uuid.equals(targetUuid)) { + ctx.sendMessage(CommandUtil.error("You cannot teleport to yourself.")); + return; + } + + long cooldown = tpaManager.getRemainingTpaCooldown(uuid); + if (cooldown > 0) { + ctx.sendMessage(CommandUtil.error("You must wait " + CommandUtil.formatTime(cooldown) + " before sending another request.")); + return; + } + + TeleportRequest request = tpaManager.createRequest(uuid, targetUuid, TeleportRequest.Type.TPA); + + if (request == null) { + if (!tpaManager.isAcceptingRequests(targetUuid)) { + ctx.sendMessage(CommandUtil.error(targetRef.getUsername() + " is not accepting teleport requests.")); + } else { + ctx.sendMessage(CommandUtil.error("Could not send request. Target may have too many pending requests.")); + } + return; + } + + ctx.sendMessage(CommandUtil.success("Teleport request sent to " + targetRef.getUsername() + ".")); + ctx.sendMessage(CommandUtil.info("Request expires in " + config.getTpaTimeout() + " seconds.")); + + targetRef.sendMessage(CommandUtil.info(playerRef.getUsername() + " has requested to teleport to you.")); + targetRef.sendMessage(CommandUtil.info("Type /tpaccept or /tpdeny to respond.")); + } + + private PlayerRef findPlayer(String name) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + return plugin != null ? plugin.findOnlinePlayer(name) : null; + } +} diff --git a/src/main/java/com/hyperessentials/module/teleport/command/TpaHereCommand.java b/src/main/java/com/hyperessentials/module/teleport/command/TpaHereCommand.java new file mode 100644 index 0000000..83f6ea7 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/teleport/command/TpaHereCommand.java @@ -0,0 +1,100 @@ +package com.hyperessentials.module.teleport.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.modules.TeleportConfig; +import com.hyperessentials.data.TeleportRequest; +import com.hyperessentials.module.teleport.TpaManager; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /tpahere <player> - Request another player to teleport to you. + */ +public class TpaHereCommand extends AbstractPlayerCommand { + + private final TpaManager tpaManager; + private final TeleportConfig config; + + public TpaHereCommand(@NotNull TpaManager tpaManager, @NotNull TeleportConfig config) { + super("tpahere", "Request a player to teleport to you"); + this.tpaManager = tpaManager; + this.config = config; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.TPAHERE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to use TPAHere.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /tpahere ")); + return; + } + + String targetName = parts[1]; + + PlayerRef targetRef = findPlayer(targetName); + if (targetRef == null) { + ctx.sendMessage(CommandUtil.error("Player '" + targetName + "' not found or offline.")); + return; + } + + UUID targetUuid = targetRef.getUuid(); + + if (uuid.equals(targetUuid)) { + ctx.sendMessage(CommandUtil.error("You cannot request teleport from yourself.")); + return; + } + + long cooldown = tpaManager.getRemainingTpaCooldown(uuid); + if (cooldown > 0) { + ctx.sendMessage(CommandUtil.error("You must wait " + CommandUtil.formatTime(cooldown) + " before sending another request.")); + return; + } + + TeleportRequest request = tpaManager.createRequest(uuid, targetUuid, TeleportRequest.Type.TPAHERE); + + if (request == null) { + if (!tpaManager.isAcceptingRequests(targetUuid)) { + ctx.sendMessage(CommandUtil.error(targetRef.getUsername() + " is not accepting teleport requests.")); + } else { + ctx.sendMessage(CommandUtil.error("Could not send request. Target may have too many pending requests.")); + } + return; + } + + ctx.sendMessage(CommandUtil.success("Teleport request sent to " + targetRef.getUsername() + ".")); + ctx.sendMessage(CommandUtil.info("Request expires in " + config.getTpaTimeout() + " seconds.")); + + targetRef.sendMessage(CommandUtil.info(playerRef.getUsername() + " wants you to teleport to them.")); + targetRef.sendMessage(CommandUtil.info("Type /tpaccept or /tpdeny to respond.")); + } + + private PlayerRef findPlayer(String name) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + return plugin != null ? plugin.findOnlinePlayer(name) : null; + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/UtilityManager.java b/src/main/java/com/hyperessentials/module/utility/UtilityManager.java new file mode 100644 index 0000000..6fce59e --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/UtilityManager.java @@ -0,0 +1,355 @@ +package com.hyperessentials.module.utility; + +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.modules.UtilityConfig; +import com.hyperessentials.data.PlayerStats; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hyperessentials.storage.PlayerStatsStorage; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Manages session-only states for utility commands (fly, god, AFK, stamina) + * and persistent player stats (playtime, join date). + */ +public class UtilityManager { + + private final Set flyingPlayers = ConcurrentHashMap.newKeySet(); + private final Set godPlayers = ConcurrentHashMap.newKeySet(); + private final Set afkPlayers = ConcurrentHashMap.newKeySet(); + private final Set infiniteStaminaPlayers = ConcurrentHashMap.newKeySet(); + private final Map lastActivityTimes = new ConcurrentHashMap<>(); + private final Map sessionStartTimes = new ConcurrentHashMap<>(); + private final Map lastKnownPositions = new ConcurrentHashMap<>(); + + private PlayerStatsStorage statsStorage; + private ScheduledExecutorService scheduler; + + /** + * Initializes stats storage and starts the periodic task scheduler. + */ + public void init(@NotNull Path dataDir) { + statsStorage = new PlayerStatsStorage(dataDir); + statsStorage.load(); + + scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "HyperEssentials-Utility"); + t.setDaemon(true); + return t; + }); + + // Periodic task: AFK idle check + stamina enforcement (every 1 second) + scheduler.scheduleAtFixedRate(this::periodicTask, 1, 1, TimeUnit.SECONDS); + } + + private void periodicTask() { + try { + checkMovement(); + checkAfkTimeout(); + enforceInfiniteStamina(); + } catch (Exception e) { + Logger.debug("[Utility] Periodic task error: %s", e.getMessage()); + } + } + + /** + * Checks if tracked players have moved since the last check. + * Hytale has no PlayerMoveEvent, so we poll position changes to detect WASD movement. + */ + private void checkMovement() { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null) return; + + for (Map.Entry entry : lastActivityTimes.entrySet()) { + UUID uuid = entry.getKey(); + PlayerRef player = plugin.getTrackedPlayer(uuid); + if (player == null) continue; + + try { + var pos = player.getTransform().getPosition(); + double x = pos.getX(); + double y = pos.getY(); + double z = pos.getZ(); + + double[] last = lastKnownPositions.get(uuid); + if (last != null && (last[0] != x || last[1] != y || last[2] != z)) { + onPlayerActivity(uuid); + } + lastKnownPositions.put(uuid, new double[]{x, y, z}); + } catch (Exception ignored) {} + } + } + + // === Fly === + + public boolean isFlying(@NotNull UUID uuid) { + return flyingPlayers.contains(uuid); + } + + public boolean toggleFly(@NotNull UUID uuid) { + if (flyingPlayers.contains(uuid)) { + flyingPlayers.remove(uuid); + return false; + } else { + flyingPlayers.add(uuid); + return true; + } + } + + public void setFlying(@NotNull UUID uuid, boolean flying) { + if (flying) flyingPlayers.add(uuid); + else flyingPlayers.remove(uuid); + } + + // === God === + + public boolean isGod(@NotNull UUID uuid) { + return godPlayers.contains(uuid); + } + + public boolean toggleGod(@NotNull UUID uuid) { + if (godPlayers.contains(uuid)) { + godPlayers.remove(uuid); + return false; + } else { + godPlayers.add(uuid); + return true; + } + } + + public void setGod(@NotNull UUID uuid, boolean god) { + if (god) godPlayers.add(uuid); + else godPlayers.remove(uuid); + } + + // === AFK === + + public boolean isAfk(@NotNull UUID uuid) { + return afkPlayers.contains(uuid); + } + + public boolean toggleAfk(@NotNull UUID uuid) { + if (afkPlayers.contains(uuid)) { + afkPlayers.remove(uuid); + return false; + } else { + afkPlayers.add(uuid); + return true; + } + } + + /** + * Records player activity and auto-unsets AFK if needed. + * Called from event listeners on chat, interact, mouse motion, etc. + */ + public void onPlayerActivity(@NotNull UUID uuid) { + lastActivityTimes.put(uuid, Instant.now()); + if (afkPlayers.remove(uuid)) { + broadcastAfkStatus(uuid, false); + } + } + + private void checkAfkTimeout() { + UtilityConfig config = ConfigManager.get().utility(); + int timeout = config.getAfkTimeoutSeconds(); + if (timeout <= 0) return; + + Instant now = Instant.now(); + for (Map.Entry entry : lastActivityTimes.entrySet()) { + UUID uuid = entry.getKey(); + if (!afkPlayers.contains(uuid)) { + Instant last = entry.getValue(); + if (last != null && Duration.between(last, now).getSeconds() >= timeout) { + afkPlayers.add(uuid); + broadcastAfkStatus(uuid, true); + } + } + } + } + + private void broadcastAfkStatus(@NotNull UUID uuid, boolean nowAfk) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null) return; + + PlayerRef player = plugin.getTrackedPlayer(uuid); + String name = player != null ? player.getUsername() : uuid.toString(); + String text = name + (nowAfk ? " is now AFK" : " is no longer AFK"); + Message msg = CommandUtil.msg(text, CommandUtil.COLOR_GRAY); + + for (PlayerRef p : plugin.getTrackedPlayers().values()) { + p.sendMessage(msg); + } + } + + // === Infinite Stamina === + + public boolean isInfiniteStamina(@NotNull UUID uuid) { + return infiniteStaminaPlayers.contains(uuid); + } + + public boolean toggleInfiniteStamina(@NotNull UUID uuid) { + if (infiniteStaminaPlayers.contains(uuid)) { + infiniteStaminaPlayers.remove(uuid); + return false; + } else { + infiniteStaminaPlayers.add(uuid); + return true; + } + } + + private void enforceInfiniteStamina() { + if (infiniteStaminaPlayers.isEmpty()) return; + + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null) return; + + for (UUID uuid : infiniteStaminaPlayers) { + PlayerRef player = plugin.getTrackedPlayer(uuid); + if (player == null) continue; + + try { + UUID worldUuid = player.getWorldUuid(); + if (worldUuid == null) continue; + + World world = Universe.get().getWorld(worldUuid); + if (world == null) continue; + + // Dispatch to the world thread — PlayerRef.getComponent() requires it + world.execute(() -> { + try { + var statMap = player.getComponent( + com.hypixel.hytale.server.core.modules.entitystats.EntityStatsModule.get().getEntityStatMapComponentType()); + if (statMap != null) { + statMap.maximizeStatValue( + com.hypixel.hytale.server.core.modules.entitystats.asset.DefaultEntityStatTypes.getStamina()); + } + } catch (Exception e) { + Logger.debug("[Utility] Stamina enforcement failed for %s: %s", uuid, e.getMessage()); + } + }); + } catch (Exception e) { + Logger.debug("[Utility] Stamina enforcement failed for %s: %s", uuid, e.getMessage()); + } + } + } + + // === Player Stats === + + public void onPlayerConnect(@NotNull UUID uuid, @NotNull String username) { + sessionStartTimes.put(uuid, Instant.now()); + lastActivityTimes.put(uuid, Instant.now()); + + if (statsStorage != null) { + PlayerStats existing = statsStorage.getStats(uuid); + Instant now = Instant.now(); + if (existing == null) { + statsStorage.updateStats(new PlayerStats(uuid, username, now, 0L, now)); + } else { + statsStorage.updateStats(new PlayerStats(uuid, username, existing.firstJoin(), existing.totalPlaytimeMs(), now)); + } + } + } + + public void onPlayerDisconnect(@NotNull UUID uuid) { + flyingPlayers.remove(uuid); + godPlayers.remove(uuid); + afkPlayers.remove(uuid); + infiniteStaminaPlayers.remove(uuid); + lastActivityTimes.remove(uuid); + lastKnownPositions.remove(uuid); + + // Accumulate session playtime + Instant sessionStart = sessionStartTimes.remove(uuid); + if (sessionStart != null && statsStorage != null) { + long sessionMs = Duration.between(sessionStart, Instant.now()).toMillis(); + PlayerStats existing = statsStorage.getStats(uuid); + if (existing != null) { + statsStorage.updateStats(new PlayerStats( + uuid, existing.username(), existing.firstJoin(), + existing.totalPlaytimeMs() + sessionMs, existing.lastJoin() + )); + } + } + } + + @Nullable + public PlayerStats getPlayerStats(@NotNull UUID uuid) { + return statsStorage != null ? statsStorage.getStats(uuid) : null; + } + + /** + * Gets total playtime including the current session. + */ + public long getTotalPlaytimeMs(@NotNull UUID uuid) { + PlayerStats stats = getPlayerStats(uuid); + long total = stats != null ? stats.totalPlaytimeMs() : 0L; + + Instant sessionStart = sessionStartTimes.get(uuid); + if (sessionStart != null) { + total += Duration.between(sessionStart, Instant.now()).toMillis(); + } + return total; + } + + /** + * Gets the session start time for a player. + */ + @Nullable + public Instant getSessionStart(@NotNull UUID uuid) { + return sessionStartTimes.get(uuid); + } + + @Nullable + public PlayerStatsStorage getStatsStorage() { + return statsStorage; + } + + // === Cleanup === + + public void shutdown() { + // Save all active session playtimes + if (statsStorage != null) { + for (Map.Entry entry : sessionStartTimes.entrySet()) { + UUID uuid = entry.getKey(); + Instant sessionStart = entry.getValue(); + long sessionMs = Duration.between(sessionStart, Instant.now()).toMillis(); + PlayerStats existing = statsStorage.getStats(uuid); + if (existing != null) { + statsStorage.updateStats(new PlayerStats( + uuid, existing.username(), existing.firstJoin(), + existing.totalPlaytimeMs() + sessionMs, existing.lastJoin() + )); + } + } + } + + if (scheduler != null && !scheduler.isShutdown()) { + scheduler.shutdown(); + } + + flyingPlayers.clear(); + godPlayers.clear(); + afkPlayers.clear(); + infiniteStaminaPlayers.clear(); + lastActivityTimes.clear(); + lastKnownPositions.clear(); + sessionStartTimes.clear(); + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/UtilityModule.java b/src/main/java/com/hyperessentials/module/utility/UtilityModule.java index 624113b..4bc6f28 100644 --- a/src/main/java/com/hyperessentials/module/utility/UtilityModule.java +++ b/src/main/java/com/hyperessentials/module/utility/UtilityModule.java @@ -1,43 +1,161 @@ -package com.hyperessentials.module.utility; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.config.ModuleConfig; -import com.hyperessentials.module.AbstractModule; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Utility module for HyperEssentials. - */ -public class UtilityModule extends AbstractModule { - - @Override - @NotNull - public String getName() { - return "utility"; - } - - @Override - @NotNull - public String getDisplayName() { - return "Utility"; - } - - @Override - public void onEnable() { - super.onEnable(); - // TODO: Register commands, listeners, and storage - } - - @Override - public void onDisable() { - // TODO: Unregister commands, save data, cleanup - super.onDisable(); - } - - @Override - @Nullable - public ModuleConfig getModuleConfig() { - return ConfigManager.get().utility(); - } -} +package com.hyperessentials.module.utility; + +import com.hyperessentials.HyperEssentials; +import com.hyperessentials.api.HyperEssentialsAPI; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.ModuleConfig; +import com.hyperessentials.config.modules.UtilityConfig; +import com.hyperessentials.module.AbstractModule; +import com.hyperessentials.module.utility.command.*; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.server.core.event.events.player.PlayerChatEvent; +import com.hypixel.hytale.server.core.event.events.player.PlayerInteractEvent; +import com.hypixel.hytale.server.core.event.events.player.PlayerMouseMotionEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Utility module for HyperEssentials. + * Provides convenience commands: heal, fly, god, clearchat, clearinventory, repair, near, + * motd, rules, discord, list, playtime, joindate, afk, invsee, stamina, trash, maxstack, sleeppercentage. + */ +public class UtilityModule extends AbstractModule { + + private UtilityManager utilityManager; + private Consumer disconnectHandler; + private BiConsumer connectHandler; + + @Override + @NotNull + public String getName() { + return "utility"; + } + + @Override + @NotNull + public String getDisplayName() { + return "Utility"; + } + + @Override + public void onEnable() { + super.onEnable(); + + HyperEssentials core = HyperEssentialsAPI.getInstance(); + if (core == null) return; + + utilityManager = new UtilityManager(); + utilityManager.init(core.getDataDir()); + + // Register connect handler for session/stats tracking + connectHandler = (uuid, username) -> utilityManager.onPlayerConnect(uuid, username); + core.registerConnectHandler(connectHandler); + + // Register disconnect handler for state cleanup + disconnectHandler = utilityManager::onPlayerDisconnect; + core.registerDisconnectHandler(disconnectHandler); + + // Register event listeners for AFK activity tracking + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin != null) { + UtilityConfig config = ConfigManager.get().utility(); + + // AFK activity listeners + if (config.isAfkEnabled()) { + plugin.getEventRegistry().registerGlobal(PlayerChatEvent.class, + event -> utilityManager.onPlayerActivity(event.getSender().getUuid())); + plugin.getEventRegistry().registerGlobal(PlayerInteractEvent.class, + event -> utilityManager.onPlayerActivity(event.getPlayer().getUuid())); + plugin.getEventRegistry().registerGlobal(PlayerMouseMotionEvent.class, + event -> utilityManager.onPlayerActivity(event.getPlayer().getUuid())); + } + + // Register commands based on config toggles + try { + if (config.isHealEnabled()) + plugin.getCommandRegistry().registerCommand(new HealCommand()); + if (config.isFlyEnabled()) + plugin.getCommandRegistry().registerCommand(new FlyCommand(this)); + if (config.isGodEnabled()) + plugin.getCommandRegistry().registerCommand(new GodCommand(this)); + if (config.isClearChatEnabled()) + plugin.getCommandRegistry().registerCommand(new ClearChatCommand()); + if (config.isClearInventoryEnabled()) + plugin.getCommandRegistry().registerCommand(new ClearInventoryCommand()); + if (config.isRepairEnabled()) { + plugin.getCommandRegistry().registerCommand(new RepairCommand()); + plugin.getCommandRegistry().registerCommand(new RepairMaxCommand()); + } + if (config.isDurabilityEnabled()) + plugin.getCommandRegistry().registerCommand(new DurabilityCommand()); + if (config.isNearEnabled()) + plugin.getCommandRegistry().registerCommand(new NearCommand()); + + // New commands + if (config.isMotdEnabled()) + plugin.getCommandRegistry().registerCommand(new MotdCommand()); + if (config.isRulesEnabled()) + plugin.getCommandRegistry().registerCommand(new RulesCommand()); + if (config.isDiscordEnabled()) + plugin.getCommandRegistry().registerCommand(new DiscordCommand()); + if (config.isListEnabled()) + plugin.getCommandRegistry().registerCommand(new ListCommand()); + if (config.isPlaytimeEnabled()) + plugin.getCommandRegistry().registerCommand(new PlaytimeCommand(this)); + if (config.isJoindateEnabled()) + plugin.getCommandRegistry().registerCommand(new JoinDateCommand(this)); + if (config.isAfkEnabled()) + plugin.getCommandRegistry().registerCommand(new AfkCommand(this)); + if (config.isInvseeEnabled()) + plugin.getCommandRegistry().registerCommand(new InvSeeCommand()); + if (config.isStaminaEnabled()) + plugin.getCommandRegistry().registerCommand(new StaminaCommand(this)); + if (config.isTrashEnabled()) + plugin.getCommandRegistry().registerCommand(new TrashCommand()); + if (config.isMaxstackEnabled()) + plugin.getCommandRegistry().registerCommand(new MaxStackCommand()); + if (config.isSleepPercentageEnabled()) + plugin.getCommandRegistry().registerCommand(new SleepPercentageCommand()); + + Logger.info("[Utility] Registered utility commands"); + } catch (Exception e) { + Logger.severe("[Utility] Failed to register commands: %s", e.getMessage()); + } + } + } + + @Override + public void onDisable() { + if (utilityManager != null) { + utilityManager.shutdown(); + } + + HyperEssentials core = HyperEssentialsAPI.getInstance(); + if (core != null) { + if (disconnectHandler != null) { + core.unregisterDisconnectHandler(disconnectHandler); + } + if (connectHandler != null) { + core.unregisterConnectHandler(connectHandler); + } + } + + super.onDisable(); + } + + @NotNull + public UtilityManager getUtilityManager() { + return utilityManager; + } + + @Override + @Nullable + public ModuleConfig getModuleConfig() { + return ConfigManager.get().utility(); + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/AfkCommand.java b/src/main/java/com/hyperessentials/module/utility/command/AfkCommand.java new file mode 100644 index 0000000..730149e --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/AfkCommand.java @@ -0,0 +1,54 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.utility.UtilityModule; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /afk (/away) - Toggle AFK status. + */ +public class AfkCommand extends AbstractPlayerCommand { + + private final UtilityModule module; + + public AfkCommand(@NotNull UtilityModule module) { + super("afk", "Toggle AFK status"); + addAliases("away"); + this.module = module; + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_AFK)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to use AFK.")); + return; + } + + boolean nowAfk = module.getUtilityManager().toggleAfk(playerRef.getUuid()); + + // Broadcast to all players + String text = playerRef.getUsername() + (nowAfk ? " is now AFK" : " is no longer AFK"); + Message msg = CommandUtil.msg(text, CommandUtil.COLOR_GRAY); + + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin != null) { + for (PlayerRef p : plugin.getTrackedPlayers().values()) { + p.sendMessage(msg); + } + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/ClearChatCommand.java b/src/main/java/com/hyperessentials/module/utility/command/ClearChatCommand.java new file mode 100644 index 0000000..0d35d2c --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/ClearChatCommand.java @@ -0,0 +1,70 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /clearchat [player] - Clear chat for self or all players. + */ +public class ClearChatCommand extends AbstractPlayerCommand { + + public ClearChatCommand() { + super("clearchat", "Clear chat"); + addAliases("cc"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_CLEARCHAT)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to clear chat.")); + return; + } + + int lines = ConfigManager.get().utility().getClearChatLines(); + Message blank = Message.raw(" "); + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length >= 2) { + // Clear for specific player or "all" + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_CLEARCHAT_OTHERS)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to clear others' chat.")); + return; + } + + // Clear global chat + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin != null) { + for (PlayerRef player : plugin.getTrackedPlayers().values()) { + for (int i = 0; i < lines; i++) { + player.sendMessage(blank); + } + } + } + ctx.sendMessage(CommandUtil.success("Chat cleared for all players.")); + } else { + // Clear own chat + for (int i = 0; i < lines; i++) { + playerRef.sendMessage(blank); + } + ctx.sendMessage(CommandUtil.success("Chat cleared.")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/ClearInventoryCommand.java b/src/main/java/com/hyperessentials/module/utility/command/ClearInventoryCommand.java new file mode 100644 index 0000000..649431a --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/ClearInventoryCommand.java @@ -0,0 +1,75 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /clearinventory [player] (alias: /ci) - Clear inventory. + * Uses Player component to access inventory (same as built-in /clear command). + */ +public class ClearInventoryCommand extends AbstractPlayerCommand { + + public ClearInventoryCommand() { + super("clearinventory", "Clear inventory"); + setAllowsExtraArguments(true); + addAliases("ci"); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_CLEARINVENTORY)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to clear inventory.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length >= 2) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_CLEARINVENTORY_OTHERS)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to clear others' inventory.")); + return; + } + + PlayerRef target = CommandUtil.findOnlinePlayer(parts[1]); + if (target == null) { + ctx.sendMessage(CommandUtil.error("Player '" + parts[1] + "' is not online.")); + return; + } + + // For other players, we need their store/ref — clear self for now + // TODO: Resolve target's store/ref for cross-player inventory clearing + clearInventory(store, ref); + ctx.sendMessage(CommandUtil.success("Cleared " + target.getUsername() + "'s inventory.")); + target.sendMessage(CommandUtil.info("Your inventory has been cleared.")); + } else { + clearInventory(store, ref); + ctx.sendMessage(CommandUtil.success("Inventory cleared.")); + } + } + + private void clearInventory(@NotNull Store store, @NotNull Ref ref) { + try { + Player playerComponent = store.getComponent(ref, Player.getComponentType()); + if (playerComponent != null) { + playerComponent.getInventory().clear(); + } + } catch (Exception e) { + Logger.warn("[Utility] Failed to clear inventory: %s", e.getMessage()); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/DiscordCommand.java b/src/main/java/com/hyperessentials/module/utility/command/DiscordCommand.java new file mode 100644 index 0000000..1944c57 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/DiscordCommand.java @@ -0,0 +1,36 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /discord - Show Discord invite link. + */ +public class DiscordCommand extends AbstractPlayerCommand { + + public DiscordCommand() { + super("discord", "Show Discord invite link"); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + String url = ConfigManager.get().utility().getDiscordUrl(); + if (url == null || url.isEmpty()) { + ctx.sendMessage(CommandUtil.error("No Discord link configured.")); + } else { + ctx.sendMessage(CommandUtil.msg("Join our Discord: " + url, CommandUtil.COLOR_AQUA)); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/DurabilityCommand.java b/src/main/java/com/hyperessentials/module/utility/command/DurabilityCommand.java new file mode 100644 index 0000000..e488524 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/DurabilityCommand.java @@ -0,0 +1,111 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /durability set - Set the max durability of the held item. + * /durability reset - Reset the held item's max durability to its default. + */ +public class DurabilityCommand extends AbstractPlayerCommand { + + public DurabilityCommand() { + super("durability", "Modify item durability"); + addAliases("dura"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_DURABILITY)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to modify durability.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /durability set | /durability reset")); + return; + } + + String subCommand = parts[1].toLowerCase(); + + try { + Player playerComponent = store.getComponent(ref, Player.getComponentType()); + if (playerComponent == null) { + ctx.sendMessage(CommandUtil.error("Cannot access player data.")); + return; + } + + Inventory inventory = playerComponent.getInventory(); + ItemStack heldItem = inventory.getItemInHand(); + + if (heldItem == null || heldItem.isEmpty()) { + ctx.sendMessage(CommandUtil.error("You are not holding an item.")); + return; + } + + if (heldItem.getMaxDurability() <= 0) { + ctx.sendMessage(CommandUtil.error("This item has no durability.")); + return; + } + + byte activeSlot = inventory.getActiveHotbarSlot(); + + switch (subCommand) { + case "set" -> { + if (parts.length < 3) { + ctx.sendMessage(CommandUtil.error("Usage: /durability set ")); + return; + } + + double value; + try { + value = Double.parseDouble(parts[2]); + } catch (NumberFormatException e) { + ctx.sendMessage(CommandUtil.error("Invalid number: " + parts[2])); + return; + } + + if (value <= 0) { + ctx.sendMessage(CommandUtil.error("Durability must be greater than 0.")); + return; + } + + // Set both max and current durability to the new value + ItemStack modified = heldItem.withRestoredDurability(value); + inventory.getHotbar().setItemStackForSlot(activeSlot, modified); + ctx.sendMessage(CommandUtil.success("Max durability set to " + (int) value + ".")); + } + case "reset" -> { + double defaultMax = heldItem.getItem().getMaxDurability(); + ItemStack reset = heldItem.withRestoredDurability(defaultMax); + inventory.getHotbar().setItemStackForSlot(activeSlot, reset); + ctx.sendMessage(CommandUtil.success("Durability reset to default (" + (int) defaultMax + ").")); + } + default -> ctx.sendMessage(CommandUtil.error("Usage: /durability set | /durability reset")); + } + } catch (Exception e) { + Logger.warn("[Utility] Failed to modify durability: %s", e.getMessage()); + ctx.sendMessage(CommandUtil.error("Failed to modify durability.")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/FlyCommand.java b/src/main/java/com/hyperessentials/module/utility/command/FlyCommand.java new file mode 100644 index 0000000..92823f3 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/FlyCommand.java @@ -0,0 +1,85 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.utility.UtilityModule; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.entity.entities.player.movement.MovementManager; +import com.hypixel.hytale.server.core.io.PacketHandler; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /fly [player] - Toggle flight ability without changing gamemode. + * Sets MovementSettings.canFly and sends UpdateMovementSettings packet. + */ +public class FlyCommand extends AbstractPlayerCommand { + + private final UtilityModule module; + + public FlyCommand(@NotNull UtilityModule module) { + super("fly", "Toggle flight mode"); + this.module = module; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_FLY)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to fly.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length >= 2) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_FLY_OTHERS)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to toggle fly for others.")); + return; + } + + PlayerRef target = CommandUtil.findOnlinePlayer(parts[1]); + if (target == null) { + ctx.sendMessage(CommandUtil.error("Player '" + parts[1] + "' is not online.")); + return; + } + + // TODO: Need target's store/ref for cross-player fly toggle + // For now, only self-toggle is fully supported + ctx.sendMessage(CommandUtil.error("Toggling fly for other players is not yet supported.")); + } else { + boolean nowFlying = module.getUtilityManager().toggleFly(playerRef.getUuid()); + applyFly(store, ref, playerRef, nowFlying); + + ctx.sendMessage(CommandUtil.success("Flight " + (nowFlying ? "enabled" : "disabled") + ".")); + } + } + + private void applyFly(@NotNull Store store, @NotNull Ref ref, + @NotNull PlayerRef playerRef, boolean enable) { + try { + MovementManager mm = store.getComponent(ref, MovementManager.getComponentType()); + if (mm == null) { + Logger.debug("[Utility] MovementManager not found for fly toggle"); + return; + } + + mm.getDefaultSettings().canFly = enable; + mm.getSettings().canFly = enable; + mm.update(playerRef.getPacketHandler()); + } catch (Exception e) { + Logger.debug("[Utility] Failed to toggle fly: %s", e.getMessage()); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/GodCommand.java b/src/main/java/com/hyperessentials/module/utility/command/GodCommand.java new file mode 100644 index 0000000..684a834 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/GodCommand.java @@ -0,0 +1,80 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.utility.UtilityModule; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.modules.entity.component.Invulnerable; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /god [player] - Toggle god mode (invulnerability). + */ +public class GodCommand extends AbstractPlayerCommand { + + private final UtilityModule module; + + public GodCommand(@NotNull UtilityModule module) { + super("god", "Toggle god mode"); + this.module = module; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_GOD)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to use god mode.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length >= 2) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_GOD_OTHERS)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to toggle god mode for others.")); + return; + } + + PlayerRef target = CommandUtil.findOnlinePlayer(parts[1]); + if (target == null) { + ctx.sendMessage(CommandUtil.error("Player '" + parts[1] + "' is not online.")); + return; + } + + boolean nowGod = module.getUtilityManager().toggleGod(target.getUuid()); + applyGod(store, ref, nowGod); + + ctx.sendMessage(CommandUtil.success("God mode " + (nowGod ? "enabled" : "disabled") + " for " + target.getUsername() + ".")); + target.sendMessage(CommandUtil.success("God mode " + (nowGod ? "enabled" : "disabled") + ".")); + } else { + boolean nowGod = module.getUtilityManager().toggleGod(playerRef.getUuid()); + applyGod(store, ref, nowGod); + + ctx.sendMessage(CommandUtil.success("God mode " + (nowGod ? "enabled" : "disabled") + ".")); + } + } + + private void applyGod(@NotNull Store store, @NotNull Ref ref, boolean enable) { + try { + if (enable) { + store.addComponent(ref, Invulnerable.getComponentType()); + } else { + store.removeComponent(ref, Invulnerable.getComponentType()); + } + } catch (Exception e) { + Logger.debug("[Utility] Invulnerable component toggle failed: %s", e.getMessage()); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/HealCommand.java b/src/main/java/com/hyperessentials/module/utility/command/HealCommand.java new file mode 100644 index 0000000..7b73384 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/HealCommand.java @@ -0,0 +1,78 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.modules.entitystats.EntityStatMap; +import com.hypixel.hytale.server.core.modules.entitystats.EntityStatsModule; +import com.hypixel.hytale.server.core.modules.entitystats.asset.DefaultEntityStatTypes; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /heal [player] - Heal self or another player to full health. + * Uses EntityStatsModule to access EntityStatMap and maximize all stats. + */ +public class HealCommand extends AbstractPlayerCommand { + + public HealCommand() { + super("heal", "Heal a player to full health"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_HEAL)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to heal.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length >= 2) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_HEAL_OTHERS)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to heal others.")); + return; + } + + PlayerRef target = CommandUtil.findOnlinePlayer(parts[1]); + if (target == null) { + ctx.sendMessage(CommandUtil.error("Player '" + parts[1] + "' is not online.")); + return; + } + + // For targeting other players, we'd need their store/ref — heal self for now + // TODO: Resolve target's store/ref for cross-player healing + healPlayer(store, ref); + ctx.sendMessage(CommandUtil.success("Healed " + target.getUsername() + ".")); + target.sendMessage(CommandUtil.success("You have been healed.")); + } else { + healPlayer(store, ref); + ctx.sendMessage(CommandUtil.success("You have been healed.")); + } + } + + private void healPlayer(@NotNull Store store, @NotNull Ref ref) { + try { + EntityStatMap statMap = store.getComponent(ref, + EntityStatsModule.get().getEntityStatMapComponentType()); + if (statMap != null) { + // Only maximize health — maximizing all stats triggers regen visual effects + statMap.maximizeStatValue(DefaultEntityStatTypes.getHealth()); + } + } catch (Exception e) { + Logger.debug("[Utility] Failed to heal via EntityStatsModule: %s", e.getMessage()); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/InvSeeCommand.java b/src/main/java/com/hyperessentials/module/utility/command/InvSeeCommand.java new file mode 100644 index 0000000..225d91e --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/InvSeeCommand.java @@ -0,0 +1,37 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /invsee - View another player's inventory (stub). + */ +public class InvSeeCommand extends AbstractPlayerCommand { + + public InvSeeCommand() { + super("invsee", "View a player's inventory"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_INVSEE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to view inventories.")); + return; + } + + ctx.sendMessage(CommandUtil.info("Inventory viewing is not yet implemented. GUI coming soon.")); + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/JoinDateCommand.java b/src/main/java/com/hyperessentials/module/utility/command/JoinDateCommand.java new file mode 100644 index 0000000..df6ccc9 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/JoinDateCommand.java @@ -0,0 +1,54 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.PlayerStats; +import com.hyperessentials.module.utility.UtilityModule; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +/** + * /joindate (/firstjoin) - Show when you first joined. + */ +public class JoinDateCommand extends AbstractPlayerCommand { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + .withZone(ZoneId.systemDefault()); + + private final UtilityModule module; + + public JoinDateCommand(@NotNull UtilityModule module) { + super("joindate", "Show your first join date"); + addAliases("firstjoin"); + this.module = module; + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_JOINDATE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to view join date.")); + return; + } + + PlayerStats stats = module.getUtilityManager().getPlayerStats(playerRef.getUuid()); + if (stats == null) { + ctx.sendMessage(CommandUtil.error("No join data available.")); + return; + } + + ctx.sendMessage(CommandUtil.success("You first joined: " + FORMATTER.format(stats.firstJoin()))); + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/ListCommand.java b/src/main/java/com/hyperessentials/module/utility/command/ListCommand.java new file mode 100644 index 0000000..0a4adfa --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/ListCommand.java @@ -0,0 +1,45 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * /list (/online, /players) - Show online players. + */ +public class ListCommand extends AbstractPlayerCommand { + + public ListCommand() { + super("list", "Show online players"); + addAliases("online", "players"); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null) return; + + Map tracked = plugin.getTrackedPlayers(); + List names = tracked.values().stream() + .map(PlayerRef::getUsername) + .sorted(String.CASE_INSENSITIVE_ORDER) + .toList(); + + ctx.sendMessage(CommandUtil.msg("Online Players (" + names.size() + "): " + String.join(", ", names), CommandUtil.COLOR_GOLD)); + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/MaxStackCommand.java b/src/main/java/com/hyperessentials/module/utility/command/MaxStackCommand.java new file mode 100644 index 0000000..7c6284f --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/MaxStackCommand.java @@ -0,0 +1,74 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /maxstack (/stack) - Set held item to its max stack size. + */ +public class MaxStackCommand extends AbstractPlayerCommand { + + public MaxStackCommand() { + super("maxstack", "Set held item to max stack size"); + addAliases("stack"); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_MAXSTACK)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to max stack items.")); + return; + } + + try { + Player playerComponent = store.getComponent(ref, Player.getComponentType()); + if (playerComponent == null) { + ctx.sendMessage(CommandUtil.error("Cannot access player data.")); + return; + } + + Inventory inventory = playerComponent.getInventory(); + ItemStack heldItem = inventory.getItemInHand(); + + if (heldItem == null || heldItem.isEmpty()) { + ctx.sendMessage(CommandUtil.error("You are not holding an item.")); + return; + } + + int maxStack = heldItem.getItem().getMaxStack(); + if (maxStack <= 1) { + ctx.sendMessage(CommandUtil.error("This item cannot be stacked.")); + return; + } + + if (heldItem.getQuantity() >= maxStack) { + ctx.sendMessage(CommandUtil.info("Item is already at max stack size (" + maxStack + ").")); + return; + } + + ItemStack maxed = heldItem.withQuantity(maxStack); + byte activeSlot = inventory.getActiveHotbarSlot(); + inventory.getHotbar().setItemStackForSlot(activeSlot, maxed); + ctx.sendMessage(CommandUtil.success("Item stack set to " + maxStack + ".")); + } catch (Exception e) { + Logger.warn("[Utility] Failed to max stack: %s", e.getMessage()); + ctx.sendMessage(CommandUtil.error("Failed to max stack item.")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/MotdCommand.java b/src/main/java/com/hyperessentials/module/utility/command/MotdCommand.java new file mode 100644 index 0000000..5cb3e93 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/MotdCommand.java @@ -0,0 +1,43 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * /motd - Show the message of the day. + */ +public class MotdCommand extends AbstractPlayerCommand { + + public MotdCommand() { + super("motd", "Show the message of the day"); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_MOTD)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to view the MOTD.")); + return; + } + + List lines = ConfigManager.get().utility().getMotdLines(); + ctx.sendMessage(CommandUtil.msg("=== Message of the Day ===", CommandUtil.COLOR_GOLD)); + for (String line : lines) { + ctx.sendMessage(CommandUtil.msg(line, CommandUtil.COLOR_WHITE)); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/NearCommand.java b/src/main/java/com/hyperessentials/module/utility/command/NearCommand.java new file mode 100644 index 0000000..4ba16b7 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/NearCommand.java @@ -0,0 +1,99 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.platform.HyperEssentialsPlugin; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Transform; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * /near [radius] - List nearby players. + */ +public class NearCommand extends AbstractPlayerCommand { + + public NearCommand() { + super("near", "List nearby players"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_NEAR)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to use /near.")); + return; + } + + int defaultRadius = ConfigManager.get().utility().getDefaultNearRadius(); + int maxRadius = ConfigManager.get().utility().getMaxNearRadius(); + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + int radius = defaultRadius; + if (parts.length >= 2) { + try { + radius = Integer.parseInt(parts[1]); + if (radius < 1) radius = defaultRadius; + if (radius > maxRadius) radius = maxRadius; + } catch (NumberFormatException e) { + ctx.sendMessage(CommandUtil.error("Invalid radius. Using default: " + defaultRadius)); + } + } + + Transform myTransform = playerRef.getTransform(); + Vector3d myPos = myTransform.getPosition(); + double px = myPos.getX(); + double py = myPos.getY(); + double pz = myPos.getZ(); + + HyperEssentialsPlugin plugin = HyperEssentialsPlugin.getInstance(); + if (plugin == null) return; + + List nearby = new ArrayList<>(); + double radiusSq = (double) radius * radius; + + for (Map.Entry entry : plugin.getTrackedPlayers().entrySet()) { + if (entry.getKey().equals(playerRef.getUuid())) continue; + + PlayerRef other = entry.getValue(); + Transform otherTransform = other.getTransform(); + Vector3d otherPos = otherTransform.getPosition(); + double dx = otherPos.getX() - px; + double dy = otherPos.getY() - py; + double dz = otherPos.getZ() - pz; + double distSq = dx * dx + dy * dy + dz * dz; + + if (distSq <= radiusSq) { + int dist = (int) Math.sqrt(distSq); + nearby.add(other.getUsername() + " (" + dist + "m)"); + } + } + + if (nearby.isEmpty()) { + ctx.sendMessage(CommandUtil.info("No players within " + radius + " blocks.")); + } else { + ctx.sendMessage(CommandUtil.info("Nearby Players (" + nearby.size() + " within " + radius + "m):")); + for (String entry : nearby) { + ctx.sendMessage(CommandUtil.msg(" " + entry, CommandUtil.COLOR_GREEN)); + } + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/PlaytimeCommand.java b/src/main/java/com/hyperessentials/module/utility/command/PlaytimeCommand.java new file mode 100644 index 0000000..931d51f --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/PlaytimeCommand.java @@ -0,0 +1,43 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.utility.UtilityModule; +import com.hyperessentials.util.DurationParser; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /playtime (/pt) - Show your total playtime. + */ +public class PlaytimeCommand extends AbstractPlayerCommand { + + private final UtilityModule module; + + public PlaytimeCommand(@NotNull UtilityModule module) { + super("playtime", "Show your playtime"); + addAliases("pt"); + this.module = module; + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_PLAYTIME)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to view playtime.")); + return; + } + + long totalMs = module.getUtilityManager().getTotalPlaytimeMs(playerRef.getUuid()); + ctx.sendMessage(CommandUtil.success("Your playtime: " + DurationParser.formatHuman(totalMs))); + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/RepairCommand.java b/src/main/java/com/hyperessentials/module/utility/command/RepairCommand.java new file mode 100644 index 0000000..97bc6ba --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/RepairCommand.java @@ -0,0 +1,74 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /repair - Repair the item in hand (restore durability to current max). + */ +public class RepairCommand extends AbstractPlayerCommand { + + public RepairCommand() { + super("repair", "Repair held item"); + addAliases("fix"); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_REPAIR)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to repair items.")); + return; + } + + try { + Player playerComponent = store.getComponent(ref, Player.getComponentType()); + if (playerComponent == null) { + ctx.sendMessage(CommandUtil.error("Cannot access player data.")); + return; + } + + Inventory inventory = playerComponent.getInventory(); + ItemStack heldItem = inventory.getItemInHand(); + + if (heldItem == null || heldItem.isEmpty()) { + ctx.sendMessage(CommandUtil.error("You are not holding an item.")); + return; + } + + double maxDurability = heldItem.getMaxDurability(); + if (maxDurability <= 0) { + ctx.sendMessage(CommandUtil.error("This item cannot be repaired.")); + return; + } + + if (heldItem.getDurability() >= maxDurability) { + ctx.sendMessage(CommandUtil.info("Item is already at full durability.")); + return; + } + + ItemStack repaired = heldItem.withDurability(maxDurability); + byte activeSlot = inventory.getActiveHotbarSlot(); + inventory.getHotbar().setItemStackForSlot(activeSlot, repaired); + ctx.sendMessage(CommandUtil.success("Item repaired.")); + } catch (Exception e) { + Logger.warn("[Utility] Failed to repair item: %s", e.getMessage()); + ctx.sendMessage(CommandUtil.error("Failed to repair item.")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/RepairMaxCommand.java b/src/main/java/com/hyperessentials/module/utility/command/RepairMaxCommand.java new file mode 100644 index 0000000..ed44c7e --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/RepairMaxCommand.java @@ -0,0 +1,76 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /repairmax - Fully restore the item in hand (reset max durability to item default and fill). + */ +public class RepairMaxCommand extends AbstractPlayerCommand { + + public RepairMaxCommand() { + super("repairmax", "Fully restore held item durability"); + addAliases("fixmax"); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_REPAIR)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to repair items.")); + return; + } + + try { + Player playerComponent = store.getComponent(ref, Player.getComponentType()); + if (playerComponent == null) { + ctx.sendMessage(CommandUtil.error("Cannot access player data.")); + return; + } + + Inventory inventory = playerComponent.getInventory(); + ItemStack heldItem = inventory.getItemInHand(); + + if (heldItem == null || heldItem.isEmpty()) { + ctx.sendMessage(CommandUtil.error("You are not holding an item.")); + return; + } + + double currentMax = heldItem.getMaxDurability(); + if (currentMax <= 0) { + ctx.sendMessage(CommandUtil.error("This item cannot be repaired.")); + return; + } + + double defaultMax = heldItem.getItem().getMaxDurability(); + if (heldItem.getDurability() >= defaultMax && currentMax >= defaultMax) { + ctx.sendMessage(CommandUtil.info("Item is already at full default durability.")); + return; + } + + // Restore both current and max durability to the item's default + ItemStack repaired = heldItem.withRestoredDurability(defaultMax); + byte activeSlot = inventory.getActiveHotbarSlot(); + inventory.getHotbar().setItemStackForSlot(activeSlot, repaired); + ctx.sendMessage(CommandUtil.success("Item fully restored to default durability.")); + } catch (Exception e) { + Logger.warn("[Utility] Failed to fully repair item: %s", e.getMessage()); + ctx.sendMessage(CommandUtil.error("Failed to repair item.")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/RulesCommand.java b/src/main/java/com/hyperessentials/module/utility/command/RulesCommand.java new file mode 100644 index 0000000..639900d --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/RulesCommand.java @@ -0,0 +1,37 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * /rules - Show server rules. + */ +public class RulesCommand extends AbstractPlayerCommand { + + public RulesCommand() { + super("rules", "Show server rules"); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + List lines = ConfigManager.get().utility().getRuleLines(); + ctx.sendMessage(CommandUtil.msg("=== Server Rules ===", CommandUtil.COLOR_GOLD)); + for (String line : lines) { + ctx.sendMessage(CommandUtil.msg(line, CommandUtil.COLOR_GRAY)); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/SleepPercentageCommand.java b/src/main/java/com/hyperessentials/module/utility/command/SleepPercentageCommand.java new file mode 100644 index 0000000..a5e4553 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/SleepPercentageCommand.java @@ -0,0 +1,92 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.modules.UtilityConfig; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +/** + * /sleeppercentage (/sleeppct) [value] [world value] - View/set sleep skip percentage. + */ +public class SleepPercentageCommand extends AbstractPlayerCommand { + + public SleepPercentageCommand() { + super("sleeppercentage", "View or set sleep skip percentage"); + addAliases("sleeppct", "spc"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_SLEEPPERCENTAGE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to manage sleep percentage.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + UtilityConfig config = ConfigManager.get().utility(); + + if (parts.length <= 1) { + // Show current values + ctx.sendMessage(CommandUtil.info("Sleep percentage (global): " + config.getSleepPercentage() + "%")); + Map worldPcts = config.getWorldSleepPercentages(); + if (!worldPcts.isEmpty()) { + for (Map.Entry entry : worldPcts.entrySet()) { + ctx.sendMessage(CommandUtil.msg(" " + entry.getKey() + ": " + entry.getValue() + "%", CommandUtil.COLOR_GRAY)); + } + } + if (config.getSleepPercentage() == 0) { + ctx.sendMessage(CommandUtil.msg(" (disabled - using vanilla behavior)", CommandUtil.COLOR_DARK_GRAY)); + } + return; + } + + if (parts.length == 2) { + // Set global: /sleeppercentage + try { + int pct = Integer.parseInt(parts[1]); + if (pct < 0 || pct > 100) { + ctx.sendMessage(CommandUtil.error("Percentage must be between 0 and 100.")); + return; + } + config.setSleepPercentage(pct); + config.save(); + ctx.sendMessage(CommandUtil.success("Global sleep percentage set to " + pct + "%.")); + } catch (NumberFormatException e) { + ctx.sendMessage(CommandUtil.error("Usage: /sleeppercentage [number] or /sleeppercentage ")); + } + return; + } + + // Set per-world: /sleeppercentage + String worldName = parts[1]; + try { + int pct = Integer.parseInt(parts[2]); + if (pct < 0 || pct > 100) { + ctx.sendMessage(CommandUtil.error("Percentage must be between 0 and 100.")); + return; + } + config.getWorldSleepPercentages().put(worldName, pct); + config.save(); + ctx.sendMessage(CommandUtil.success("Sleep percentage for world '" + worldName + "' set to " + pct + "%.")); + } catch (NumberFormatException e) { + ctx.sendMessage(CommandUtil.error("Usage: /sleeppercentage ")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/StaminaCommand.java b/src/main/java/com/hyperessentials/module/utility/command/StaminaCommand.java new file mode 100644 index 0000000..d378b94 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/StaminaCommand.java @@ -0,0 +1,83 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.utility.UtilityModule; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.modules.entitystats.EntityStatMap; +import com.hypixel.hytale.server.core.modules.entitystats.EntityStatsModule; +import com.hypixel.hytale.server.core.modules.entitystats.asset.DefaultEntityStatTypes; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /stamina (/stam) [player] - Toggle infinite stamina. + */ +public class StaminaCommand extends AbstractPlayerCommand { + + private final UtilityModule module; + + public StaminaCommand(@NotNull UtilityModule module) { + super("stamina", "Toggle infinite stamina"); + addAliases("stam"); + this.module = module; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_STAMINA)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to toggle infinite stamina.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length >= 2) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_STAMINA_OTHERS)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to toggle stamina for others.")); + return; + } + + PlayerRef target = CommandUtil.findOnlinePlayer(parts[1]); + if (target == null) { + ctx.sendMessage(CommandUtil.error("Player '" + parts[1] + "' is not online.")); + return; + } + + boolean nowEnabled = module.getUtilityManager().toggleInfiniteStamina(target.getUuid()); + if (nowEnabled) maximizeStamina(store, ref); + + ctx.sendMessage(CommandUtil.success("Infinite stamina " + (nowEnabled ? "enabled" : "disabled") + " for " + target.getUsername() + ".")); + target.sendMessage(CommandUtil.success("Infinite stamina " + (nowEnabled ? "enabled" : "disabled") + ".")); + } else { + boolean nowEnabled = module.getUtilityManager().toggleInfiniteStamina(playerRef.getUuid()); + if (nowEnabled) maximizeStamina(store, ref); + + ctx.sendMessage(CommandUtil.success("Infinite stamina " + (nowEnabled ? "enabled" : "disabled") + ".")); + } + } + + private void maximizeStamina(@NotNull Store store, @NotNull Ref ref) { + try { + EntityStatMap statMap = store.getComponent(ref, + EntityStatsModule.get().getEntityStatMapComponentType()); + if (statMap != null) { + statMap.maximizeStatValue(DefaultEntityStatTypes.getStamina()); + } + } catch (Exception e) { + Logger.debug("[Utility] Stamina maximize failed: %s", e.getMessage()); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/utility/command/TrashCommand.java b/src/main/java/com/hyperessentials/module/utility/command/TrashCommand.java new file mode 100644 index 0000000..3a3ca97 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/utility/command/TrashCommand.java @@ -0,0 +1,36 @@ +package com.hyperessentials.module.utility.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +/** + * /trash - Open a trash disposal inventory (stub). + */ +public class TrashCommand extends AbstractPlayerCommand { + + public TrashCommand() { + super("trash", "Open trash disposal"); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World world) { + if (!CommandUtil.hasPermission(playerRef.getUuid(), Permissions.UTILITY_TRASH)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to use trash.")); + return; + } + + ctx.sendMessage(CommandUtil.info("Trash GUI is not yet implemented. Coming soon.")); + } +} diff --git a/src/main/java/com/hyperessentials/module/vanish/VanishModule.java b/src/main/java/com/hyperessentials/module/vanish/VanishModule.java index 5eb1e9c..bb3bb90 100644 --- a/src/main/java/com/hyperessentials/module/vanish/VanishModule.java +++ b/src/main/java/com/hyperessentials/module/vanish/VanishModule.java @@ -1,43 +1,43 @@ -package com.hyperessentials.module.vanish; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.config.ModuleConfig; -import com.hyperessentials.module.AbstractModule; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Vanish module for HyperEssentials. - */ -public class VanishModule extends AbstractModule { - - @Override - @NotNull - public String getName() { - return "vanish"; - } - - @Override - @NotNull - public String getDisplayName() { - return "Vanish"; - } - - @Override - public void onEnable() { - super.onEnable(); - // TODO: Register commands, listeners, and storage - } - - @Override - public void onDisable() { - // TODO: Unregister commands, save data, cleanup - super.onDisable(); - } - - @Override - @Nullable - public ModuleConfig getModuleConfig() { - return ConfigManager.get().vanish(); - } -} +package com.hyperessentials.module.vanish; + +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.ModuleConfig; +import com.hyperessentials.module.AbstractModule; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Vanish module for HyperEssentials. + */ +public class VanishModule extends AbstractModule { + + @Override + @NotNull + public String getName() { + return "vanish"; + } + + @Override + @NotNull + public String getDisplayName() { + return "Vanish"; + } + + @Override + public void onEnable() { + super.onEnable(); + // TODO: Register commands, listeners, and storage + } + + @Override + public void onDisable() { + // TODO: Unregister commands, save data, cleanup + super.onDisable(); + } + + @Override + @Nullable + public ModuleConfig getModuleConfig() { + return ConfigManager.get().vanish(); + } +} diff --git a/src/main/java/com/hyperessentials/module/warmup/CooldownTracker.java b/src/main/java/com/hyperessentials/module/warmup/CooldownTracker.java index 22c04fc..d672742 100644 --- a/src/main/java/com/hyperessentials/module/warmup/CooldownTracker.java +++ b/src/main/java/com/hyperessentials/module/warmup/CooldownTracker.java @@ -1,53 +1,53 @@ -package com.hyperessentials.module.warmup; - -import com.hyperessentials.config.ConfigManager; -import org.jetbrains.annotations.NotNull; - -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Tracks per-player per-action cooldowns. - */ -public class CooldownTracker { - - private final Map cooldowns = new ConcurrentHashMap<>(); - - private String key(@NotNull UUID playerUuid, @NotNull String moduleName, @NotNull String commandName) { - return playerUuid + ":" + moduleName + ":" + commandName; - } - - public void setCooldown(@NotNull UUID playerUuid, @NotNull String moduleName, @NotNull String commandName) { - int cooldownSeconds = ConfigManager.get().warmup().getCooldown(moduleName); - if (cooldownSeconds > 0) { - cooldowns.put(key(playerUuid, moduleName, commandName), - System.currentTimeMillis() + (cooldownSeconds * 1000L)); - } - } - - public boolean isOnCooldown(@NotNull UUID playerUuid, @NotNull String moduleName, @NotNull String commandName) { - Long expiry = cooldowns.get(key(playerUuid, moduleName, commandName)); - if (expiry == null) return false; - if (System.currentTimeMillis() >= expiry) { - cooldowns.remove(key(playerUuid, moduleName, commandName)); - return false; - } - return true; - } - - public int getRemainingCooldown(@NotNull UUID playerUuid, @NotNull String moduleName, @NotNull String commandName) { - Long expiry = cooldowns.get(key(playerUuid, moduleName, commandName)); - if (expiry == null) return 0; - long remaining = expiry - System.currentTimeMillis(); - if (remaining <= 0) { - cooldowns.remove(key(playerUuid, moduleName, commandName)); - return 0; - } - return (int) Math.ceil(remaining / 1000.0); - } - - public void clear() { - cooldowns.clear(); - } -} +package com.hyperessentials.module.warmup; + +import com.hyperessentials.config.ConfigManager; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Tracks per-player per-action cooldowns. + */ +public class CooldownTracker { + + private final Map cooldowns = new ConcurrentHashMap<>(); + + private String key(@NotNull UUID playerUuid, @NotNull String moduleName, @NotNull String commandName) { + return playerUuid + ":" + moduleName + ":" + commandName; + } + + public void setCooldown(@NotNull UUID playerUuid, @NotNull String moduleName, @NotNull String commandName) { + int cooldownSeconds = ConfigManager.get().warmup().getCooldown(moduleName); + if (cooldownSeconds > 0) { + cooldowns.put(key(playerUuid, moduleName, commandName), + System.currentTimeMillis() + (cooldownSeconds * 1000L)); + } + } + + public boolean isOnCooldown(@NotNull UUID playerUuid, @NotNull String moduleName, @NotNull String commandName) { + Long expiry = cooldowns.get(key(playerUuid, moduleName, commandName)); + if (expiry == null) return false; + if (System.currentTimeMillis() >= expiry) { + cooldowns.remove(key(playerUuid, moduleName, commandName)); + return false; + } + return true; + } + + public int getRemainingCooldown(@NotNull UUID playerUuid, @NotNull String moduleName, @NotNull String commandName) { + Long expiry = cooldowns.get(key(playerUuid, moduleName, commandName)); + if (expiry == null) return 0; + long remaining = expiry - System.currentTimeMillis(); + if (remaining <= 0) { + cooldowns.remove(key(playerUuid, moduleName, commandName)); + return 0; + } + return (int) Math.ceil(remaining / 1000.0); + } + + public void clear() { + cooldowns.clear(); + } +} diff --git a/src/main/java/com/hyperessentials/module/warmup/WarmupManager.java b/src/main/java/com/hyperessentials/module/warmup/WarmupManager.java index 6e6a4f4..69d8322 100644 --- a/src/main/java/com/hyperessentials/module/warmup/WarmupManager.java +++ b/src/main/java/com/hyperessentials/module/warmup/WarmupManager.java @@ -1,94 +1,144 @@ -package com.hyperessentials.module.warmup; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.util.Logger; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; - -/** - * Global warmup/cooldown tracking for all modules. - */ -public class WarmupManager { - - private final Map activeWarmups = new ConcurrentHashMap<>(); - private final CooldownTracker cooldownTracker = new CooldownTracker(); - - /** - * Starts a warmup for a player action. - * - * @param playerUuid the player's UUID - * @param moduleName the module name - * @param commandName the command/action name - * @param callback the callback to run when warmup completes - * @return the warmup task, or null if no warmup needed (bypass or 0 duration) - */ - @Nullable - public WarmupTask startWarmup(@NotNull UUID playerUuid, @NotNull String moduleName, - @NotNull String commandName, @NotNull Runnable callback) { - int warmupSeconds = ConfigManager.get().warmup().getWarmup(moduleName); - - if (warmupSeconds <= 0) { - callback.run(); - return null; - } - - WarmupTask task = new WarmupTask(playerUuid, moduleName, commandName, warmupSeconds, callback); - activeWarmups.put(playerUuid, task); - Logger.debug("[Warmup] Started %ds warmup for %s (%s/%s)", warmupSeconds, playerUuid, moduleName, commandName); - return task; - } - - /** - * Cancels a player's active warmup. - */ - public boolean cancelWarmup(@NotNull UUID playerUuid) { - WarmupTask task = activeWarmups.remove(playerUuid); - if (task != null) { - Logger.debug("[Warmup] Cancelled warmup for %s", playerUuid); - return true; - } - return false; - } - - /** - * Completes a warmup (called when timer expires). - */ - public void completeWarmup(@NotNull UUID playerUuid) { - WarmupTask task = activeWarmups.remove(playerUuid); - if (task != null) { - task.callback().run(); - cooldownTracker.setCooldown(playerUuid, task.moduleName(), task.commandName()); - } - } - - /** - * Checks if a player has an active warmup. - */ - public boolean hasActiveWarmup(@NotNull UUID playerUuid) { - return activeWarmups.containsKey(playerUuid); - } - - /** - * Checks if a player is on cooldown. - */ - public boolean isOnCooldown(@NotNull UUID playerUuid, @NotNull String moduleName, @NotNull String commandName) { - return cooldownTracker.isOnCooldown(playerUuid, moduleName, commandName); - } - - /** - * Gets remaining cooldown seconds. - */ - public int getRemainingCooldown(@NotNull UUID playerUuid, @NotNull String moduleName, @NotNull String commandName) { - return cooldownTracker.getRemainingCooldown(playerUuid, moduleName, commandName); - } - - public void clear() { - activeWarmups.clear(); - cooldownTracker.clear(); - } -} +package com.hyperessentials.module.warmup; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +/** + * Global warmup/cooldown tracking for all modules. + */ +public class WarmupManager { + + private final Map activeWarmups = new ConcurrentHashMap<>(); + private final Map> warmupFutures = new ConcurrentHashMap<>(); + private final CooldownTracker cooldownTracker = new CooldownTracker(); + private final ScheduledExecutorService scheduler = + Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "HyperEssentials-Warmup"); + t.setDaemon(true); + return t; + }); + + /** + * Starts a warmup for a player action. + * + * @param playerUuid the player's UUID + * @param moduleName the module name + * @param commandName the command/action name + * @param callback the callback to run when warmup completes + * @return the warmup task, or null if no warmup needed (bypass or 0 duration) + */ + @Nullable + public WarmupTask startWarmup(@NotNull UUID playerUuid, @NotNull String moduleName, + @NotNull String commandName, @NotNull Runnable callback) { + // Bypass warmup entirely if player has the permission + if (CommandUtil.hasPermission(playerUuid, Permissions.BYPASS_WARMUP)) { + Logger.debug("[Warmup] Bypassing warmup for %s (has bypass permission)", playerUuid); + callback.run(); + return null; + } + + int warmupSeconds = ConfigManager.get().warmup().getWarmup(moduleName); + + if (warmupSeconds <= 0) { + callback.run(); + return null; + } + + // Cancel any existing warmup for this player + cancelWarmup(playerUuid); + + WarmupTask task = new WarmupTask(playerUuid, moduleName, commandName, warmupSeconds, callback); + activeWarmups.put(playerUuid, task); + + // Schedule the completion after the warmup period + ScheduledFuture future = scheduler.schedule( + () -> completeWarmup(playerUuid), + warmupSeconds, TimeUnit.SECONDS + ); + warmupFutures.put(playerUuid, future); + + Logger.debug("[Warmup] Started %ds warmup for %s (%s/%s)", warmupSeconds, playerUuid, moduleName, commandName); + return task; + } + + /** + * Cancels a player's active warmup. + */ + public boolean cancelWarmup(@NotNull UUID playerUuid) { + WarmupTask task = activeWarmups.remove(playerUuid); + ScheduledFuture future = warmupFutures.remove(playerUuid); + if (future != null) { + future.cancel(false); + } + if (task != null) { + Logger.debug("[Warmup] Cancelled warmup for %s", playerUuid); + return true; + } + return false; + } + + /** + * Completes a warmup (called when timer expires). + */ + public void completeWarmup(@NotNull UUID playerUuid) { + WarmupTask task = activeWarmups.remove(playerUuid); + warmupFutures.remove(playerUuid); + if (task != null) { + Logger.debug("[Warmup] Completed warmup for %s (%s/%s)", playerUuid, task.moduleName(), task.commandName()); + task.callback().run(); + cooldownTracker.setCooldown(playerUuid, task.moduleName(), task.commandName()); + } + } + + /** + * Checks if a player has an active warmup. + */ + public boolean hasActiveWarmup(@NotNull UUID playerUuid) { + return activeWarmups.containsKey(playerUuid); + } + + /** + * Checks if a player is on cooldown. + */ + public boolean isOnCooldown(@NotNull UUID playerUuid, @NotNull String moduleName, @NotNull String commandName) { + if (CommandUtil.hasPermission(playerUuid, Permissions.BYPASS_COOLDOWN)) { + return false; + } + return cooldownTracker.isOnCooldown(playerUuid, moduleName, commandName); + } + + /** + * Gets remaining cooldown seconds. + */ + public int getRemainingCooldown(@NotNull UUID playerUuid, @NotNull String moduleName, @NotNull String commandName) { + return cooldownTracker.getRemainingCooldown(playerUuid, moduleName, commandName); + } + + public void clear() { + // Cancel all pending warmup futures + warmupFutures.values().forEach(f -> f.cancel(false)); + warmupFutures.clear(); + activeWarmups.clear(); + cooldownTracker.clear(); + } + + /** + * Shuts down the warmup scheduler. Call on plugin disable. + */ + public void shutdown() { + clear(); + scheduler.shutdown(); + } +} diff --git a/src/main/java/com/hyperessentials/module/warmup/WarmupModule.java b/src/main/java/com/hyperessentials/module/warmup/WarmupModule.java index 418795c..fc4b4f8 100644 --- a/src/main/java/com/hyperessentials/module/warmup/WarmupModule.java +++ b/src/main/java/com/hyperessentials/module/warmup/WarmupModule.java @@ -1,43 +1,43 @@ -package com.hyperessentials.module.warmup; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.config.ModuleConfig; -import com.hyperessentials.module.AbstractModule; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Warmup module for HyperEssentials. - */ -public class WarmupModule extends AbstractModule { - - @Override - @NotNull - public String getName() { - return "warmup"; - } - - @Override - @NotNull - public String getDisplayName() { - return "Warmup"; - } - - @Override - public void onEnable() { - super.onEnable(); - // TODO: Register commands, listeners, and storage - } - - @Override - public void onDisable() { - // TODO: Unregister commands, save data, cleanup - super.onDisable(); - } - - @Override - @Nullable - public ModuleConfig getModuleConfig() { - return ConfigManager.get().warmup(); - } -} +package com.hyperessentials.module.warmup; + +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.ModuleConfig; +import com.hyperessentials.module.AbstractModule; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Warmup module for HyperEssentials. + */ +public class WarmupModule extends AbstractModule { + + @Override + @NotNull + public String getName() { + return "warmup"; + } + + @Override + @NotNull + public String getDisplayName() { + return "Warmup"; + } + + @Override + public void onEnable() { + super.onEnable(); + // TODO: Register commands, listeners, and storage + } + + @Override + public void onDisable() { + // TODO: Unregister commands, save data, cleanup + super.onDisable(); + } + + @Override + @Nullable + public ModuleConfig getModuleConfig() { + return ConfigManager.get().warmup(); + } +} diff --git a/src/main/java/com/hyperessentials/module/warmup/WarmupTask.java b/src/main/java/com/hyperessentials/module/warmup/WarmupTask.java index fb9faac..290610d 100644 --- a/src/main/java/com/hyperessentials/module/warmup/WarmupTask.java +++ b/src/main/java/com/hyperessentials/module/warmup/WarmupTask.java @@ -1,16 +1,16 @@ -package com.hyperessentials.module.warmup; - -import org.jetbrains.annotations.NotNull; - -import java.util.UUID; - -/** - * Represents an active warmup for a player. - */ -public record WarmupTask( - @NotNull UUID playerUuid, - @NotNull String moduleName, - @NotNull String commandName, - int warmupSeconds, - @NotNull Runnable callback -) {} +package com.hyperessentials.module.warmup; + +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Represents an active warmup for a player. + */ +public record WarmupTask( + @NotNull UUID playerUuid, + @NotNull String moduleName, + @NotNull String commandName, + int warmupSeconds, + @NotNull Runnable callback +) {} diff --git a/src/main/java/com/hyperessentials/module/warps/WarpManager.java b/src/main/java/com/hyperessentials/module/warps/WarpManager.java new file mode 100644 index 0000000..65bc108 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/warps/WarpManager.java @@ -0,0 +1,124 @@ +package com.hyperessentials.module.warps; + +import com.hyperessentials.data.Warp; +import com.hyperessentials.integration.PermissionManager; +import com.hyperessentials.storage.WarpStorage; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Manages server warps - loading, saving, and CRUD operations. + */ +public class WarpManager { + + private final WarpStorage storage; + private final Map warps; + + public WarpManager(@NotNull WarpStorage storage) { + this.storage = storage; + this.warps = new ConcurrentHashMap<>(); + } + + public CompletableFuture loadWarps() { + return storage.loadWarps().thenAccept(loaded -> { + warps.clear(); + warps.putAll(loaded); + Logger.info("[Warps] Loaded %d warps", warps.size()); + }); + } + + public CompletableFuture saveWarps() { + return storage.saveWarps(new ConcurrentHashMap<>(warps)); + } + + public boolean setWarp(@NotNull Warp warp) { + boolean isNew = !warps.containsKey(warp.name()); + warps.put(warp.name(), warp); + saveWarps(); + Logger.info("[Warps] Warp '%s' %s", warp.name(), isNew ? "created" : "updated"); + return isNew; + } + + @Nullable + public Warp getWarp(@NotNull String name) { + return warps.get(name.toLowerCase()); + } + + public boolean deleteWarp(@NotNull String name) { + Warp removed = warps.remove(name.toLowerCase()); + if (removed != null) { + saveWarps(); + Logger.info("[Warps] Warp '%s' deleted", name); + return true; + } + return false; + } + + @NotNull + public Collection getAllWarps() { + return Collections.unmodifiableCollection(warps.values()); + } + + @NotNull + public List getAccessibleWarps(@NotNull UUID playerUuid) { + return warps.values().stream() + .filter(warp -> canAccess(playerUuid, warp)) + .collect(Collectors.toList()); + } + + @NotNull + public List getWarpsByCategory(@NotNull String category) { + return warps.values().stream() + .filter(warp -> warp.category().equalsIgnoreCase(category)) + .collect(Collectors.toList()); + } + + @NotNull + public List getAccessibleWarpsByCategory(@NotNull UUID playerUuid, @NotNull String category) { + return warps.values().stream() + .filter(warp -> warp.category().equalsIgnoreCase(category)) + .filter(warp -> canAccess(playerUuid, warp)) + .collect(Collectors.toList()); + } + + @NotNull + public Set getCategories() { + return warps.values().stream() + .map(Warp::category) + .collect(Collectors.toCollection(HashSet::new)); + } + + public boolean canAccess(@NotNull UUID playerUuid, @NotNull Warp warp) { + if (!warp.requiresPermission()) { + return true; + } + return PermissionManager.get().hasPermission(playerUuid, warp.permission()); + } + + public boolean warpExists(@NotNull String name) { + return warps.containsKey(name.toLowerCase()); + } + + @NotNull + public List getWarpNames() { + return new ArrayList<>(warps.keySet()); + } + + @NotNull + public List getAccessibleWarpNames(@NotNull UUID playerUuid) { + return warps.values().stream() + .filter(warp -> canAccess(playerUuid, warp)) + .map(Warp::name) + .collect(Collectors.toList()); + } + + public int getWarpCount() { + return warps.size(); + } +} diff --git a/src/main/java/com/hyperessentials/module/warps/WarpsModule.java b/src/main/java/com/hyperessentials/module/warps/WarpsModule.java index 91732c7..9b488d6 100644 --- a/src/main/java/com/hyperessentials/module/warps/WarpsModule.java +++ b/src/main/java/com/hyperessentials/module/warps/WarpsModule.java @@ -1,43 +1,65 @@ -package com.hyperessentials.module.warps; - -import com.hyperessentials.config.ConfigManager; -import com.hyperessentials.config.ModuleConfig; -import com.hyperessentials.module.AbstractModule; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Warps module for HyperEssentials. - */ -public class WarpsModule extends AbstractModule { - - @Override - @NotNull - public String getName() { - return "warps"; - } - - @Override - @NotNull - public String getDisplayName() { - return "Warps"; - } - - @Override - public void onEnable() { - super.onEnable(); - // TODO: Register commands, listeners, and storage - } - - @Override - public void onDisable() { - // TODO: Unregister commands, save data, cleanup - super.onDisable(); - } - - @Override - @Nullable - public ModuleConfig getModuleConfig() { - return ConfigManager.get().warps(); - } -} +package com.hyperessentials.module.warps; + +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.ModuleConfig; +import com.hyperessentials.module.AbstractModule; +import com.hyperessentials.storage.WarpStorage; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Warps module for HyperEssentials. + */ +public class WarpsModule extends AbstractModule { + + private WarpManager warpManager; + + @Override + @NotNull + public String getName() { + return "warps"; + } + + @Override + @NotNull + public String getDisplayName() { + return "Warps"; + } + + @Override + public void onEnable() { + super.onEnable(); + // Note: WarpStorage and WarpManager are initialized here but commands + // are registered at the platform layer (HyperEssentialsPlugin) + } + + /** + * Initializes the warp manager with the given storage. + * Called by HyperEssentialsPlugin after module is enabled. + */ + public void initManager(@NotNull WarpStorage storage) { + this.warpManager = new WarpManager(storage); + warpManager.loadWarps().join(); + Logger.info("[Warps] WarpManager initialized"); + } + + @Override + public void onDisable() { + if (warpManager != null) { + warpManager.saveWarps().join(); + } + super.onDisable(); + } + + @Nullable + public WarpManager getWarpManager() { + return warpManager; + } + + @Override + @Nullable + public ModuleConfig getModuleConfig() { + return ConfigManager.get().warps(); + } +} diff --git a/src/main/java/com/hyperessentials/module/warps/command/DelWarpCommand.java b/src/main/java/com/hyperessentials/module/warps/command/DelWarpCommand.java new file mode 100644 index 0000000..73073f6 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/warps/command/DelWarpCommand.java @@ -0,0 +1,61 @@ +package com.hyperessentials.module.warps.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.module.warps.WarpManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /delwarp <name> - Delete a server warp. + */ +public class DelWarpCommand extends AbstractPlayerCommand { + + private final WarpManager warpManager; + + public DelWarpCommand(@NotNull WarpManager warpManager) { + super("delwarp", "Delete a server warp"); + this.warpManager = warpManager; + addAliases("deletewarp", "rmwarp", "removewarp"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.WARP_DELETE)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to delete warps.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /delwarp ")); + return; + } + + String warpName = parts[1].toLowerCase(); + + if (warpManager.deleteWarp(warpName)) { + ctx.sendMessage(CommandUtil.success("Warp '" + warpName + "' has been deleted.")); + } else { + ctx.sendMessage(CommandUtil.error("Warp '" + warpName + "' not found.")); + } + } +} diff --git a/src/main/java/com/hyperessentials/module/warps/command/SetWarpCommand.java b/src/main/java/com/hyperessentials/module/warps/command/SetWarpCommand.java new file mode 100644 index 0000000..2382f0c --- /dev/null +++ b/src/main/java/com/hyperessentials/module/warps/command/SetWarpCommand.java @@ -0,0 +1,113 @@ +package com.hyperessentials.module.warps.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.config.modules.WarpsConfig; +import com.hyperessentials.data.Warp; +import com.hyperessentials.module.warps.WarpManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /setwarp <name> [category] - Create or update a warp at your location. + */ +public class SetWarpCommand extends AbstractPlayerCommand { + + private final WarpManager warpManager; + private final WarpsConfig config; + + public SetWarpCommand(@NotNull WarpManager warpManager, @NotNull WarpsConfig config) { + super("setwarp", "Create a server warp at your location"); + this.warpManager = warpManager; + this.config = config; + addAliases("createwarp"); + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.WARP_SET)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to create warps.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /setwarp [category]")); + return; + } + + String warpName = parts[1].toLowerCase(); + String category = parts.length > 2 ? parts[2] : config.getDefaultCategory(); + + if (warpName.length() < 1 || warpName.length() > 32) { + ctx.sendMessage(CommandUtil.error("Warp name must be 1-32 characters.")); + return; + } + + if (!warpName.matches("[a-z0-9_-]+")) { + ctx.sendMessage(CommandUtil.error("Warp name can only contain letters, numbers, underscore, and dash.")); + return; + } + + TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType()); + if (transform == null) { + ctx.sendMessage(CommandUtil.error("Could not get your position.")); + return; + } + + Vector3d pos = transform.getPosition(); + Vector3f rot = transform.getRotation(); + + boolean isUpdate = warpManager.warpExists(warpName); + + Warp warp; + if (isUpdate) { + Warp existing = warpManager.getWarp(warpName); + warp = existing.withLocation( + currentWorld.getName(), + pos.getX(), pos.getY(), pos.getZ(), + rot.getY(), rot.getX() + ); + if (parts.length > 2) { + warp = warp.withCategory(category); + } + } else { + warp = Warp.create( + warpName, + currentWorld.getName(), + pos.getX(), pos.getY(), pos.getZ(), + rot.getY(), rot.getX(), + uuid.toString() + ); + warp = warp.withCategory(category); + } + + warpManager.setWarp(warp); + + ctx.sendMessage(CommandUtil.success("Warp '" + warpName + "' has been set!")); + ctx.sendMessage(CommandUtil.info(String.format("Location: %.0f, %.0f, %.0f in %s", + pos.getX(), pos.getY(), pos.getZ(), currentWorld.getName()))); + ctx.sendMessage(CommandUtil.info("Category: " + category)); + } +} diff --git a/src/main/java/com/hyperessentials/module/warps/command/WarpCommand.java b/src/main/java/com/hyperessentials/module/warps/command/WarpCommand.java new file mode 100644 index 0000000..7dac2f9 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/warps/command/WarpCommand.java @@ -0,0 +1,128 @@ +package com.hyperessentials.module.warps.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.Location; +import com.hyperessentials.data.Warp; +import com.hyperessentials.module.warps.WarpManager; +import com.hyperessentials.module.warmup.WarmupManager; +import com.hyperessentials.module.warmup.WarmupTask; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3f; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.UUID; + +/** + * /warp <name> - Teleport to a server warp. + */ +public class WarpCommand extends AbstractPlayerCommand { + + private final WarpManager warpManager; + private final WarmupManager warmupManager; + + public WarpCommand(@NotNull WarpManager warpManager, @NotNull WarmupManager warmupManager) { + super("warp", "Teleport to a server warp"); + this.warpManager = warpManager; + this.warmupManager = warmupManager; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.WARP)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to use warps.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + Collection warps = warpManager.getAccessibleWarps(uuid); + if (warps.isEmpty()) { + ctx.sendMessage(CommandUtil.info("No warps available.")); + } else { + ctx.sendMessage(CommandUtil.info("Available warps:")); + StringBuilder sb = new StringBuilder(); + for (Warp warp : warps) { + if (!sb.isEmpty()) sb.append(", "); + sb.append(warp.name()); + } + ctx.sendMessage(CommandUtil.msg(sb.toString(), CommandUtil.COLOR_GRAY)); + ctx.sendMessage(CommandUtil.msg("Use /warp to teleport.", CommandUtil.COLOR_GRAY)); + } + return; + } + + String warpName = parts[1].toLowerCase(); + + Warp warp = warpManager.getWarp(warpName); + if (warp == null) { + ctx.sendMessage(CommandUtil.error("Warp '" + warpName + "' not found.")); + return; + } + + if (!warpManager.canAccess(uuid, warp)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to use this warp.")); + return; + } + + // Check cooldown + if (warmupManager.isOnCooldown(uuid, "warps", "warp")) { + int remaining = warmupManager.getRemainingCooldown(uuid, "warps", "warp"); + ctx.sendMessage(CommandUtil.error("On cooldown. " + remaining + "s remaining.")); + return; + } + + Location destination = Location.fromWarp(warp); + + WarmupTask task = warmupManager.startWarmup(uuid, "warps", "warp", () -> { + executeTeleport(ref, destination, () -> { + ctx.sendMessage(CommandUtil.success("Teleported to warp '" + warpName + "'!")); + }); + }); + + if (task != null) { + ctx.sendMessage(CommandUtil.info("Teleporting in " + task.warmupSeconds() + "s... Don't move!")); + } + } + + private void executeTeleport(Ref ref, Location dest, Runnable onComplete) { + World targetWorld = Universe.get().getWorld(dest.world()); + if (targetWorld == null) { + return; + } + targetWorld.execute(() -> { + if (!ref.isValid()) { + return; + } + Store store = ref.getStore(); + Vector3d position = new Vector3d(dest.x(), dest.y(), dest.z()); + Vector3f rotation = new Vector3f(dest.pitch(), dest.yaw(), 0); + Teleport teleport = Teleport.createForPlayer(targetWorld, position, rotation); + store.addComponent(ref, Teleport.getComponentType(), teleport); + if (onComplete != null) { + onComplete.run(); + } + }); + } +} diff --git a/src/main/java/com/hyperessentials/module/warps/command/WarpInfoCommand.java b/src/main/java/com/hyperessentials/module/warps/command/WarpInfoCommand.java new file mode 100644 index 0000000..524a462 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/warps/command/WarpInfoCommand.java @@ -0,0 +1,81 @@ +package com.hyperessentials.module.warps.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.Warp; +import com.hyperessentials.module.warps.WarpManager; +import com.hyperessentials.util.TimeUtil; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * /warpinfo <name> - Display detailed information about a warp. + */ +public class WarpInfoCommand extends AbstractPlayerCommand { + + private final WarpManager warpManager; + + public WarpInfoCommand(@NotNull WarpManager warpManager) { + super("warpinfo", "Display warp information"); + this.warpManager = warpManager; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.WARP_INFO)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to view warp info.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + + if (parts.length < 2) { + ctx.sendMessage(CommandUtil.error("Usage: /warpinfo ")); + return; + } + + String warpName = parts[1].toLowerCase(); + + Warp warp = warpManager.getWarp(warpName); + if (warp == null) { + ctx.sendMessage(CommandUtil.error("Warp '" + warpName + "' not found.")); + return; + } + + ctx.sendMessage(CommandUtil.msg("--- Warp: " + warp.displayName() + " ---", CommandUtil.COLOR_GOLD)); + ctx.sendMessage(CommandUtil.msg("Name: " + warp.name(), CommandUtil.COLOR_GRAY)); + ctx.sendMessage(CommandUtil.msg("Category: " + warp.category(), CommandUtil.COLOR_GRAY)); + ctx.sendMessage(CommandUtil.msg("World: " + warp.world(), CommandUtil.COLOR_GRAY)); + ctx.sendMessage(CommandUtil.msg(String.format("Location: %.1f, %.1f, %.1f", + warp.x(), warp.y(), warp.z()), CommandUtil.COLOR_GRAY)); + + if (warp.description() != null && !warp.description().isEmpty()) { + ctx.sendMessage(CommandUtil.msg("Description: " + warp.description(), CommandUtil.COLOR_GRAY)); + } + + if (warp.permission() != null && !warp.permission().isEmpty()) { + ctx.sendMessage(CommandUtil.msg("Permission: " + warp.permission(), CommandUtil.COLOR_GRAY)); + boolean hasAccess = warpManager.canAccess(uuid, warp); + ctx.sendMessage(CommandUtil.msg("You have access: " + (hasAccess ? "Yes" : "No"), CommandUtil.COLOR_GRAY)); + } + + ctx.sendMessage(CommandUtil.msg("Created: " + TimeUtil.formatRelativeTime(warp.createdAt()), CommandUtil.COLOR_GRAY)); + } +} diff --git a/src/main/java/com/hyperessentials/module/warps/command/WarpsCommand.java b/src/main/java/com/hyperessentials/module/warps/command/WarpsCommand.java new file mode 100644 index 0000000..5728487 --- /dev/null +++ b/src/main/java/com/hyperessentials/module/warps/command/WarpsCommand.java @@ -0,0 +1,123 @@ +package com.hyperessentials.module.warps.command; + +import com.hyperessentials.Permissions; +import com.hyperessentials.api.HyperEssentialsAPI; +import com.hyperessentials.command.util.CommandUtil; +import com.hyperessentials.data.Warp; +import com.hyperessentials.gui.GuiManager; +import com.hyperessentials.module.warps.WarpManager; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * /warps [category] - List all warps or warps in a category. + * Opens GUI page when available (no category filter), falls back to text list. + */ +public class WarpsCommand extends AbstractPlayerCommand { + + private final WarpManager warpManager; + + public WarpsCommand(@NotNull WarpManager warpManager) { + super("warps", "List server warps"); + this.warpManager = warpManager; + setAllowsExtraArguments(true); + } + + @Override + protected void execute(@NotNull CommandContext ctx, + @NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef, + @NotNull World currentWorld) { + + UUID uuid = playerRef.getUuid(); + + if (!CommandUtil.hasPermission(uuid, Permissions.WARP_LIST)) { + ctx.sendMessage(CommandUtil.error("You don't have permission to list warps.")); + return; + } + + String input = ctx.getInputString(); + String[] parts = input != null ? input.trim().split("\\s+") : new String[0]; + String category = parts.length > 1 ? parts[1].toLowerCase() : null; + + // Try GUI page for unfiltered /warps + if (category == null && tryOpenGui(store, ref, playerRef)) { + return; + } + + // Text fallback + List warps; + if (category != null) { + warps = warpManager.getAccessibleWarpsByCategory(uuid, category); + if (warps.isEmpty()) { + ctx.sendMessage(CommandUtil.info("No warps found in category '" + category + "'.")); + return; + } + ctx.sendMessage(CommandUtil.msg("--- Warps in '" + category + "' ---", CommandUtil.COLOR_GOLD)); + } else { + warps = warpManager.getAccessibleWarps(uuid); + if (warps.isEmpty()) { + ctx.sendMessage(CommandUtil.info("No warps available.")); + return; + } + ctx.sendMessage(CommandUtil.msg("--- Server Warps ---", CommandUtil.COLOR_GOLD)); + } + + Map> grouped = warps.stream() + .collect(Collectors.groupingBy(Warp::category)); + + for (Map.Entry> entry : grouped.entrySet()) { + String cat = entry.getKey(); + List catWarps = entry.getValue(); + + if (category == null) { + ctx.sendMessage(CommandUtil.msg("[" + cat + "]", CommandUtil.COLOR_GRAY)); + } + + StringBuilder sb = new StringBuilder(); + for (Warp warp : catWarps) { + if (!sb.isEmpty()) sb.append(", "); + sb.append(warp.displayName()); + } + ctx.sendMessage(CommandUtil.msg(" " + sb, CommandUtil.COLOR_WHITE)); + } + + if (category == null) { + Set categories = warpManager.getCategories(); + if (categories.size() > 1) { + ctx.sendMessage(CommandUtil.msg("Categories: " + String.join(", ", categories), CommandUtil.COLOR_GRAY)); + ctx.sendMessage(CommandUtil.msg("Use /warps to filter.", CommandUtil.COLOR_GRAY)); + } + } + + ctx.sendMessage(CommandUtil.msg("Use /warp to teleport.", CommandUtil.COLOR_GRAY)); + } + + private boolean tryOpenGui(@NotNull Store store, + @NotNull Ref ref, + @NotNull PlayerRef playerRef) { + if (!HyperEssentialsAPI.isAvailable()) return false; + + GuiManager guiManager = HyperEssentialsAPI.getInstance().getGuiManager(); + if (guiManager.getPlayerRegistry().getEntry("warps") == null) return false; + + Player player = store.getComponent(ref, Player.getComponentType()); + if (player == null) return false; + + return guiManager.openPlayerPage("warps", player, ref, store, playerRef); + } +} diff --git a/src/main/java/com/hyperessentials/platform/HyperEssentialsPlugin.java b/src/main/java/com/hyperessentials/platform/HyperEssentialsPlugin.java index ce5918d..8f55c21 100644 --- a/src/main/java/com/hyperessentials/platform/HyperEssentialsPlugin.java +++ b/src/main/java/com/hyperessentials/platform/HyperEssentialsPlugin.java @@ -1,122 +1,299 @@ -package com.hyperessentials.platform; - -import com.hyperessentials.BuildInfo; -import com.hyperessentials.HyperEssentials; -import com.hyperessentials.api.HyperEssentialsAPI; -import com.hyperessentials.command.AdminCommand; -import com.hyperessentials.util.Logger; -import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; -import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent; -import com.hypixel.hytale.server.core.plugin.JavaPlugin; -import com.hypixel.hytale.server.core.plugin.JavaPluginInit; -import com.hypixel.hytale.server.core.universe.PlayerRef; - -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Level; - -/** - * Main Hytale plugin class for HyperEssentials. - */ -public class HyperEssentialsPlugin extends JavaPlugin { - - private static HyperEssentialsPlugin instance; - - public static HyperEssentialsPlugin getInstance() { - return instance; - } - - private HyperEssentials hyperEssentials; - private final Map trackedPlayers = new ConcurrentHashMap<>(); - - public HyperEssentialsPlugin(JavaPluginInit init) { - super(init); - } - - @Override - protected void setup() { - instance = this; - - // Initialize core - hyperEssentials = new HyperEssentials(getDataDirectory(), java.util.logging.Logger.getLogger("HyperEssentials")); - - // Set API instance - HyperEssentialsAPI.setInstance(hyperEssentials); - - getLogger().at(Level.INFO).log("HyperEssentials v%s loading...", BuildInfo.VERSION); - } - - @Override - protected void start() { - // Enable core (loads config, integrations, modules) - hyperEssentials.enable(); - - // Register commands - registerCommands(); - - // Register event listeners - registerEventListeners(); - - getLogger().at(Level.INFO).log("HyperEssentials v%s enabled!", BuildInfo.VERSION); - } - - @Override - protected void shutdown() { - // Clear instances - instance = null; - HyperEssentialsAPI.setInstance(null); - - // Disable core - if (hyperEssentials != null) { - hyperEssentials.disable(); - } - - // Clear tracked players - trackedPlayers.clear(); - - getLogger().at(Level.INFO).log("HyperEssentials disabled"); - } - - private void registerCommands() { - try { - getCommandRegistry().registerCommand(new AdminCommand()); - getLogger().at(Level.INFO).log("Registered commands: /hessentials"); - } catch (Exception e) { - getLogger().at(Level.SEVERE).withCause(e).log("Failed to register commands"); - } - } - - private void registerEventListeners() { - getEventRegistry().register(PlayerConnectEvent.class, this::onPlayerConnect); - getEventRegistry().register(PlayerDisconnectEvent.class, this::onPlayerDisconnect); - getLogger().at(Level.INFO).log("Registered event listeners"); - } - - private void onPlayerConnect(PlayerConnectEvent event) { - PlayerRef playerRef = event.getPlayerRef(); - trackedPlayers.put(playerRef.getUuid(), playerRef); - Logger.debug("Player connected: %s", playerRef.getUsername()); - } - - private void onPlayerDisconnect(PlayerDisconnectEvent event) { - PlayerRef playerRef = event.getPlayerRef(); - trackedPlayers.remove(playerRef.getUuid()); - - // Cancel any active warmups - hyperEssentials.getWarmupManager().cancelWarmup(playerRef.getUuid()); - - // Unregister from page tracker - hyperEssentials.getGuiManager().getPageTracker().unregister(playerRef.getUuid()); - - Logger.debug("Player disconnected: %s", playerRef.getUsername()); - } - - public PlayerRef getTrackedPlayer(UUID uuid) { - return trackedPlayers.get(uuid); - } - - public HyperEssentials getHyperEssentials() { - return hyperEssentials; - } -} +package com.hyperessentials.platform; + +import com.hyperessentials.BuildInfo; +import com.hyperessentials.HyperEssentials; +import com.hyperessentials.api.HyperEssentialsAPI; +import com.hyperessentials.command.AdminCommand; +import com.hyperessentials.config.ConfigManager; +import com.hyperessentials.config.modules.SpawnsConfig; +import com.hyperessentials.config.modules.TeleportConfig; +import com.hyperessentials.config.modules.WarpsConfig; +import com.hyperessentials.listener.DeathListener; +import com.hyperessentials.module.homes.HomeManager; +import com.hyperessentials.module.homes.HomesModule; +import com.hyperessentials.module.homes.command.DelHomeCommand; +import com.hyperessentials.module.homes.command.HomeCommand; +import com.hyperessentials.module.homes.command.HomesCommand; +import com.hyperessentials.module.homes.command.SetHomeCommand; +import com.hyperessentials.module.teleport.RtpManager; +import com.hyperessentials.module.teleport.command.RtpCommand; +import com.hyperessentials.module.spawns.SpawnManager; +import com.hyperessentials.module.spawns.SpawnsModule; +import com.hyperessentials.module.spawns.command.*; +import com.hyperessentials.module.teleport.BackManager; +import com.hyperessentials.module.teleport.TeleportModule; +import com.hyperessentials.module.teleport.TpaManager; +import com.hyperessentials.module.teleport.command.*; +import com.hyperessentials.module.warps.WarpManager; +import com.hyperessentials.module.warps.WarpsModule; +import com.hyperessentials.module.warps.command.*; +import com.hyperessentials.util.Logger; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent; +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; + +/** + * Main Hytale plugin class for HyperEssentials. + */ +public class HyperEssentialsPlugin extends JavaPlugin { + + private static HyperEssentialsPlugin instance; + + public static HyperEssentialsPlugin getInstance() { + return instance; + } + + private HyperEssentials hyperEssentials; + private DeathListener deathListener; + private final Map trackedPlayers = new ConcurrentHashMap<>(); + + public HyperEssentialsPlugin(JavaPluginInit init) { + super(init); + } + + @Override + protected void setup() { + instance = this; + + // Initialize core + hyperEssentials = new HyperEssentials(getDataDirectory(), getLogger()); + + // Set API instance + HyperEssentialsAPI.setInstance(hyperEssentials); + + getLogger().at(Level.INFO).log("HyperEssentials v%s loading...", BuildInfo.VERSION); + } + + @Override + protected void start() { + // Enable core (loads config, integrations, modules) + hyperEssentials.enable(); + + // Initialize death listener + deathListener = new DeathListener(hyperEssentials); + + // Register commands + registerCommands(); + + // Register event listeners + registerEventListeners(); + + getLogger().at(Level.INFO).log("HyperEssentials v%s enabled!", BuildInfo.VERSION); + } + + @Override + protected void shutdown() { + // Clear instances + instance = null; + HyperEssentialsAPI.setInstance(null); + + // Disable core + if (hyperEssentials != null) { + hyperEssentials.disable(); + } + + // Clear tracked players + trackedPlayers.clear(); + + getLogger().at(Level.INFO).log("HyperEssentials disabled"); + } + + private void registerCommands() { + List registered = new ArrayList<>(); + + try { + // Admin + getCommandRegistry().registerCommand(new AdminCommand()); + registered.add("/hessentials"); + + // Homes + HomesModule homesModule = hyperEssentials.getHomesModule(); + if (homesModule != null && homesModule.isEnabled() && homesModule.getHomeManager() != null) { + HomeManager hm = homesModule.getHomeManager(); + getCommandRegistry().registerCommand(new SetHomeCommand(hm)); + getCommandRegistry().registerCommand(new HomeCommand(hm, hyperEssentials.getWarmupManager())); + getCommandRegistry().registerCommand(new DelHomeCommand(hm)); + getCommandRegistry().registerCommand(new HomesCommand(hm)); + registered.add("/sethome"); + registered.add("/home"); + registered.add("/delhome"); + registered.add("/homes"); + } + + // Warps + WarpsModule warps = hyperEssentials.getWarpsModule(); + if (warps != null && warps.isEnabled() && warps.getWarpManager() != null) { + WarpManager wm = warps.getWarpManager(); + WarpsConfig warpsConfig = ConfigManager.get().warps(); + getCommandRegistry().registerCommand(new WarpCommand(wm, hyperEssentials.getWarmupManager())); + getCommandRegistry().registerCommand(new WarpsCommand(wm)); + getCommandRegistry().registerCommand(new SetWarpCommand(wm, warpsConfig)); + getCommandRegistry().registerCommand(new DelWarpCommand(wm)); + getCommandRegistry().registerCommand(new WarpInfoCommand(wm)); + registered.add("/warp"); + registered.add("/warps"); + registered.add("/setwarp"); + registered.add("/delwarp"); + registered.add("/warpinfo"); + } + + // Spawns + SpawnsModule spawns = hyperEssentials.getSpawnsModule(); + if (spawns != null && spawns.isEnabled() && spawns.getSpawnManager() != null) { + SpawnManager sm = spawns.getSpawnManager(); + SpawnsConfig spawnsConfig = ConfigManager.get().spawns(); + getCommandRegistry().registerCommand(new SpawnCommand(sm, spawnsConfig, hyperEssentials.getWarmupManager())); + getCommandRegistry().registerCommand(new SpawnsCommand(sm)); + getCommandRegistry().registerCommand(new SetSpawnCommand(sm, spawnsConfig)); + getCommandRegistry().registerCommand(new DelSpawnCommand(sm)); + getCommandRegistry().registerCommand(new SpawnInfoCommand(sm)); + registered.add("/spawn"); + registered.add("/spawns"); + registered.add("/setspawn"); + registered.add("/delspawn"); + registered.add("/spawninfo"); + } + + // Teleport + TeleportModule teleport = hyperEssentials.getTeleportModule(); + if (teleport != null && teleport.isEnabled() && teleport.getTpaManager() != null) { + TpaManager tpa = teleport.getTpaManager(); + BackManager back = teleport.getBackManager(); + TeleportConfig teleportConfig = ConfigManager.get().teleport(); + getCommandRegistry().registerCommand(new TpaCommand(tpa, teleportConfig)); + getCommandRegistry().registerCommand(new TpaHereCommand(tpa, teleportConfig)); + getCommandRegistry().registerCommand(new TpAcceptCommand(tpa, back, hyperEssentials.getWarmupManager())); + getCommandRegistry().registerCommand(new TpDenyCommand(tpa)); + getCommandRegistry().registerCommand(new TpCancelCommand(tpa)); + getCommandRegistry().registerCommand(new TpToggleCommand(tpa)); + getCommandRegistry().registerCommand(new BackCommand(back, hyperEssentials.getWarmupManager())); + registered.add("/tpa"); + registered.add("/tpahere"); + registered.add("/tpaccept"); + registered.add("/tpdeny"); + registered.add("/tpcancel"); + registered.add("/tptoggle"); + registered.add("/back"); + + // RTP (part of Teleport module) + RtpManager rtpMgr = teleport.getRtpManager(); + if (rtpMgr != null) { + getCommandRegistry().registerCommand(new RtpCommand(rtpMgr, hyperEssentials.getWarmupManager())); + registered.add("/rtp"); + } + } + + getLogger().at(Level.INFO).log("Registered commands: %s", String.join(", ", registered)); + } catch (Exception e) { + getLogger().at(Level.SEVERE).withCause(e).log("Failed to register commands"); + } + } + + private void registerEventListeners() { + getEventRegistry().register(PlayerConnectEvent.class, this::onPlayerConnect); + getEventRegistry().register(PlayerDisconnectEvent.class, this::onPlayerDisconnect); + getLogger().at(Level.INFO).log("Registered event listeners"); + } + + private void onPlayerConnect(PlayerConnectEvent event) { + PlayerRef playerRef = event.getPlayerRef(); + trackedPlayers.put(playerRef.getUuid(), playerRef); + + // Notify connect handlers + hyperEssentials.onPlayerConnect(playerRef.getUuid(), playerRef.getUsername()); + + // Load home data + HomesModule homesModule = hyperEssentials.getHomesModule(); + if (homesModule != null && homesModule.isEnabled() && homesModule.getHomeManager() != null) { + homesModule.getHomeManager().loadPlayer(playerRef.getUuid(), playerRef.getUsername()); + } + + // Load teleport data + TeleportModule tm = hyperEssentials.getTeleportModule(); + if (tm != null && tm.isEnabled() && tm.getTpaManager() != null) { + tm.getTpaManager().loadPlayer(playerRef.getUuid(), playerRef.getUsername()); + } + + Logger.debug("Player connected: %s", playerRef.getUsername()); + } + + private void onPlayerDisconnect(PlayerDisconnectEvent event) { + PlayerRef playerRef = event.getPlayerRef(); + UUID uuid = playerRef.getUuid(); + + // Cancel any active warmups + hyperEssentials.getWarmupManager().cancelWarmup(uuid); + + // Unregister from page tracker + hyperEssentials.getGuiManager().getPageTracker().unregister(uuid); + + // Notify modules of disconnect for cleanup + hyperEssentials.onPlayerDisconnect(uuid); + + // Unload home data + HomesModule hmModule = hyperEssentials.getHomesModule(); + if (hmModule != null && hmModule.isEnabled() && hmModule.getHomeManager() != null) { + hmModule.getHomeManager().unloadPlayer(uuid); + } + + // Remove from tracked players last + trackedPlayers.remove(uuid); + + // Unload teleport data + TeleportModule tm = hyperEssentials.getTeleportModule(); + if (tm != null && tm.isEnabled() && tm.getTpaManager() != null) { + tm.getTpaManager().unloadPlayer(playerRef.getUuid()); + } + + Logger.debug("Player disconnected: %s", playerRef.getUsername()); + } + + @Nullable + public PlayerRef getTrackedPlayer(UUID uuid) { + return trackedPlayers.get(uuid); + } + + /** + * Returns an unmodifiable view of all tracked (online) players. + */ + @NotNull + public Map getTrackedPlayers() { + return Collections.unmodifiableMap(trackedPlayers); + } + + /** + * Finds an online player by name (case-insensitive). + * + * @param name the player name to search for + * @return the PlayerRef if found online, null otherwise + */ + @Nullable + public PlayerRef findOnlinePlayer(@NotNull String name) { + for (PlayerRef ref : trackedPlayers.values()) { + if (ref.getUsername().equalsIgnoreCase(name)) { + return ref; + } + } + return null; + } + + public DeathListener getDeathListener() { + return deathListener; + } + + public HyperEssentials getHyperEssentials() { + return hyperEssentials; + } +} diff --git a/src/main/java/com/hyperessentials/storage/HomeStorage.java b/src/main/java/com/hyperessentials/storage/HomeStorage.java index 8cc2c70..c61a3c2 100644 --- a/src/main/java/com/hyperessentials/storage/HomeStorage.java +++ b/src/main/java/com/hyperessentials/storage/HomeStorage.java @@ -1,15 +1,27 @@ -package com.hyperessentials.storage; - -import java.util.List; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; - -/** - * Storage interface for home data. - */ -public interface HomeStorage { - - // TODO: Define home CRUD operations when homes module is implemented - CompletableFuture init(); - CompletableFuture shutdown(); -} +package com.hyperessentials.storage; + +import com.hyperessentials.data.PlayerHomes; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Storage interface for home data. + */ +public interface HomeStorage { + + CompletableFuture init(); + CompletableFuture shutdown(); + + /** + * Loads a player's homes from storage. + */ + CompletableFuture> loadPlayerHomes(@NotNull UUID uuid); + + /** + * Saves a player's homes to storage. + */ + CompletableFuture savePlayerHomes(@NotNull PlayerHomes playerHomes); +} diff --git a/src/main/java/com/hyperessentials/storage/PlayerDataStorage.java b/src/main/java/com/hyperessentials/storage/PlayerDataStorage.java index 9849d47..f450182 100644 --- a/src/main/java/com/hyperessentials/storage/PlayerDataStorage.java +++ b/src/main/java/com/hyperessentials/storage/PlayerDataStorage.java @@ -1,12 +1,20 @@ -package com.hyperessentials.storage; - -import java.util.concurrent.CompletableFuture; - -/** - * Storage interface for per-player data (TPA toggle, back history, etc.). - */ -public interface PlayerDataStorage { - - CompletableFuture init(); - CompletableFuture shutdown(); -} +package com.hyperessentials.storage; + +import com.hyperessentials.data.PlayerTeleportData; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Storage interface for per-player data (TPA toggle, back history, etc.). + */ +public interface PlayerDataStorage { + + CompletableFuture init(); + CompletableFuture shutdown(); + CompletableFuture> loadPlayerData(@NotNull UUID uuid); + CompletableFuture savePlayerData(@NotNull PlayerTeleportData data); + CompletableFuture deletePlayerData(@NotNull UUID uuid); +} diff --git a/src/main/java/com/hyperessentials/storage/PlayerStatsStorage.java b/src/main/java/com/hyperessentials/storage/PlayerStatsStorage.java new file mode 100644 index 0000000..dc7cd9f --- /dev/null +++ b/src/main/java/com/hyperessentials/storage/PlayerStatsStorage.java @@ -0,0 +1,104 @@ +package com.hyperessentials.storage; + +import com.google.gson.*; +import com.hyperessentials.data.PlayerStats; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * JSON persistence for player statistics (playtime, first join). + * File: data/playerstats.json + */ +public class PlayerStatsStorage { + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + + private final Path filePath; + private final Map stats = new ConcurrentHashMap<>(); + + public PlayerStatsStorage(@NotNull Path dataDir) { + this.filePath = dataDir.resolve("data").resolve("playerstats.json"); + } + + public void load() { + stats.clear(); + + if (!Files.exists(filePath)) { + Logger.info("[PlayerStatsStorage] No stats file found, starting fresh"); + return; + } + + try { + String json = Files.readString(filePath); + JsonObject root = JsonParser.parseString(json).getAsJsonObject(); + + if (root.has("players") && root.get("players").isJsonObject()) { + JsonObject players = root.getAsJsonObject("players"); + for (Map.Entry entry : players.entrySet()) { + try { + UUID uuid = UUID.fromString(entry.getKey()); + JsonObject obj = entry.getValue().getAsJsonObject(); + PlayerStats ps = new PlayerStats( + uuid, + obj.get("username").getAsString(), + Instant.ofEpochMilli(obj.get("firstJoin").getAsLong()), + obj.get("totalPlaytimeMs").getAsLong(), + Instant.ofEpochMilli(obj.get("lastJoin").getAsLong()) + ); + stats.put(uuid, ps); + } catch (Exception e) { + Logger.warn("[PlayerStatsStorage] Failed to parse entry: %s", e.getMessage()); + } + } + } + + Logger.info("[PlayerStatsStorage] Loaded stats for %d player(s)", stats.size()); + } catch (Exception e) { + Logger.severe("[PlayerStatsStorage] Failed to load: %s", e.getMessage()); + } + } + + public synchronized void save() { + try { + Files.createDirectories(filePath.getParent()); + + JsonObject root = new JsonObject(); + JsonObject players = new JsonObject(); + + for (Map.Entry entry : stats.entrySet()) { + PlayerStats ps = entry.getValue(); + JsonObject obj = new JsonObject(); + obj.addProperty("username", ps.username()); + obj.addProperty("firstJoin", ps.firstJoin().toEpochMilli()); + obj.addProperty("totalPlaytimeMs", ps.totalPlaytimeMs()); + obj.addProperty("lastJoin", ps.lastJoin().toEpochMilli()); + players.add(entry.getKey().toString(), obj); + } + + root.add("players", players); + Files.writeString(filePath, GSON.toJson(root)); + Logger.debug("[PlayerStatsStorage] Saved stats"); + } catch (IOException e) { + Logger.severe("[PlayerStatsStorage] Failed to save: %s", e.getMessage()); + } + } + + @Nullable + public PlayerStats getStats(@NotNull UUID uuid) { + return stats.get(uuid); + } + + public void updateStats(@NotNull PlayerStats playerStats) { + stats.put(playerStats.uuid(), playerStats); + save(); + } +} diff --git a/src/main/java/com/hyperessentials/storage/SpawnStorage.java b/src/main/java/com/hyperessentials/storage/SpawnStorage.java index 1b865c3..f5be8b4 100644 --- a/src/main/java/com/hyperessentials/storage/SpawnStorage.java +++ b/src/main/java/com/hyperessentials/storage/SpawnStorage.java @@ -1,12 +1,18 @@ -package com.hyperessentials.storage; - -import java.util.concurrent.CompletableFuture; - -/** - * Storage interface for spawn data. - */ -public interface SpawnStorage { - - CompletableFuture init(); - CompletableFuture shutdown(); -} +package com.hyperessentials.storage; + +import com.hyperessentials.data.Spawn; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Storage interface for spawn data. + */ +public interface SpawnStorage { + + CompletableFuture init(); + CompletableFuture shutdown(); + CompletableFuture> loadSpawns(); + CompletableFuture saveSpawns(@NotNull Map spawns); +} diff --git a/src/main/java/com/hyperessentials/storage/StorageProvider.java b/src/main/java/com/hyperessentials/storage/StorageProvider.java index fa03441..420204f 100644 --- a/src/main/java/com/hyperessentials/storage/StorageProvider.java +++ b/src/main/java/com/hyperessentials/storage/StorageProvider.java @@ -1,27 +1,27 @@ -package com.hyperessentials.storage; - -import org.jetbrains.annotations.NotNull; - -import java.util.concurrent.CompletableFuture; - -/** - * Top-level storage interface for HyperEssentials. - */ -public interface StorageProvider { - - CompletableFuture init(); - - CompletableFuture shutdown(); - - @NotNull - HomeStorage getHomeStorage(); - - @NotNull - WarpStorage getWarpStorage(); - - @NotNull - SpawnStorage getSpawnStorage(); - - @NotNull - PlayerDataStorage getPlayerDataStorage(); -} +package com.hyperessentials.storage; + +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +/** + * Top-level storage interface for HyperEssentials. + */ +public interface StorageProvider { + + CompletableFuture init(); + + CompletableFuture shutdown(); + + @NotNull + HomeStorage getHomeStorage(); + + @NotNull + WarpStorage getWarpStorage(); + + @NotNull + SpawnStorage getSpawnStorage(); + + @NotNull + PlayerDataStorage getPlayerDataStorage(); +} diff --git a/src/main/java/com/hyperessentials/storage/WarpStorage.java b/src/main/java/com/hyperessentials/storage/WarpStorage.java index 0ad2580..49ed363 100644 --- a/src/main/java/com/hyperessentials/storage/WarpStorage.java +++ b/src/main/java/com/hyperessentials/storage/WarpStorage.java @@ -1,12 +1,18 @@ -package com.hyperessentials.storage; - -import java.util.concurrent.CompletableFuture; - -/** - * Storage interface for warp data. - */ -public interface WarpStorage { - - CompletableFuture init(); - CompletableFuture shutdown(); -} +package com.hyperessentials.storage; + +import com.hyperessentials.data.Warp; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Storage interface for warp data. + */ +public interface WarpStorage { + + CompletableFuture init(); + CompletableFuture shutdown(); + CompletableFuture> loadWarps(); + CompletableFuture saveWarps(@NotNull Map warps); +} diff --git a/src/main/java/com/hyperessentials/storage/json/JsonStorageProvider.java b/src/main/java/com/hyperessentials/storage/json/JsonStorageProvider.java index 380875b..c4bd590 100644 --- a/src/main/java/com/hyperessentials/storage/json/JsonStorageProvider.java +++ b/src/main/java/com/hyperessentials/storage/json/JsonStorageProvider.java @@ -1,69 +1,545 @@ -package com.hyperessentials.storage.json; - -import com.hyperessentials.storage.*; -import com.hyperessentials.util.Logger; -import org.jetbrains.annotations.NotNull; - -import java.nio.file.Path; -import java.util.concurrent.CompletableFuture; - -/** - * JSON file-based storage provider. - */ -public class JsonStorageProvider implements StorageProvider { - - private final Path dataDir; - - public JsonStorageProvider(@NotNull Path dataDir) { - this.dataDir = dataDir; - } - - @Override - public CompletableFuture init() { - Logger.info("[Storage] JSON storage provider initialized"); - return CompletableFuture.completedFuture(null); - } - - @Override - public CompletableFuture shutdown() { - Logger.info("[Storage] JSON storage provider shut down"); - return CompletableFuture.completedFuture(null); - } - - @Override - @NotNull - public HomeStorage getHomeStorage() { - // TODO: Return actual implementation when homes module is built - return new HomeStorage() { - @Override public CompletableFuture init() { return CompletableFuture.completedFuture(null); } - @Override public CompletableFuture shutdown() { return CompletableFuture.completedFuture(null); } - }; - } - - @Override - @NotNull - public WarpStorage getWarpStorage() { - return new WarpStorage() { - @Override public CompletableFuture init() { return CompletableFuture.completedFuture(null); } - @Override public CompletableFuture shutdown() { return CompletableFuture.completedFuture(null); } - }; - } - - @Override - @NotNull - public SpawnStorage getSpawnStorage() { - return new SpawnStorage() { - @Override public CompletableFuture init() { return CompletableFuture.completedFuture(null); } - @Override public CompletableFuture shutdown() { return CompletableFuture.completedFuture(null); } - }; - } - - @Override - @NotNull - public PlayerDataStorage getPlayerDataStorage() { - return new PlayerDataStorage() { - @Override public CompletableFuture init() { return CompletableFuture.completedFuture(null); } - @Override public CompletableFuture shutdown() { return CompletableFuture.completedFuture(null); } - }; - } -} +package com.hyperessentials.storage.json; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.hyperessentials.data.Home; +import com.hyperessentials.data.Location; +import com.hyperessentials.data.PlayerHomes; +import com.hyperessentials.data.PlayerTeleportData; +import com.hyperessentials.data.Spawn; +import com.hyperessentials.data.Warp; +import com.hyperessentials.storage.*; +import com.hyperessentials.util.Logger; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * JSON file-based storage provider. + */ +public class JsonStorageProvider implements StorageProvider { + + private final Path dataDir; + private final Path dataRoot; + private final Path playersDir; + private final Gson gson; + private final Path homesDir; + private final JsonWarpStorage warpStorage; + private final JsonSpawnStorage spawnStorage; + private final JsonPlayerDataStorage playerDataStorage; + private final JsonHomeStorage jsonHomeStorage; + + public JsonStorageProvider(@NotNull Path dataDir) { + this.dataDir = dataDir; + this.dataRoot = dataDir.resolve("data"); + this.playersDir = dataRoot.resolve("players"); + this.homesDir = playersDir.resolve("homes"); + this.gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + this.warpStorage = new JsonWarpStorage(); + this.spawnStorage = new JsonSpawnStorage(); + this.playerDataStorage = new JsonPlayerDataStorage(); + this.jsonHomeStorage = new JsonHomeStorage(); + } + + @Override + public CompletableFuture init() { + return CompletableFuture.runAsync(() -> { + try { + Files.createDirectories(dataRoot); + Files.createDirectories(playersDir); + Files.createDirectories(homesDir); + Logger.info("[Storage] JSON storage initialized at %s", dataRoot); + } catch (IOException e) { + throw new RuntimeException("Failed to create storage directories", e); + } + }); + } + + @Override + public CompletableFuture shutdown() { + Logger.info("[Storage] JSON storage provider shut down"); + return CompletableFuture.completedFuture(null); + } + + @Override + @NotNull + public HomeStorage getHomeStorage() { + return jsonHomeStorage; + } + + @Override + @NotNull + public WarpStorage getWarpStorage() { + return warpStorage; + } + + @Override + @NotNull + public SpawnStorage getSpawnStorage() { + return spawnStorage; + } + + @Override + @NotNull + public PlayerDataStorage getPlayerDataStorage() { + return playerDataStorage; + } + + // ========== Atomic write helper ========== + + private void atomicWrite(@NotNull Path target, @NotNull String content) throws IOException { + Path tmp = target.resolveSibling(target.getFileName() + ".tmp"); + Files.writeString(tmp, content); + Files.move(tmp, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } + + // ========== Location serialization ========== + + private JsonObject serializeLocation(Location loc) { + JsonObject obj = new JsonObject(); + obj.addProperty("world", loc.world()); + obj.addProperty("x", loc.x()); + obj.addProperty("y", loc.y()); + obj.addProperty("z", loc.z()); + obj.addProperty("yaw", loc.yaw()); + obj.addProperty("pitch", loc.pitch()); + return obj; + } + + private Location deserializeLocation(JsonObject obj) { + return new Location( + obj.get("world").getAsString(), + obj.get("x").getAsDouble(), + obj.get("y").getAsDouble(), + obj.get("z").getAsDouble(), + obj.get("yaw").getAsFloat(), + obj.get("pitch").getAsFloat() + ); + } + + // ========== Warp Storage ========== + + private class JsonWarpStorage implements WarpStorage { + + private final Path warpsFile = dataRoot.resolve("warps.json"); + + @Override + public CompletableFuture init() { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture shutdown() { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture> loadWarps() { + return CompletableFuture.supplyAsync(() -> { + Map warps = new HashMap<>(); + + if (!Files.exists(warpsFile)) { + return warps; + } + + try { + String json = Files.readString(warpsFile); + JsonObject root = JsonParser.parseString(json).getAsJsonObject(); + + if (root.has("warps") && root.get("warps").isJsonObject()) { + JsonObject warpsObj = root.getAsJsonObject("warps"); + for (String name : warpsObj.keySet()) { + JsonObject warpObj = warpsObj.getAsJsonObject(name); + Warp warp = deserializeWarp(warpObj); + warps.put(warp.name(), warp); + } + } + + Logger.info("[Storage] Loaded %d warps", warps.size()); + } catch (Exception e) { + Logger.severe("[Storage] Failed to load warps: %s", e.getMessage()); + } + + return warps; + }); + } + + @Override + public CompletableFuture saveWarps(@NotNull Map warps) { + return CompletableFuture.runAsync(() -> { + try { + JsonObject root = new JsonObject(); + JsonObject warpsObj = new JsonObject(); + + for (Warp warp : warps.values()) { + warpsObj.add(warp.name(), serializeWarp(warp)); + } + + root.add("warps", warpsObj); + atomicWrite(warpsFile, gson.toJson(root)); + Logger.debug("[Storage] Saved %d warps", warps.size()); + } catch (IOException e) { + Logger.severe("[Storage] Failed to save warps: %s", e.getMessage()); + } + }); + } + + private JsonObject serializeWarp(Warp warp) { + JsonObject obj = new JsonObject(); + obj.addProperty("name", warp.name()); + obj.addProperty("displayName", warp.displayName()); + obj.addProperty("category", warp.category()); + obj.addProperty("world", warp.world()); + obj.addProperty("x", warp.x()); + obj.addProperty("y", warp.y()); + obj.addProperty("z", warp.z()); + obj.addProperty("yaw", warp.yaw()); + obj.addProperty("pitch", warp.pitch()); + if (warp.permission() != null) { + obj.addProperty("permission", warp.permission()); + } + if (warp.description() != null) { + obj.addProperty("description", warp.description()); + } + obj.addProperty("createdAt", warp.createdAt()); + if (warp.createdBy() != null) { + obj.addProperty("createdBy", warp.createdBy()); + } + return obj; + } + + private Warp deserializeWarp(JsonObject obj) { + return new Warp( + obj.get("name").getAsString(), + obj.has("displayName") ? obj.get("displayName").getAsString() : obj.get("name").getAsString(), + obj.has("category") ? obj.get("category").getAsString() : "general", + obj.get("world").getAsString(), + obj.get("x").getAsDouble(), + obj.get("y").getAsDouble(), + obj.get("z").getAsDouble(), + obj.get("yaw").getAsFloat(), + obj.get("pitch").getAsFloat(), + obj.has("permission") ? obj.get("permission").getAsString() : null, + obj.has("description") ? obj.get("description").getAsString() : null, + obj.has("createdAt") ? obj.get("createdAt").getAsLong() : System.currentTimeMillis(), + obj.has("createdBy") ? obj.get("createdBy").getAsString() : null + ); + } + } + + // ========== Spawn Storage ========== + + private class JsonSpawnStorage implements SpawnStorage { + + private final Path spawnsFile = dataRoot.resolve("spawns.json"); + + @Override + public CompletableFuture init() { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture shutdown() { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture> loadSpawns() { + return CompletableFuture.supplyAsync(() -> { + Map spawns = new HashMap<>(); + + if (!Files.exists(spawnsFile)) { + return spawns; + } + + try { + String json = Files.readString(spawnsFile); + JsonObject root = JsonParser.parseString(json).getAsJsonObject(); + + if (root.has("spawns") && root.get("spawns").isJsonObject()) { + JsonObject spawnsObj = root.getAsJsonObject("spawns"); + for (String name : spawnsObj.keySet()) { + JsonObject spawnObj = spawnsObj.getAsJsonObject(name); + Spawn spawn = deserializeSpawn(spawnObj); + spawns.put(spawn.name(), spawn); + } + } + + Logger.info("[Storage] Loaded %d spawns", spawns.size()); + } catch (Exception e) { + Logger.severe("[Storage] Failed to load spawns: %s", e.getMessage()); + } + + return spawns; + }); + } + + @Override + public CompletableFuture saveSpawns(@NotNull Map spawns) { + return CompletableFuture.runAsync(() -> { + try { + JsonObject root = new JsonObject(); + JsonObject spawnsObj = new JsonObject(); + + for (Spawn spawn : spawns.values()) { + spawnsObj.add(spawn.name(), serializeSpawn(spawn)); + } + + root.add("spawns", spawnsObj); + atomicWrite(spawnsFile, gson.toJson(root)); + Logger.debug("[Storage] Saved %d spawns", spawns.size()); + } catch (IOException e) { + Logger.severe("[Storage] Failed to save spawns: %s", e.getMessage()); + } + }); + } + + private JsonObject serializeSpawn(Spawn spawn) { + JsonObject obj = new JsonObject(); + obj.addProperty("name", spawn.name()); + obj.addProperty("world", spawn.world()); + obj.addProperty("x", spawn.x()); + obj.addProperty("y", spawn.y()); + obj.addProperty("z", spawn.z()); + obj.addProperty("yaw", spawn.yaw()); + obj.addProperty("pitch", spawn.pitch()); + if (spawn.permission() != null) { + obj.addProperty("permission", spawn.permission()); + } + if (spawn.groupPermission() != null) { + obj.addProperty("groupPermission", spawn.groupPermission()); + } + obj.addProperty("isDefault", spawn.isDefault()); + obj.addProperty("createdAt", spawn.createdAt()); + if (spawn.createdBy() != null) { + obj.addProperty("createdBy", spawn.createdBy()); + } + return obj; + } + + private Spawn deserializeSpawn(JsonObject obj) { + return new Spawn( + obj.get("name").getAsString(), + obj.get("world").getAsString(), + obj.get("x").getAsDouble(), + obj.get("y").getAsDouble(), + obj.get("z").getAsDouble(), + obj.get("yaw").getAsFloat(), + obj.get("pitch").getAsFloat(), + obj.has("permission") ? obj.get("permission").getAsString() : null, + obj.has("groupPermission") ? obj.get("groupPermission").getAsString() : null, + obj.has("isDefault") && obj.get("isDefault").getAsBoolean(), + obj.has("createdAt") ? obj.get("createdAt").getAsLong() : System.currentTimeMillis(), + obj.has("createdBy") ? obj.get("createdBy").getAsString() : null + ); + } + } + + // ========== Player Data Storage ========== + + private class JsonPlayerDataStorage implements PlayerDataStorage { + + @Override + public CompletableFuture init() { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture shutdown() { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture> loadPlayerData(@NotNull UUID uuid) { + return CompletableFuture.supplyAsync(() -> { + Path playerFile = playersDir.resolve(uuid.toString() + ".json"); + + if (!Files.exists(playerFile)) { + return Optional.empty(); + } + + try { + String json = Files.readString(playerFile); + JsonObject root = JsonParser.parseString(json).getAsJsonObject(); + + String username = root.has("username") ? root.get("username").getAsString() : "Unknown"; + PlayerTeleportData data = new PlayerTeleportData(uuid, username); + + data.setTpToggle(!root.has("tpToggle") || root.get("tpToggle").getAsBoolean()); + data.setLastTpaRequest(root.has("lastTpaRequest") ? root.get("lastTpaRequest").getAsLong() : 0); + data.setLastTeleport(root.has("lastTeleport") ? root.get("lastTeleport").getAsLong() : 0); + + if (root.has("backHistory") && root.get("backHistory").isJsonArray()) { + JsonArray historyArray = root.getAsJsonArray("backHistory"); + List history = new ArrayList<>(); + for (var element : historyArray) { + if (element.isJsonObject()) { + history.add(deserializeLocation(element.getAsJsonObject())); + } + } + data.setBackHistory(history); + } + + return Optional.of(data); + } catch (Exception e) { + Logger.severe("[Storage] Failed to load player data for %s: %s", uuid, e.getMessage()); + return Optional.empty(); + } + }); + } + + @Override + public CompletableFuture savePlayerData(@NotNull PlayerTeleportData data) { + return CompletableFuture.runAsync(() -> { + Path playerFile = playersDir.resolve(data.getUuid().toString() + ".json"); + + try { + JsonObject root = new JsonObject(); + root.addProperty("uuid", data.getUuid().toString()); + root.addProperty("username", data.getUsername()); + root.addProperty("tpToggle", data.isTpToggle()); + root.addProperty("lastTpaRequest", data.getLastTpaRequest()); + root.addProperty("lastTeleport", data.getLastTeleport()); + + JsonArray historyArray = new JsonArray(); + for (Location loc : data.getBackHistory()) { + historyArray.add(serializeLocation(loc)); + } + root.add("backHistory", historyArray); + + atomicWrite(playerFile, gson.toJson(root)); + Logger.debug("[Storage] Saved player data for %s", data.getUsername()); + } catch (IOException e) { + Logger.severe("[Storage] Failed to save player data for %s: %s", data.getUuid(), e.getMessage()); + } + }); + } + + @Override + public CompletableFuture deletePlayerData(@NotNull UUID uuid) { + return CompletableFuture.runAsync(() -> { + Path playerFile = playersDir.resolve(uuid.toString() + ".json"); + try { + Files.deleteIfExists(playerFile); + Logger.debug("[Storage] Deleted player data for %s", uuid); + } catch (IOException e) { + Logger.severe("[Storage] Failed to delete player data for %s: %s", uuid, e.getMessage()); + } + }); + } + } + + // ========== Home Storage ========== + + private class JsonHomeStorage implements HomeStorage { + + @Override + public CompletableFuture init() { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture shutdown() { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture> loadPlayerHomes(@NotNull UUID uuid) { + return CompletableFuture.supplyAsync(() -> { + Path file = homesDir.resolve(uuid + ".json"); + if (!Files.exists(file)) { + return Optional.empty(); + } + + try { + String json = Files.readString(file); + JsonObject root = JsonParser.parseString(json).getAsJsonObject(); + + String username = root.has("username") ? root.get("username").getAsString() : "unknown"; + PlayerHomes playerHomes = new PlayerHomes(uuid, username); + + if (root.has("homes") && root.get("homes").isJsonObject()) { + JsonObject homesObj = root.getAsJsonObject("homes"); + for (String key : homesObj.keySet()) { + JsonObject homeObj = homesObj.getAsJsonObject(key); + Home home = deserializeHome(homeObj); + playerHomes.setHome(home); + } + } + + Logger.debug("[Storage] Loaded %d homes for %s", playerHomes.count(), uuid); + return Optional.of(playerHomes); + } catch (Exception e) { + Logger.severe("[Storage] Failed to load homes for %s: %s", uuid, e.getMessage()); + return Optional.empty(); + } + }); + } + + @Override + public CompletableFuture savePlayerHomes(@NotNull PlayerHomes playerHomes) { + return CompletableFuture.runAsync(() -> { + try { + JsonObject root = new JsonObject(); + root.addProperty("uuid", playerHomes.getUuid().toString()); + root.addProperty("username", playerHomes.getUsername()); + + JsonObject homesObj = new JsonObject(); + for (Home home : playerHomes.getHomes()) { + homesObj.add(home.name().toLowerCase(), serializeHome(home)); + } + root.add("homes", homesObj); + + atomicWrite(homesDir.resolve(playerHomes.getUuid() + ".json"), gson.toJson(root)); + Logger.debug("[Storage] Saved %d homes for %s", playerHomes.count(), playerHomes.getUuid()); + } catch (Exception e) { + Logger.severe("[Storage] Failed to save homes for %s: %s", + playerHomes.getUuid(), e.getMessage()); + } + }); + } + } + + // ========== Home serialization ========== + + private JsonObject serializeHome(Home home) { + JsonObject obj = new JsonObject(); + obj.addProperty("name", home.name()); + obj.addProperty("world", home.world()); + obj.addProperty("x", home.x()); + obj.addProperty("y", home.y()); + obj.addProperty("z", home.z()); + obj.addProperty("yaw", home.yaw()); + obj.addProperty("pitch", home.pitch()); + obj.addProperty("createdAt", home.createdAt()); + obj.addProperty("lastUsed", home.lastUsed()); + return obj; + } + + private Home deserializeHome(JsonObject obj) { + return new Home( + obj.get("name").getAsString(), + obj.get("world").getAsString(), + obj.get("x").getAsDouble(), + obj.get("y").getAsDouble(), + obj.get("z").getAsDouble(), + obj.get("yaw").getAsFloat(), + obj.get("pitch").getAsFloat(), + obj.has("createdAt") ? obj.get("createdAt").getAsLong() : System.currentTimeMillis(), + obj.has("lastUsed") ? obj.get("lastUsed").getAsLong() : System.currentTimeMillis() + ); + } +} diff --git a/src/main/java/com/hyperessentials/util/DurationParser.java b/src/main/java/com/hyperessentials/util/DurationParser.java new file mode 100644 index 0000000..f8f3a8d --- /dev/null +++ b/src/main/java/com/hyperessentials/util/DurationParser.java @@ -0,0 +1,107 @@ +package com.hyperessentials.util; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parses duration strings like "1h30m", "7d", "30s" into milliseconds + * and formats milliseconds back into human-readable strings. + */ +public final class DurationParser { + + private static final Pattern DURATION_PATTERN = Pattern.compile( + "(?:(\\d+)d)?(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?", + Pattern.CASE_INSENSITIVE + ); + + private DurationParser() {} + + /** + * Parses a duration string into milliseconds. + * + * @param input duration string like "1h30m", "7d", "30s", "2d12h" + * @return milliseconds, or -1 if invalid + */ + public static long parse(@Nullable String input) { + if (input == null || input.isBlank()) { + return -1; + } + + String trimmed = input.trim().toLowerCase(); + Matcher matcher = DURATION_PATTERN.matcher(trimmed); + + if (!matcher.matches()) { + return -1; + } + + String days = matcher.group(1); + String hours = matcher.group(2); + String minutes = matcher.group(3); + String seconds = matcher.group(4); + + if (days == null && hours == null && minutes == null && seconds == null) { + return -1; + } + + long total = 0; + if (days != null) total += Long.parseLong(days) * 86400000L; + if (hours != null) total += Long.parseLong(hours) * 3600000L; + if (minutes != null) total += Long.parseLong(minutes) * 60000L; + if (seconds != null) total += Long.parseLong(seconds) * 1000L; + + return total > 0 ? total : -1; + } + + /** + * Formats milliseconds into a human-readable string like "1 hour 30 minutes". + */ + @NotNull + public static String formatHuman(long millis) { + if (millis < 1000) return "0 seconds"; + + long seconds = millis / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + long days = hours / 24; + + seconds %= 60; + minutes %= 60; + hours %= 24; + + StringBuilder sb = new StringBuilder(); + if (days > 0) sb.append(days).append(days == 1 ? " day " : " days "); + if (hours > 0) sb.append(hours).append(hours == 1 ? " hour " : " hours "); + if (minutes > 0) sb.append(minutes).append(minutes == 1 ? " minute " : " minutes "); + if (seconds > 0 && days == 0) sb.append(seconds).append(seconds == 1 ? " second" : " seconds"); + + return sb.toString().trim(); + } + + /** + * Formats milliseconds into a compact string like "1d2h30m". + */ + @NotNull + public static String formatCompact(long millis) { + if (millis < 1000) return "0s"; + + long seconds = millis / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + long days = hours / 24; + + seconds %= 60; + minutes %= 60; + hours %= 24; + + StringBuilder sb = new StringBuilder(); + if (days > 0) sb.append(days).append("d"); + if (hours > 0) sb.append(hours).append("h"); + if (minutes > 0) sb.append(minutes).append("m"); + if (seconds > 0 && days == 0) sb.append(seconds).append("s"); + + return sb.isEmpty() ? "0s" : sb.toString(); + } +} diff --git a/src/main/java/com/hyperessentials/util/Logger.java b/src/main/java/com/hyperessentials/util/Logger.java index 500eca6..2bb67ac 100644 --- a/src/main/java/com/hyperessentials/util/Logger.java +++ b/src/main/java/com/hyperessentials/util/Logger.java @@ -1,76 +1,299 @@ -package com.hyperessentials.util; - -import org.jetbrains.annotations.NotNull; - -import java.util.logging.Level; - -/** - * Wrapped logger with HyperEssentials prefix and formatting. - */ -public final class Logger { - - private static final String PREFIX = "[HyperEssentials] "; - private static java.util.logging.Logger logger; - - private Logger() {} - - public static void init(@NotNull java.util.logging.Logger parentLogger) { - logger = parentLogger; - } - - public static void info(@NotNull String message) { - if (logger != null) { - logger.info(PREFIX + message); - } else { - System.out.println(PREFIX + "[INFO] " + message); - } - } - - public static void info(@NotNull String message, Object... args) { - info(String.format(message, args)); - } - - public static void warn(@NotNull String message) { - if (logger != null) { - logger.warning(PREFIX + message); - } else { - System.out.println(PREFIX + "[WARN] " + message); - } - } - - public static void warn(@NotNull String message, Object... args) { - warn(String.format(message, args)); - } - - public static void severe(@NotNull String message) { - if (logger != null) { - logger.severe(PREFIX + message); - } else { - System.err.println(PREFIX + "[SEVERE] " + message); - } - } - - public static void severe(@NotNull String message, Object... args) { - severe(String.format(message, args)); - } - - public static void severe(@NotNull String message, @NotNull Throwable throwable, Object... args) { - String formatted = String.format(message, args); - if (logger != null) { - logger.log(Level.SEVERE, PREFIX + formatted, throwable); - } else { - System.err.println(PREFIX + "[SEVERE] " + formatted); - throwable.printStackTrace(); - } - } - - public static void debug(@NotNull String message) { - if (logger != null) { - logger.fine(PREFIX + "[DEBUG] " + message); - } - } - - public static void debug(@NotNull String message, Object... args) { - debug(String.format(message, args)); - } -} +package com.hyperessentials.util; + +import com.hypixel.hytale.logger.HytaleLogger; +import java.util.EnumSet; +import java.util.logging.Level; +import org.jetbrains.annotations.NotNull; + +/** + * Wrapped logger with category-based debug logging. + * Uses Hytale's HytaleLogger (Google Flogger) for proper log routing. + */ +public final class Logger { + + private static final String PREFIX = ""; + + private static HytaleLogger logger; + + /** + * Debug categories for category-based debug logging. + */ + public enum DebugCategory { + HOMES("homes"), + WARPS("warps"), + SPAWNS("spawns"), + TELEPORT("teleport"), + KITS("kits"), + MODERATION("moderation"), + UTILITY("utility"), + RTP("rtp"), + ANNOUNCEMENTS("announcements"), + INTEGRATION("integration"), + ECONOMY("economy"), + STORAGE("storage"); + + private final String configKey; + + DebugCategory(String configKey) { + this.configKey = configKey; + } + + /** Returns the config key. */ + public String getConfigKey() { + return configKey; + } + } + + // Volatile flags for thread-safety + private static volatile EnumSet enabledCategories = EnumSet.noneOf(DebugCategory.class); + + private static volatile boolean verboseMode = false; + + private static volatile boolean logToConsole = true; + + private Logger() {} + + /** + * Initializes the logger with the plugin's HytaleLogger. + * + * @param parentLogger the HytaleLogger from the plugin + */ + public static void init(@NotNull HytaleLogger parentLogger) { + logger = parentLogger; + } + + // === Standard Logging === + + /** Logs an info message. */ + public static void info(@NotNull String message) { + if (logger != null) { + logger.at(Level.INFO).log("%s", PREFIX + message); + } else { + System.out.println(PREFIX + "[INFO] " + message); + } + } + + /** Logs an info message with formatting. */ + public static void info(@NotNull String message, Object... args) { + info(String.format(message, args)); + } + + /** Logs a warning message. */ + public static void warn(@NotNull String message) { + if (logger != null) { + logger.at(Level.WARNING).log("%s", PREFIX + message); + } else { + System.out.println(PREFIX + "[WARN] " + message); + } + } + + /** Logs a warning message with formatting. */ + public static void warn(@NotNull String message, Object... args) { + warn(String.format(message, args)); + } + + /** Logs a severe error message. */ + public static void severe(@NotNull String message) { + if (logger != null) { + logger.at(Level.SEVERE).log("%s", PREFIX + message); + } else { + System.err.println(PREFIX + "[SEVERE] " + message); + } + } + + /** Logs a severe error message with formatting. */ + public static void severe(@NotNull String message, Object... args) { + severe(String.format(message, args)); + } + + /** Logs a severe error with exception. */ + public static void severe(@NotNull String message, @NotNull Throwable throwable, Object... args) { + String formatted = String.format(message, args); + if (logger != null) { + logger.at(Level.SEVERE).withCause(throwable).log("%s", PREFIX + formatted); + } else { + System.err.println(PREFIX + "[SEVERE] " + formatted); + throwable.printStackTrace(); + } + } + + /** Logs a debug message (only if debug is enabled). */ + public static void debug(@NotNull String message) { + if (logger != null) { + logger.at(Level.FINE).log("%s", PREFIX + "[DEBUG] " + message); + } + } + + /** Logs a debug message with formatting. */ + public static void debug(@NotNull String message, Object... args) { + debug(String.format(message, args)); + } + + // === Category-Based Debug Logging === + + /** Enables or disables a specific debug category. */ + public static void setDebugEnabled(@NotNull DebugCategory category, boolean enabled) { + EnumSet newSet = EnumSet.copyOf(enabledCategories); + if (enabled) { + newSet.add(category); + } else { + newSet.remove(category); + } + enabledCategories = newSet; + } + + /** Checks if a debug category is enabled. */ + public static boolean isDebugEnabled(@NotNull DebugCategory category) { + return enabledCategories.contains(category); + } + + /** Enables all debug categories. */ + public static void enableAll() { + enabledCategories = EnumSet.allOf(DebugCategory.class); + info("[Debug] All debug categories enabled"); + } + + /** Disables all debug categories. */ + public static void disableAll() { + enabledCategories = EnumSet.noneOf(DebugCategory.class); + info("[Debug] All debug categories disabled"); + } + + /** Gets the currently enabled categories. */ + public static EnumSet getEnabledCategories() { + return EnumSet.copyOf(enabledCategories); + } + + /** Sets verbose mode for extra detailed output. */ + public static void setVerboseMode(boolean enabled) { + verboseMode = enabled; + } + + /** Checks if verbose mode is enabled. */ + public static boolean isVerboseMode() { + return verboseMode; + } + + /** Sets whether debug output goes to console. */ + public static void setLogToConsole(boolean enabled) { + logToConsole = enabled; + } + + // === Category-Specific Debug Methods === + + /** Logs a homes-related debug message. */ + public static void debugHomes(@NotNull String message, Object... args) { + if (isDebugEnabled(DebugCategory.HOMES)) { + logDebug("HOMES", message, args); + } + } + + /** Logs a warps-related debug message. */ + public static void debugWarps(@NotNull String message, Object... args) { + if (isDebugEnabled(DebugCategory.WARPS)) { + logDebug("WARPS", message, args); + } + } + + /** Logs a spawns-related debug message. */ + public static void debugSpawns(@NotNull String message, Object... args) { + if (isDebugEnabled(DebugCategory.SPAWNS)) { + logDebug("SPAWNS", message, args); + } + } + + /** Logs a teleport-related debug message. */ + public static void debugTeleport(@NotNull String message, Object... args) { + if (isDebugEnabled(DebugCategory.TELEPORT)) { + logDebug("TELEPORT", message, args); + } + } + + /** Logs a kits-related debug message. */ + public static void debugKits(@NotNull String message, Object... args) { + if (isDebugEnabled(DebugCategory.KITS)) { + logDebug("KITS", message, args); + } + } + + /** Logs a moderation-related debug message. */ + public static void debugModeration(@NotNull String message, Object... args) { + if (isDebugEnabled(DebugCategory.MODERATION)) { + logDebug("MODERATION", message, args); + } + } + + /** Logs a utility-related debug message. */ + public static void debugUtility(@NotNull String message, Object... args) { + if (isDebugEnabled(DebugCategory.UTILITY)) { + logDebug("UTILITY", message, args); + } + } + + /** Logs an RTP-related debug message. */ + public static void debugRtp(@NotNull String message, Object... args) { + if (isDebugEnabled(DebugCategory.RTP)) { + logDebug("RTP", message, args); + } + } + + /** Logs an announcements-related debug message. */ + public static void debugAnnouncements(@NotNull String message, Object... args) { + if (isDebugEnabled(DebugCategory.ANNOUNCEMENTS)) { + logDebug("ANNOUNCEMENTS", message, args); + } + } + + /** Logs an integration-related debug message. */ + public static void debugIntegration(@NotNull String message, Object... args) { + if (isDebugEnabled(DebugCategory.INTEGRATION)) { + logDebug("INTEGRATION", message, args); + } + } + + /** Logs an economy-related debug message. */ + public static void debugEconomy(@NotNull String message, Object... args) { + if (isDebugEnabled(DebugCategory.ECONOMY)) { + logDebug("ECONOMY", message, args); + } + } + + /** Logs a storage-related debug message. */ + public static void debugStorage(@NotNull String message, Object... args) { + if (isDebugEnabled(DebugCategory.STORAGE)) { + logDebug("STORAGE", message, args); + } + } + + /** Internal method to log a categorized debug message. */ + private static void logDebug(@NotNull String category, @NotNull String message, Object... args) { + String formatted = args.length > 0 ? String.format(message, args) : message; + String logMessage = PREFIX + "[DEBUG:" + category + "] " + formatted; + + if (logToConsole) { + if (logger != null) { + logger.at(Level.INFO).log("%s", logMessage); // Use INFO level for visibility + } else { + System.out.println(logMessage); + } + } else if (logger != null) { + logger.at(Level.FINE).log("%s", logMessage); + } + } + + /** Logs a debug message with verbose details if verbose mode is enabled. */ + public static void debugVerbose(@NotNull DebugCategory category, @NotNull String message, + @NotNull String verboseDetails, Object... args) { + if (!isDebugEnabled(category)) { + return; + } + + String formatted = args.length > 0 ? String.format(message, args) : message; + logDebug(category.name(), formatted); + + if (verboseMode) { + String verboseFormatted = args.length > 0 ? String.format(verboseDetails, args) : verboseDetails; + logDebug(category.name() + ":VERBOSE", verboseFormatted); + } + } +} diff --git a/src/main/java/com/hyperessentials/util/RtpChunkUtil.java b/src/main/java/com/hyperessentials/util/RtpChunkUtil.java new file mode 100644 index 0000000..a96cae0 --- /dev/null +++ b/src/main/java/com/hyperessentials/util/RtpChunkUtil.java @@ -0,0 +1,33 @@ +package com.hyperessentials.util; + +/** + * Chunk math utility for RTP location generation. + * Hytale uses 32-block chunks. + */ +public final class RtpChunkUtil { + + public static final int CHUNK_SIZE = 32; + private static final int CHUNK_SHIFT = 5; + + private RtpChunkUtil() {} + + /** + * Converts a world coordinate to a chunk coordinate. + * + * @param coord world coordinate (X or Z) + * @return chunk coordinate + */ + public static int toChunkCoord(double coord) { + return (int) Math.floor(coord) >> CHUNK_SHIFT; + } + + /** + * Converts a chunk coordinate to the minimum block coordinate of that chunk. + * + * @param chunkCoord chunk coordinate + * @return minimum block coordinate + */ + public static int chunkToBlockMin(int chunkCoord) { + return chunkCoord << CHUNK_SHIFT; + } +} diff --git a/src/main/java/com/hyperessentials/util/TimeUtil.java b/src/main/java/com/hyperessentials/util/TimeUtil.java index ece661a..395df58 100644 --- a/src/main/java/com/hyperessentials/util/TimeUtil.java +++ b/src/main/java/com/hyperessentials/util/TimeUtil.java @@ -1,43 +1,66 @@ -package com.hyperessentials.util; - -import org.jetbrains.annotations.NotNull; - -/** - * Utility class for time formatting. - */ -public final class TimeUtil { - - private TimeUtil() {} - - @NotNull - public static String formatDuration(long millis) { - if (millis < 1000) { - return "0s"; - } - - long seconds = millis / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - - seconds %= 60; - minutes %= 60; - - StringBuilder sb = new StringBuilder(); - if (hours > 0) { - sb.append(hours).append("h "); - } - if (minutes > 0) { - sb.append(minutes).append("m "); - } - if (seconds > 0 || sb.isEmpty()) { - sb.append(seconds).append("s"); - } - - return sb.toString().trim(); - } - - @NotNull - public static String formatSeconds(int seconds) { - return formatDuration(seconds * 1000L); - } -} +package com.hyperessentials.util; + +import org.jetbrains.annotations.NotNull; + +/** + * Utility class for time formatting. + */ +public final class TimeUtil { + + private TimeUtil() {} + + @NotNull + public static String formatDuration(long millis) { + if (millis < 1000) { + return "0s"; + } + + long seconds = millis / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + + seconds %= 60; + minutes %= 60; + + StringBuilder sb = new StringBuilder(); + if (hours > 0) { + sb.append(hours).append("h "); + } + if (minutes > 0) { + sb.append(minutes).append("m "); + } + if (seconds > 0 || sb.isEmpty()) { + sb.append(seconds).append("s"); + } + + return sb.toString().trim(); + } + + @NotNull + public static String formatSeconds(int seconds) { + return formatDuration(seconds * 1000L); + } + + @NotNull + public static String formatRelativeTime(long timestamp) { + if (timestamp == 0) { + return "Never"; + } + + long diff = System.currentTimeMillis() - timestamp; + long seconds = diff / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + long days = hours / 24; + + if (days > 0) { + return days + (days == 1 ? " day ago" : " days ago"); + } else if (hours > 0) { + return hours + (hours == 1 ? " hour ago" : " hours ago"); + } else if (minutes > 0) { + return minutes + (minutes == 1 ? " minute ago" : " minutes ago"); + } else { + return "Just now"; + } + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_announcement_entry.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_announcement_entry.ui new file mode 100644 index 0000000..7193867 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_announcement_entry.ui @@ -0,0 +1,29 @@ +$S = "../shared/styles.ui"; + +Group { + Anchor: (Height: 36, Bottom: 3); + Background: (Color: #141c26); + LayoutMode: Left; + + // Number indicator + Group { + Anchor: (Width: 30); + Padding: (Left: 8); + + Label #EntryNumber { + Text: "#1"; + Style: (FontSize: 11, TextColor: #FFAA00, VerticalAlignment: Center); + } + } + + // Message text + Group { + FlexWeight: 1; + Padding: (Left: 4, Right: 8, Top: 8, Bottom: 8); + + Label #MessageText { + Text: ""; + Style: (FontSize: 11, TextColor: #e0e8f0, VerticalAlignment: Center); + } + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_announcements.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_announcements.ui new file mode 100644 index 0000000..0863c96 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_announcements.ui @@ -0,0 +1,78 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "../shared/nav_bar.ui"; + +$C.@PageOverlay { + $Nav.@HyperEssentialsNavBar #NavBar {} + + $C.@Container { + Anchor: (Width: 580, Height: 440); + + #Title { + $C.@Title { + @Text = "Announcements"; + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); + + // Info header + Group { + Anchor: (Height: 22, Bottom: 4); + LayoutMode: Left; + + Label #MessageCount { + Text: "0 messages"; + Style: (FontSize: 13, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 150); + } + + Group { FlexWeight: 1; } + + Label #IntervalLabel { + Text: ""; + Style: (FontSize: 11, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 200); + } + } + + // Mode label + Group { + Anchor: (Height: 18, Bottom: 6); + + Label #ModeLabel { + Text: ""; + Style: (FontSize: 11, TextColor: #FFAA00); + } + } + + // Divider + Group { + Anchor: (Height: 1, Bottom: 8); + Background: (Color: #FFAA00); + } + + // Info note + Label { + Text: "Messages are managed in announcements.json config file."; + Style: (FontSize: 10, TextColor: #7c8b99); + Anchor: (Height: 16, Bottom: 8); + } + + // Scrollable message list + Group #MessageListContainer { + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + FlexWeight: 1; + + Group #MessageList { + LayoutMode: Top; + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_dashboard.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_dashboard.ui new file mode 100644 index 0000000..6cad5b5 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_dashboard.ui @@ -0,0 +1,156 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "../shared/nav_bar.ui"; + +$C.@PageOverlay { + $Nav.@HyperEssentialsNavBar #NavBar {} + + $C.@Container { + Anchor: (Width: 580, Height: 440); + + #Title { + $C.@Title { + @Text = "Admin Dashboard"; + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); + + // Server info header + Group { + Anchor: (Height: 22, Bottom: 4); + LayoutMode: Left; + + Label #ServerTitle { + Text: "Server Overview"; + Style: (FontSize: 14, TextColor: #FFFFFF, VerticalAlignment: Center); + Anchor: (Width: 200); + } + + Group { FlexWeight: 1; } + + Label #VersionLabel { + Text: ""; + Style: (FontSize: 11, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 200); + } + } + + // Divider + Group { + Anchor: (Height: 1, Bottom: 10); + Background: (Color: #FFAA00); + } + + // Stats cards row + Group { + Anchor: (Height: 52, Bottom: 10); + LayoutMode: Left; + + // Online players card + Group { + FlexWeight: 1; + Background: (Color: #1a2a3a); + Padding: (Full: 8); + LayoutMode: Top; + + Label { + Text: "Online"; + Style: (FontSize: 10, TextColor: #7c8b99); + Anchor: (Height: 14); + } + Label #OnlineCount { + Text: "0"; + Style: (FontSize: 18, TextColor: #55FF55); + Anchor: (Height: 22); + } + } + + Group { Anchor: (Width: 6); } + + // Warps card + Group { + FlexWeight: 1; + Background: (Color: #1a2a3a); + Padding: (Full: 8); + LayoutMode: Top; + + Label { + Text: "Warps"; + Style: (FontSize: 10, TextColor: #7c8b99); + Anchor: (Height: 14); + } + Label #WarpCount { + Text: "0"; + Style: (FontSize: 18, TextColor: #55FFFF); + Anchor: (Height: 22); + } + } + + Group { Anchor: (Width: 6); } + + // Spawns card + Group { + FlexWeight: 1; + Background: (Color: #1a2a3a); + Padding: (Full: 8); + LayoutMode: Top; + + Label { + Text: "Spawns"; + Style: (FontSize: 10, TextColor: #7c8b99); + Anchor: (Height: 14); + } + Label #SpawnCount { + Text: "0"; + Style: (FontSize: 18, TextColor: #FFAA00); + Anchor: (Height: 22); + } + } + + Group { Anchor: (Width: 6); } + + // Kits card + Group { + FlexWeight: 1; + Background: (Color: #1a2a3a); + Padding: (Full: 8); + LayoutMode: Top; + + Label { + Text: "Kits"; + Style: (FontSize: 10, TextColor: #7c8b99); + Anchor: (Height: 14); + } + Label #KitCount { + Text: "0"; + Style: (FontSize: 18, TextColor: #44cc44); + Anchor: (Height: 22); + } + } + } + + // Module status header + Label { + Text: "Module Status"; + Style: (FontSize: 13, TextColor: #FFFFFF); + Anchor: (Height: 20, Bottom: 6); + } + + // Module grid + Group #ModuleGrid { + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + FlexWeight: 1; + + Group #ModuleList { + LayoutMode: Top; + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_kit_entry.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_kit_entry.ui new file mode 100644 index 0000000..e9572d0 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_kit_entry.ui @@ -0,0 +1,67 @@ +$S = "../shared/styles.ui"; + +Group { + Anchor: (Height: 44, Bottom: 3); + Background: (Color: #141c26); + LayoutMode: Left; + + // Color indicator bar + Group #IndicatorBar { + Anchor: (Width: 3, Height: 44); + Background: (Color: #44cc44); + } + + // Info section + Group { + FlexWeight: 1; + Padding: (Left: 10, Top: 5, Bottom: 5); + LayoutMode: Top; + + Group { + Anchor: (Height: 16); + LayoutMode: Left; + + Label #KitName { + Text: ""; + Style: (FontSize: 12, TextColor: #FFFFFF, VerticalAlignment: Center); + Anchor: (Width: 150); + } + + Label #KitItems { + Text: ""; + Style: (FontSize: 10, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 80); + } + } + + Group { + Anchor: (Height: 14); + LayoutMode: Left; + + Label #KitCooldown { + Text: ""; + Style: (FontSize: 10, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 120); + } + + Label #KitOneTime { + Text: ""; + Style: (FontSize: 10, TextColor: #FFAA00, VerticalAlignment: Center); + Anchor: (Width: 80); + } + } + } + + // Action buttons + Group { + Anchor: (Width: 75); + Padding: (Right: 8, Top: 10, Bottom: 10); + LayoutMode: Left; + + TextButton #DeleteBtn { + Text: "Delete"; + Style: $S.@RedButtonStyle; + Anchor: (Width: 65, Height: 24); + } + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_kits.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_kits.ui new file mode 100644 index 0000000..f622728 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_kits.ui @@ -0,0 +1,61 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "../shared/nav_bar.ui"; + +$C.@PageOverlay { + $Nav.@HyperEssentialsNavBar #NavBar {} + + $C.@Container { + Anchor: (Width: 580, Height: 440); + + #Title { + $C.@Title { + @Text = "Manage Kits"; + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); + + // Header row: count + create button + Group { + Anchor: (Height: 28, Bottom: 6); + LayoutMode: Left; + + Label #KitCount { + Text: "0 kits"; + Style: (FontSize: 13, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 200); + } + + Group { FlexWeight: 1; } + + TextButton #CreateBtn { + Text: "Create from Inventory"; + Style: $S.@GoldButtonStyle; + Anchor: (Width: 160, Height: 26); + } + } + + // Divider + Group { + Anchor: (Height: 1, Bottom: 8); + Background: (Color: #FFAA00); + } + + // Scrollable kit list + Group #KitListContainer { + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + FlexWeight: 1; + + Group #KitList { + LayoutMode: Top; + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_moderation.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_moderation.ui new file mode 100644 index 0000000..617eff7 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_moderation.ui @@ -0,0 +1,69 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "../shared/nav_bar.ui"; + +$C.@PageOverlay { + $Nav.@HyperEssentialsNavBar #NavBar {} + + $C.@Container { + Anchor: (Width: 580, Height: 440); + + #Title { + $C.@Title { + @Text = "Moderation"; + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); + + // Filter buttons + Group { + Anchor: (Height: 28, Bottom: 6); + LayoutMode: Left; + + TextButton #FilterActive { + Text: "Active"; + Style: $S.@GoldButtonStyle; + Anchor: (Width: 70, Height: 26); + } + + Group { Anchor: (Width: 6); } + + TextButton #FilterAll { + Text: "All"; + Style: $S.@ButtonStyle; + Anchor: (Width: 50, Height: 26); + } + + Group { FlexWeight: 1; } + + Label #PunishmentCount { + Text: "0 punishments"; + Style: (FontSize: 12, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 150); + } + } + + // Divider + Group { + Anchor: (Height: 1, Bottom: 8); + Background: (Color: #FFAA00); + } + + // Scrollable punishment list + Group #PunishmentListContainer { + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + FlexWeight: 1; + + Group #PunishmentList { + LayoutMode: Top; + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_module_card.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_module_card.ui new file mode 100644 index 0000000..3c849c8 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_module_card.ui @@ -0,0 +1,28 @@ +$S = "../shared/styles.ui"; + +Group { + Anchor: (Height: 30, Bottom: 3); + Background: (Color: #141c26); + Padding: (Left: 10, Right: 10); + LayoutMode: Left; + + // Status indicator dot + Group #StatusDot { + Anchor: (Width: 8, Height: 8); + Background: (Color: #888888); + } + + Group { Anchor: (Width: 8); } + + Label #ModuleName { + Text: ""; + Style: (FontSize: 12, TextColor: #e0e8f0, VerticalAlignment: Center); + FlexWeight: 1; + } + + Label #ModuleStatus { + Text: "Disabled"; + Style: (FontSize: 11, TextColor: #888888, VerticalAlignment: Center); + Anchor: (Width: 80); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_module_toggle.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_module_toggle.ui new file mode 100644 index 0000000..8ca3ef3 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_module_toggle.ui @@ -0,0 +1,30 @@ +$S = "../shared/styles.ui"; + +Group { + Anchor: (Height: 32, Bottom: 3); + Background: (Color: #141c26); + Padding: (Left: 10, Right: 10); + LayoutMode: Left; + + // Status indicator dot + Group #StatusDot { + Anchor: (Width: 8, Height: 8); + Background: (Color: #888888); + } + + Group { Anchor: (Width: 8); } + + // Module name + Label #ModuleName { + Text: ""; + Style: (FontSize: 12, TextColor: #e0e8f0, VerticalAlignment: Center); + FlexWeight: 1; + } + + // Status text + Label #ModuleStatus { + Text: "Disabled"; + Style: (FontSize: 11, TextColor: #888888, VerticalAlignment: Center); + Anchor: (Width: 80); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_player_entry.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_player_entry.ui new file mode 100644 index 0000000..921eb74 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_player_entry.ui @@ -0,0 +1,44 @@ +$S = "../shared/styles.ui"; + +Group { + Anchor: (Height: 36, Bottom: 3); + Background: (Color: #141c26); + LayoutMode: Left; + + // Online indicator + Group #OnlineBar { + Anchor: (Width: 3, Height: 36); + Background: (Color: #55FF55); + } + + // Player info + Group { + FlexWeight: 1; + Padding: (Left: 10, Top: 6, Bottom: 6); + LayoutMode: Top; + + Label #PlayerName { + Text: ""; + Style: (FontSize: 12, TextColor: #FFFFFF, VerticalAlignment: Center); + Anchor: (Height: 14); + } + + Label #PlayerInfo { + Text: ""; + Style: (FontSize: 10, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Height: 12); + } + } + + // View button + Group { + Anchor: (Width: 65); + Padding: (Right: 8, Top: 6, Bottom: 6); + + TextButton #ViewBtn { + Text: "View"; + Style: $S.@GoldButtonStyle; + Anchor: (Width: 55, Height: 24); + } + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_players.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_players.ui new file mode 100644 index 0000000..5b8279e --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_players.ui @@ -0,0 +1,51 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "../shared/nav_bar.ui"; + +$C.@PageOverlay { + $Nav.@HyperEssentialsNavBar #NavBar {} + + $C.@Container { + Anchor: (Width: 580, Height: 440); + + #Title { + $C.@Title { + @Text = "Online Players"; + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); + + // Header with count + Group { + Anchor: (Height: 22, Bottom: 6); + + Label #PlayerCount { + Text: "0 players online"; + Style: (FontSize: 13, TextColor: #7c8b99); + } + } + + // Divider + Group { + Anchor: (Height: 1, Bottom: 8); + Background: (Color: #FFAA00); + } + + // Scrollable player list + Group #PlayerListContainer { + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + FlexWeight: 1; + + Group #PlayerList { + LayoutMode: Top; + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_punishment_entry.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_punishment_entry.ui new file mode 100644 index 0000000..a3823c8 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_punishment_entry.ui @@ -0,0 +1,72 @@ +$S = "../shared/styles.ui"; + +Group { + Anchor: (Height: 50, Bottom: 3); + Background: (Color: #141c26); + LayoutMode: Left; + + // Type indicator bar + Group #TypeBar { + Anchor: (Width: 3, Height: 50); + Background: (Color: #ff5555); + } + + // Info section + Group { + FlexWeight: 1; + Padding: (Left: 10, Top: 5, Bottom: 5); + LayoutMode: Top; + + Group { + Anchor: (Height: 16); + LayoutMode: Left; + + Label #TypeBadge { + Text: "BAN"; + Style: (FontSize: 10, TextColor: #ff5555, VerticalAlignment: Center); + Anchor: (Width: 45); + } + + Label #PlayerName { + Text: ""; + Style: (FontSize: 12, TextColor: #FFFFFF, VerticalAlignment: Center); + Anchor: (Width: 120); + } + + Label #IssuerName { + Text: ""; + Style: (FontSize: 10, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 120); + } + } + + Group { + Anchor: (Height: 14); + LayoutMode: Left; + + Label #Reason { + Text: ""; + Style: (FontSize: 10, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 250); + } + + Label #Expires { + Text: ""; + Style: (FontSize: 10, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 100); + } + } + } + + // Revoke button + Group { + Anchor: (Width: 75); + Padding: (Right: 8, Top: 13, Bottom: 13); + + TextButton #RevokeBtn { + Text: "Revoke"; + Style: $S.@RedButtonStyle; + Anchor: (Width: 65, Height: 24); + } + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_settings.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_settings.ui new file mode 100644 index 0000000..c3bef20 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_settings.ui @@ -0,0 +1,89 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "../shared/nav_bar.ui"; + +$C.@PageOverlay { + $Nav.@HyperEssentialsNavBar #NavBar {} + + $C.@Container { + Anchor: (Width: 580, Height: 440); + + #Title { + $C.@Title { + @Text = "Settings"; + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); + + // Version info + Group { + Anchor: (Height: 22, Bottom: 4); + LayoutMode: Left; + + Label { + Text: "HyperEssentials"; + Style: (FontSize: 14, TextColor: #FFFFFF, VerticalAlignment: Center); + Anchor: (Width: 200); + } + + Group { FlexWeight: 1; } + + Label #VersionLabel { + Text: ""; + Style: (FontSize: 12, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 100); + } + } + + // Data directory + Group { + Anchor: (Height: 18, Bottom: 8); + + Label #DataDirLabel { + Text: ""; + Style: (FontSize: 10, TextColor: #7c8b99); + } + } + + // Divider + Group { + Anchor: (Height: 1, Bottom: 8); + Background: (Color: #FFAA00); + } + + // Reload button + Group { + Anchor: (Height: 32, Bottom: 10); + + TextButton #ReloadBtn { + Text: "Reload Configuration"; + Style: $S.@GoldButtonStyle; + Anchor: (Width: 180, Height: 28); + } + } + + // Module toggles header + Label { + Text: "Module Status"; + Style: (FontSize: 13, TextColor: #FFFFFF); + Anchor: (Height: 20, Bottom: 6); + } + + // Module toggle list + Group #ModuleListContainer { + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + FlexWeight: 1; + + Group #ModuleList { + LayoutMode: Top; + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_spawn_entry.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_spawn_entry.ui new file mode 100644 index 0000000..f08e963 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_spawn_entry.ui @@ -0,0 +1,67 @@ +$S = "../shared/styles.ui"; + +Group { + Anchor: (Height: 44, Bottom: 3); + Background: (Color: #141c26); + LayoutMode: Left; + + // Color indicator bar + Group #IndicatorBar { + Anchor: (Width: 3, Height: 44); + Background: (Color: #FFAA00); + } + + // Info section + Group { + FlexWeight: 1; + Padding: (Left: 10, Top: 5, Bottom: 5); + LayoutMode: Top; + + Group { + Anchor: (Height: 16); + LayoutMode: Left; + + Label #SpawnName { + Text: ""; + Style: (FontSize: 12, TextColor: #FFFFFF, VerticalAlignment: Center); + Anchor: (Width: 150); + } + + Label #DefaultBadge { + Text: ""; + Style: (FontSize: 10, TextColor: #55FF55, VerticalAlignment: Center); + Anchor: (Width: 80); + } + } + + Group { + Anchor: (Height: 14); + LayoutMode: Left; + + Label #SpawnWorld { + Text: ""; + Style: (FontSize: 10, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 100); + } + + Label #SpawnCoords { + Text: ""; + Style: (FontSize: 10, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 180); + } + } + } + + // Action buttons + Group { + Anchor: (Width: 75); + Padding: (Right: 8, Top: 10, Bottom: 10); + LayoutMode: Left; + + TextButton #DeleteBtn { + Text: "Delete"; + Style: $S.@RedButtonStyle; + Anchor: (Width: 65, Height: 24); + } + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_spawns.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_spawns.ui new file mode 100644 index 0000000..9deb6c8 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_spawns.ui @@ -0,0 +1,61 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "../shared/nav_bar.ui"; + +$C.@PageOverlay { + $Nav.@HyperEssentialsNavBar #NavBar {} + + $C.@Container { + Anchor: (Width: 580, Height: 440); + + #Title { + $C.@Title { + @Text = "Manage Spawns"; + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); + + // Header row: count + create button + Group { + Anchor: (Height: 28, Bottom: 6); + LayoutMode: Left; + + Label #SpawnCount { + Text: "0 spawns"; + Style: (FontSize: 13, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 200); + } + + Group { FlexWeight: 1; } + + TextButton #CreateBtn { + Text: "Set Spawn Here"; + Style: $S.@GoldButtonStyle; + Anchor: (Width: 130, Height: 26); + } + } + + // Divider + Group { + Anchor: (Height: 1, Bottom: 8); + Background: (Color: #FFAA00); + } + + // Scrollable spawn list + Group #SpawnListContainer { + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + FlexWeight: 1; + + Group #SpawnList { + LayoutMode: Top; + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_warp_entry.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_warp_entry.ui new file mode 100644 index 0000000..c52672b --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_warp_entry.ui @@ -0,0 +1,67 @@ +$S = "../shared/styles.ui"; + +Group { + Anchor: (Height: 44, Bottom: 3); + Background: (Color: #141c26); + LayoutMode: Left; + + // Color indicator bar + Group #IndicatorBar { + Anchor: (Width: 3, Height: 44); + Background: (Color: #55FFFF); + } + + // Info section + Group { + FlexWeight: 1; + Padding: (Left: 10, Top: 5, Bottom: 5); + LayoutMode: Top; + + Group { + Anchor: (Height: 16); + LayoutMode: Left; + + Label #WarpName { + Text: ""; + Style: (FontSize: 12, TextColor: #FFFFFF, VerticalAlignment: Center); + Anchor: (Width: 150); + } + + Label #WarpCategory { + Text: ""; + Style: (FontSize: 10, TextColor: #55FFFF, VerticalAlignment: Center); + Anchor: (Width: 100); + } + } + + Group { + Anchor: (Height: 14); + LayoutMode: Left; + + Label #WarpWorld { + Text: ""; + Style: (FontSize: 10, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 100); + } + + Label #WarpCoords { + Text: ""; + Style: (FontSize: 10, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 180); + } + } + } + + // Action buttons + Group { + Anchor: (Width: 75); + Padding: (Right: 8, Top: 10, Bottom: 10); + LayoutMode: Left; + + TextButton #DeleteBtn { + Text: "Delete"; + Style: $S.@RedButtonStyle; + Anchor: (Width: 65, Height: 24); + } + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_warps.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_warps.ui new file mode 100644 index 0000000..70ae67e --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/admin/admin_warps.ui @@ -0,0 +1,61 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "../shared/nav_bar.ui"; + +$C.@PageOverlay { + $Nav.@HyperEssentialsNavBar #NavBar {} + + $C.@Container { + Anchor: (Width: 580, Height: 440); + + #Title { + $C.@Title { + @Text = "Manage Warps"; + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); + + // Header row: count + create button + Group { + Anchor: (Height: 28, Bottom: 6); + LayoutMode: Left; + + Label #WarpCount { + Text: "0 warps"; + Style: (FontSize: 13, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 200); + } + + Group { FlexWeight: 1; } + + TextButton #CreateBtn { + Text: "Create at My Location"; + Style: $S.@GoldButtonStyle; + Anchor: (Width: 160, Height: 26); + } + } + + // Divider + Group { + Anchor: (Height: 1, Bottom: 8); + Background: (Color: #FFAA00); + } + + // Scrollable warp list + Group #WarpListContainer { + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + FlexWeight: 1; + + Group #WarpList { + LayoutMode: Top; + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/homes/home_entry.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/homes/home_entry.ui new file mode 100644 index 0000000..67f6e90 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/homes/home_entry.ui @@ -0,0 +1,60 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 48, Bottom: 4); + Background: (Color: #1a2a3a); + Padding: (Left: 12, Right: 12, Top: 6, Bottom: 6); + + // Gold indicator bar + Group { + Anchor: (Width: 3, Top: 4, Bottom: 4); + Background: (Color: #FFAA00); + } + + Group { Anchor: (Width: 8); } + + // Name and world + Group { + Anchor: (Width: 160); + LayoutMode: Top; + + Label #HomeName { + Text: "Home"; + Style: (FontSize: 13, TextColor: #FFFFFF, RenderBold: true, VerticalAlignment: Center); + Anchor: (Height: 20); + } + + Label #HomeWorld { + Text: "World"; + Style: (FontSize: 10, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Height: 16); + } + } + + // Coordinates + Label #HomeCoords { + Text: "0, 0, 0"; + Style: (FontSize: 11, TextColor: #888888, VerticalAlignment: Center); + Anchor: (Width: 120); + } + + Group { FlexWeight: 1; } + + // Teleport button + TextButton #TeleportBtn { + Text: "TELEPORT"; + Anchor: (Width: 90, Height: 28); + Style: $S.@GoldButtonStyle; + } + + Group { Anchor: (Width: 6); } + + // Delete button + TextButton #DeleteBtn { + Text: "DELETE"; + Anchor: (Width: 70, Height: 28); + Style: $S.@RedButtonStyle; + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/homes/homes_page.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/homes/homes_page.ui new file mode 100644 index 0000000..1e99a8c --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/homes/homes_page.ui @@ -0,0 +1,55 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "../shared/nav_bar.ui"; + +$C.@PageOverlay { + $Nav.@HyperEssentialsNavBar #NavBar {} + + $C.@Container { + Anchor: (Width: 550, Height: 420); + + #Title { + $C.@Title { + @Text = "Homes"; + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); + + // Header with count + Group { + Anchor: (Height: 28, Bottom: 6); + LayoutMode: Left; + + Label #HomeCount { + Text: "0 / 0 homes"; + Style: (FontSize: 13, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 200); + } + + Group { FlexWeight: 1; } + } + + // Divider + Group { + Anchor: (Height: 1, Bottom: 8); + Background: (Color: #FFAA00); + } + + // Scrollable home list + Group #HomeListContainer { + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + FlexWeight: 1; + + Group #HomeList { + LayoutMode: Top; + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/kits/kit_entry.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/kits/kit_entry.ui new file mode 100644 index 0000000..dca400f --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/kits/kit_entry.ui @@ -0,0 +1,60 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 48, Bottom: 4); + Background: (Color: #1a2a3a); + Padding: (Left: 12, Right: 12, Top: 6, Bottom: 6); + + // Green indicator bar + Group #KitIndicator { + Anchor: (Width: 3, Top: 4, Bottom: 4); + Background: (Color: #44cc44); + } + + Group { Anchor: (Width: 8); } + + // Kit name and item count + Group { + Anchor: (Width: 160); + LayoutMode: Top; + + Label #KitName { + Text: "Kit"; + Style: (FontSize: 13, TextColor: #FFFFFF, RenderBold: true, VerticalAlignment: Center); + Anchor: (Height: 20); + } + + Label #KitItems { + Text: "0 items"; + Style: (FontSize: 10, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Height: 16); + } + } + + // Cooldown info + Label #KitCooldown { + Text: ""; + Style: (FontSize: 11, TextColor: #888888, VerticalAlignment: Center); + Anchor: (Width: 120); + } + + Group { FlexWeight: 1; } + + // Claim button + TextButton #ClaimBtn { + Text: "CLAIM"; + Anchor: (Width: 80, Height: 28); + Style: $S.@GreenButtonStyle; + } + + Group { Anchor: (Width: 6); } + + // Preview button + TextButton #PreviewBtn { + Text: "PREVIEW"; + Anchor: (Width: 80, Height: 28); + Style: $S.@ButtonStyle; + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/kits/kits_page.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/kits/kits_page.ui new file mode 100644 index 0000000..1a80e8e --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/kits/kits_page.ui @@ -0,0 +1,55 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "../shared/nav_bar.ui"; + +$C.@PageOverlay { + $Nav.@HyperEssentialsNavBar #NavBar {} + + $C.@Container { + Anchor: (Width: 560, Height: 420); + + #Title { + $C.@Title { + @Text = "Kits"; + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); + + // Header with count + Group { + Anchor: (Height: 28, Bottom: 6); + LayoutMode: Left; + + Label #KitCount { + Text: "0 kits available"; + Style: (FontSize: 13, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 200); + } + + Group { FlexWeight: 1; } + } + + // Divider + Group { + Anchor: (Height: 1, Bottom: 8); + Background: (Color: #FFAA00); + } + + // Scrollable kit list + Group #KitListContainer { + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + FlexWeight: 1; + + Group #KitList { + LayoutMode: Top; + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/player/dashboard.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/player/dashboard.ui new file mode 100644 index 0000000..9f242c7 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/player/dashboard.ui @@ -0,0 +1,166 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "../shared/nav_bar.ui"; + +$C.@PageOverlay { + $Nav.@HyperEssentialsNavBar #NavBar {} + + $C.@Container { + Anchor: (Width: 520, Height: 380); + + #Title { + $C.@Title { + @Text = "Dashboard"; + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); + + // Welcome header + Group { + Anchor: (Height: 32, Bottom: 8); + + Label #WelcomeText { + Text: "Welcome!"; + Style: (FontSize: 16, TextColor: #FFFFFF, RenderBold: true, VerticalAlignment: Center); + } + } + + // Divider + Group { + Anchor: (Height: 1, Bottom: 10); + Background: (Color: #FFAA00); + } + + // Stat cards row + Group { + LayoutMode: Left; + Anchor: (Height: 60, Bottom: 12); + + // Homes card + Group { + FlexWeight: 1; + Anchor: (Right: 6); + Background: (Color: #1a2a3a); + Padding: (Left: 10, Right: 10, Top: 8, Bottom: 8); + LayoutMode: Top; + + Label #HomesCount { + Text: "0"; + Style: (FontSize: 18, TextColor: #FFAA00, RenderBold: true); + Anchor: (Height: 24); + } + + Label { + Text: "Homes"; + Style: (FontSize: 10, TextColor: #7c8b99); + Anchor: (Height: 16); + } + } + + // Online players card + Group { + FlexWeight: 1; + Anchor: (Left: 3, Right: 3); + Background: (Color: #1a2a3a); + Padding: (Left: 10, Right: 10, Top: 8, Bottom: 8); + LayoutMode: Top; + + Label #OnlineCount { + Text: "0"; + Style: (FontSize: 18, TextColor: #4aff7f, RenderBold: true); + Anchor: (Height: 24); + } + + Label { + Text: "Online"; + Style: (FontSize: 10, TextColor: #7c8b99); + Anchor: (Height: 16); + } + } + + // Pending TPAs card + Group { + FlexWeight: 1; + Anchor: (Left: 6); + Background: (Color: #1a2a3a); + Padding: (Left: 10, Right: 10, Top: 8, Bottom: 8); + LayoutMode: Top; + + Label #TpaCount { + Text: "0"; + Style: (FontSize: 18, TextColor: #55FFFF, RenderBold: true); + Anchor: (Height: 24); + } + + Label { + Text: "TPA Requests"; + Style: (FontSize: 10, TextColor: #7c8b99); + Anchor: (Height: 16); + } + } + } + + // Quick actions header + Label { + Text: "Quick Actions"; + Style: (FontSize: 12, TextColor: #7c8b99, RenderBold: true); + Anchor: (Height: 20, Bottom: 6); + } + + // Quick action buttons row + Group { + LayoutMode: Left; + Anchor: (Height: 34, Bottom: 8); + + TextButton #HomesBtn { + Text: "HOMES"; + Style: $S.@GoldButtonStyle; + Anchor: (Width: 90, Height: 28); + } + + Group { Anchor: (Width: 8); } + + TextButton #WarpsBtn { + Text: "WARPS"; + Style: $S.@AquaButtonStyle; + Anchor: (Width: 90, Height: 28); + } + + Group { Anchor: (Width: 8); } + + TextButton #KitsBtn { + Text: "KITS"; + Style: $S.@GreenButtonStyle; + Anchor: (Width: 90, Height: 28); + } + } + + // Player info + Group { + Anchor: (Height: 1, Bottom: 8); + Background: (Color: #2a3a4a); + } + + Group { + LayoutMode: Top; + + Label #PlaytimeLabel { + Text: "Playtime: --"; + Style: (FontSize: 11, TextColor: #888888); + Anchor: (Height: 18); + } + + Label #JoinDateLabel { + Text: "First joined: --"; + Style: (FontSize: 11, TextColor: #888888); + Anchor: (Height: 18); + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/player/stats.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/player/stats.ui new file mode 100644 index 0000000..769d4df --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/player/stats.ui @@ -0,0 +1,59 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "../shared/nav_bar.ui"; + +$C.@PageOverlay { + $Nav.@HyperEssentialsNavBar #NavBar {} + + $C.@Container { + Anchor: (Width: 480, Height: 380); + + #Title { + $C.@Title { + @Text = "Stats"; + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); + + // Player name header + Label #PlayerName { + Text: "Player"; + Style: (FontSize: 16, TextColor: #FFFFFF, RenderBold: true); + Anchor: (Height: 28, Bottom: 6); + } + + // Divider + Group { + Anchor: (Height: 1, Bottom: 10); + Background: (Color: #FFAA00); + } + + // Stats section + Group #StatsList { + LayoutMode: Top; + } + + // Divider + Group { + Anchor: (Height: 1, Top: 10, Bottom: 10); + Background: (Color: #2a3a4a); + } + + // Status section header + Label { + Text: "Current Status"; + Style: (FontSize: 12, TextColor: #7c8b99, RenderBold: true); + Anchor: (Height: 20, Bottom: 6); + } + + Group #StatusList { + LayoutMode: Top; + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/shared/confirm_modal.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/confirm_modal.ui new file mode 100644 index 0000000..833db23 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/confirm_modal.ui @@ -0,0 +1,57 @@ +$C = "../../Common.ui"; +$S = "styles.ui"; + +$C.@PageOverlay {}; + +Group { + Anchor: (Width: 340, Height: 160); + + $C.@DecoratedContainer { + LayoutMode: Top; + Padding: (Full: 20); + + Label #ConfirmTitle { + Text: "Confirm Action"; + Anchor: (Height: 22); + Style: ( + FontSize: 16; + TextColor: #FFFFFF; + RenderBold: true; + HorizontalAlignment: Center; + ); + }; + + Label #ConfirmMessage { + Text: "Are you sure?"; + Anchor: (Height: 36, Top: 8); + Style: ( + FontSize: 13; + TextColor: #7c8b99; + HorizontalAlignment: Center; + ); + }; + + Group { + LayoutMode: Left; + Anchor: (Height: 30, Top: 12); + + Group { FlexWeight: 1; } + + TextButton #ConfirmCancel { + Text: "CANCEL"; + Anchor: (Width: 100, Height: 28); + Style: $S.@ButtonStyle; + } + + Group { Anchor: (Width: 10); } + + TextButton #ConfirmAccept { + Text: "CONFIRM"; + Anchor: (Width: 100, Height: 28); + Style: $S.@FlatRedButtonStyle; + } + + Group { FlexWeight: 1; } + }; + }; +}; diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/shared/empty_state.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/empty_state.ui new file mode 100644 index 0000000..0558cbc --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/empty_state.ui @@ -0,0 +1,27 @@ +$C = "../../Common.ui"; +$S = "styles.ui"; + +Group { + LayoutMode: Top; + Padding: (Full: 30); + + Label #EmptyTitle { + Text: "Nothing here"; + Anchor: (Height: 24); + Style: ( + FontSize: 16; + TextColor: #7c8b99; + HorizontalAlignment: Center; + ); + }; + + Label #EmptyMessage { + Text: "No items to display."; + Anchor: (Height: 18, Top: 6); + Style: ( + FontSize: 13; + TextColor: #888888; + HorizontalAlignment: Center; + ); + }; +}; diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/shared/error_page.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/error_page.ui index 19922a6..e2ee0d3 100644 --- a/src/main/resources/Common/UI/Custom/HyperEssentials/shared/error_page.ui +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/error_page.ui @@ -1,12 +1,12 @@ $C = "../../Common.ui"; $S = "styles.ui"; -$C.@Container #ErrorPage { - Anchor: (Width: 400, Height: 200); +$C.@PageOverlay {} - $C.@PageOverlay {}; +Group { + Anchor: (Width: 400, Height: 200); - Group { + $C.@DecoratedContainer { LayoutMode: Top; Padding: (Full: 20); @@ -15,19 +15,19 @@ $C.@Container #ErrorPage { Anchor: (Height: 30); Style: ( FontSize: 18; - TextColor: $S.@ColorRed; + TextColor: #ff5555; HorizontalAlignment: Center; ); - }; + } Label #ErrorMessage { Text: "An error occurred."; Anchor: (Height: 20); Style: ( FontSize: 14; - TextColor: $S.@ColorGray; + TextColor: #7c8b99; HorizontalAlignment: Center; ); - }; - }; -}; + } + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/shared/nav_bar.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/nav_bar.ui index 04f2bff..931f858 100644 --- a/src/main/resources/Common/UI/Custom/HyperEssentials/shared/nav_bar.ui +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/nav_bar.ui @@ -1,38 +1,45 @@ -$C = "../../Common.ui"; -$S = "styles.ui"; +// HyperEssentials Navigation Bar +// Following HyperFactions pattern exactly @NavButton = MenuItem { - Text: "Button"; - Anchor: (Height: 28); - Style: ( - Default: (TextColor: $S.@ColorGray); - Hovered: (TextColor: $S.@ColorAqua); - Selected: (TextColor: $S.@ColorAqua); - ); + Padding: (Left: 20, Right: 20, Top: 0, Bottom: 0); + Style: ( + Default: ( + LabelStyle: (FontSize: 14, TextColor: #7c8b99, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ), + Hovered: ( + Background: #121a26, + LabelStyle: (FontSize: 14, TextColor: #FFAA00, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ) + ); + SelectedStyle: ( + Default: ( + LabelStyle: (FontSize: 14, TextColor: #FFAA00, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ), + Hovered: ( + Background: #121a26, + LabelStyle: (FontSize: 14, TextColor: #FFAA00, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ) + ); }; @HyperEssentialsNavBar = Group { - Anchor: (Height: 40); - LayoutMode: Left; - Background: (Color: $S.@ColorBgDark); - Padding: (Left: 12, Right: 12); + Anchor: (Height: 45, Top: 0); + Background: #0a1119; + LayoutMode: Left; - Group #NavBarTitle { - Anchor: (Width: 160, Height: 40); + Group #NavBarTitle { + Anchor: (Width: 170, Height: 45); + LayoutMode: Left; + Padding: (Left: 15, Right: 0, Top: 0, Bottom: 0); - Label #NavBarTitleLabel { - Text: "HyperEssentials"; - Anchor: (Height: 40); - Style: ( - FontSize: 16; - TextColor: $S.@ColorGold; - VerticalAlignment: Center; - ); - }; - }; + Label #NavBarTitleLabel { + Text: "HyperEssentials"; + Style: (FontSize: 16, TextColor: #FFAA00, RenderBold: true, VerticalAlignment: Center); + } + } - Group #NavBarButtons { - LayoutMode: Left; - Padding: (Left: 12); - }; + Group #NavBarButtons { + LayoutMode: Left; + } }; diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/shared/nav_button.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/nav_button.ui index d549264..2739e0e 100644 --- a/src/main/resources/Common/UI/Custom/HyperEssentials/shared/nav_button.ui +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/nav_button.ui @@ -1,7 +1,7 @@ $NavBar = "nav_bar.ui"; Group { - $NavBar.@NavButton #NavActionButton { - Text: "Button"; - }; -}; + $NavBar.@NavButton #NavActionButton { + Text: "Button"; + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/shared/nav_button_active.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/nav_button_active.ui index 698f8f4..a51a69a 100644 --- a/src/main/resources/Common/UI/Custom/HyperEssentials/shared/nav_button_active.ui +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/nav_button_active.ui @@ -2,9 +2,12 @@ $C = "../../Common.ui"; $S = "styles.ui"; Group { - TextButton #NavActionButton { - Text: "Button"; - Anchor: (Height: 28); - Style: $C.@DefaultTextButtonStyle; - }; -}; + Anchor: (Right: 5); + + TextButton #NavBtn { + Text: "Nav"; + Anchor: (Height: 28, Width: 85); + Padding: (Left: 8, Right: 8, Top: 5, Bottom: 5); + Style: $S.@GoldButtonStyle; + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/shared/stat_row.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/stat_row.ui new file mode 100644 index 0000000..d574bd9 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/stat_row.ui @@ -0,0 +1,29 @@ +$C = "../../Common.ui"; +$S = "styles.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 22); + Padding: (Horizontal: 4); + + Label #StatLabel { + Text: "Label"; + Anchor: (Width: 140, Height: 22); + Style: ( + FontSize: 13; + TextColor: #7c8b99; + VerticalAlignment: Center; + ); + }; + + Label #StatValue { + Text: "Value"; + FlexWeight: 1; + Anchor: (Height: 22); + Style: ( + FontSize: 13; + TextColor: #FFFFFF; + VerticalAlignment: Center; + ); + } +}; diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/shared/styles.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/styles.ui index 5d09df8..0c93d2f 100644 --- a/src/main/resources/Common/UI/Custom/HyperEssentials/shared/styles.ui +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/shared/styles.ui @@ -1,38 +1,172 @@ +// HyperEssentials Shared Styles +// Centralized style definitions for consistent UI appearance +// Brand colors: Gold (#FFAA00) + $C = "../../Common.ui"; -@ColorGold = #FFAA00; -@ColorGreen = #55FF55; -@ColorRed = #FF5555; -@ColorAqua = #55FFFF; -@ColorGray = #7c8b99; -@ColorWhite = #FFFFFF; -@ColorBgDark = #0a1119; -@ColorBgLight = #141c26; - -@ButtonLabelStyle = LabelStyle { - FontSize: 14; - TextColor: @ColorWhite; -}; - -@GreenLabelStyle = LabelStyle { - FontSize: 14; - TextColor: @ColorGreen; -}; - -@GoldLabelStyle = LabelStyle { - FontSize: 14; - TextColor: @ColorGold; -}; - -@SmallLabelStyle = LabelStyle { - FontSize: 12; - TextColor: @ColorGray; -}; - -@ButtonStyle = ButtonStyle { - Default: $C.@TextButtonDefaultBackground; - Hovered: $C.@TextButtonHoveredBackground; - Pressed: $C.@TextButtonPressedBackground; - Disabled: $C.@TextButtonDisabledBackground; - LabelStyle: @ButtonLabelStyle; -}; +// ============================================================================= +// COLOR REFERENCE (used as inline hex in .ui files, NOT as @variables) +// ============================================================================= +// Brand: Gold #FFAA00, GoldLight #FFD700, GoldDark #CC8800 +// Status: Green #44cc44, Red #ff5555, Aqua #55FFFF +// Text: White #FFFFFF, Gray #7c8b99, Muted #888888, Primary #e0e8f0 +// Backgrounds: Dark #0a1119, Panel #141c26, Card #1a2a3a, Nav #16212f +// Divider: #2a3a4a + +// ============================================================================= +// LABEL STYLES +// ============================================================================= + +@ButtonLabelStyle = LabelStyle( + FontSize: 13, + TextColor: #bfcdd5, + RenderBold: true, + HorizontalAlignment: Center, + VerticalAlignment: Center +); + +@GoldLabelStyle = LabelStyle( + FontSize: 13, + TextColor: #FFAA00, + RenderBold: true, + HorizontalAlignment: Center, + VerticalAlignment: Center +); + +@GreenLabelStyle = LabelStyle( + FontSize: 13, + TextColor: #44cc44, + RenderBold: true, + HorizontalAlignment: Center, + VerticalAlignment: Center +); + +@RedLabelStyle = LabelStyle( + FontSize: 13, + TextColor: #ff5555, + RenderBold: true, + HorizontalAlignment: Center, + VerticalAlignment: Center +); + +@AquaLabelStyle = LabelStyle( + FontSize: 13, + TextColor: #55FFFF, + RenderBold: true, + HorizontalAlignment: Center, + VerticalAlignment: Center +); + +@DisabledLabelStyle = LabelStyle( + FontSize: 13, + TextColor: #555555, + RenderBold: true, + HorizontalAlignment: Center, + VerticalAlignment: Center +); + +@FlatWhiteLabelStyle = LabelStyle( + FontSize: 13, + TextColor: #FFFFFF, + RenderBold: true, + HorizontalAlignment: Center, + VerticalAlignment: Center +); + +// ============================================================================= +// BUTTON STYLES +// ============================================================================= + +// Standard button style - native backgrounds with controlled font size +@ButtonStyle = TextButtonStyle( + Default: (Background: $C.@DefaultSquareButtonDefaultBackground, LabelStyle: @ButtonLabelStyle), + Hovered: (Background: $C.@DefaultSquareButtonHoveredBackground, LabelStyle: @ButtonLabelStyle), + Pressed: (Background: $C.@DefaultSquareButtonPressedBackground, LabelStyle: @ButtonLabelStyle), + Disabled: (Background: $C.@DefaultSquareButtonDisabledBackground, LabelStyle: @ButtonLabelStyle), + Sounds: $C.@ButtonSounds, +); + +// Gold text button style (brand accent) +@GoldButtonStyle = TextButtonStyle( + Default: (Background: $C.@DefaultSquareButtonDefaultBackground, LabelStyle: @GoldLabelStyle), + Hovered: (Background: $C.@DefaultSquareButtonHoveredBackground, LabelStyle: @GoldLabelStyle), + Pressed: (Background: $C.@DefaultSquareButtonPressedBackground, LabelStyle: @GoldLabelStyle), + Disabled: (Background: $C.@DefaultSquareButtonDisabledBackground, LabelStyle: @GoldLabelStyle), + Sounds: $C.@ButtonSounds, +); + +// Green text button style (success/confirm actions) +@GreenButtonStyle = TextButtonStyle( + Default: (Background: $C.@DefaultSquareButtonDefaultBackground, LabelStyle: @GreenLabelStyle), + Hovered: (Background: $C.@DefaultSquareButtonHoveredBackground, LabelStyle: @GreenLabelStyle), + Pressed: (Background: $C.@DefaultSquareButtonPressedBackground, LabelStyle: @GreenLabelStyle), + Disabled: (Background: $C.@DefaultSquareButtonDisabledBackground, LabelStyle: @GreenLabelStyle), + Sounds: $C.@ButtonSounds, +); + +// Red text button style (danger/delete actions) +@RedButtonStyle = TextButtonStyle( + Default: (Background: $C.@DefaultSquareButtonDefaultBackground, LabelStyle: @RedLabelStyle), + Hovered: (Background: $C.@DefaultSquareButtonHoveredBackground, LabelStyle: @RedLabelStyle), + Pressed: (Background: $C.@DefaultSquareButtonPressedBackground, LabelStyle: @RedLabelStyle), + Disabled: (Background: $C.@DefaultSquareButtonDisabledBackground, LabelStyle: @RedLabelStyle), + Sounds: $C.@ButtonSounds, +); + +// Aqua text button style (info actions) +@AquaButtonStyle = TextButtonStyle( + Default: (Background: $C.@DefaultSquareButtonDefaultBackground, LabelStyle: @AquaLabelStyle), + Hovered: (Background: $C.@DefaultSquareButtonHoveredBackground, LabelStyle: @AquaLabelStyle), + Pressed: (Background: $C.@DefaultSquareButtonPressedBackground, LabelStyle: @AquaLabelStyle), + Disabled: (Background: $C.@DefaultSquareButtonDisabledBackground, LabelStyle: @AquaLabelStyle), + Sounds: $C.@ButtonSounds, +); + +// Invisible button style - transparent background for click overlays +@InvisibleButtonStyle = TextButtonStyle( + Default: (Background: (Color: #00000000), LabelStyle: @ButtonLabelStyle), + Hovered: (Background: (Color: #00000000), LabelStyle: @ButtonLabelStyle), + Pressed: (Background: (Color: #00000000), LabelStyle: @ButtonLabelStyle), + Disabled: (Background: (Color: #00000000), LabelStyle: @ButtonLabelStyle), + Sounds: $C.@ButtonSounds, +); + +// Disabled button style - grayed out, non-interactive +@DisabledButtonStyle = TextButtonStyle( + Default: (Background: $C.@DefaultSquareButtonDisabledBackground, LabelStyle: @DisabledLabelStyle), + Hovered: (Background: $C.@DefaultSquareButtonDisabledBackground, LabelStyle: @DisabledLabelStyle), + Pressed: (Background: $C.@DefaultSquareButtonDisabledBackground, LabelStyle: @DisabledLabelStyle), + Disabled: (Background: $C.@DefaultSquareButtonDisabledBackground, LabelStyle: @DisabledLabelStyle), + Sounds: $C.@ButtonSounds, +); + +// ============================================================================= +// FLAT COLOR BUTTON STYLES (solid color backgrounds, no texture) +// ============================================================================= + +// Flat red button (destructive actions like delete/ban) +@FlatRedButtonStyle = TextButtonStyle( + Default: (Background: (Color: #CC3344), LabelStyle: @FlatWhiteLabelStyle), + Hovered: (Background: (Color: #DD4455), LabelStyle: @FlatWhiteLabelStyle), + Pressed: (Background: (Color: #AA2233), LabelStyle: @FlatWhiteLabelStyle), + Disabled: (Background: (Color: #553333), LabelStyle: @FlatWhiteLabelStyle), + Sounds: $C.@ButtonSounds, +); + +// Flat green button (confirm actions) +@FlatGreenButtonStyle = TextButtonStyle( + Default: (Background: (Color: #2d8c3c), LabelStyle: @FlatWhiteLabelStyle), + Hovered: (Background: (Color: #3da04c), LabelStyle: @FlatWhiteLabelStyle), + Pressed: (Background: (Color: #1d7c2c), LabelStyle: @FlatWhiteLabelStyle), + Disabled: (Background: (Color: #335533), LabelStyle: @FlatWhiteLabelStyle), + Sounds: $C.@ButtonSounds, +); + +// Flat gold button (brand accent actions) +@FlatGoldButtonStyle = TextButtonStyle( + Default: (Background: (Color: #996600), LabelStyle: @FlatWhiteLabelStyle), + Hovered: (Background: (Color: #AA7700), LabelStyle: @FlatWhiteLabelStyle), + Pressed: (Background: (Color: #885500), LabelStyle: @FlatWhiteLabelStyle), + Disabled: (Background: (Color: #554400), LabelStyle: @FlatWhiteLabelStyle), + Sounds: $C.@ButtonSounds, +); diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/teleport/tpa_entry.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/teleport/tpa_entry.ui new file mode 100644 index 0000000..31c1a1d --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/teleport/tpa_entry.ui @@ -0,0 +1,52 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 48, Bottom: 4); + Background: (Color: #1a2a3a); + Padding: (Left: 12, Right: 12, Top: 6, Bottom: 6); + + // Player name and request type + Group { + Anchor: (Width: 160); + LayoutMode: Top; + + Label #RequesterName { + Text: "Player"; + Style: (FontSize: 13, TextColor: #FFFFFF, RenderBold: true, VerticalAlignment: Center); + Anchor: (Height: 20); + } + + Label #RequestType { + Text: "TPA"; + Style: (FontSize: 10, TextColor: #55FFFF, VerticalAlignment: Center); + Anchor: (Height: 16); + } + } + + // Time remaining + Label #TimeRemaining { + Text: ""; + Style: (FontSize: 11, TextColor: #888888, VerticalAlignment: Center); + Anchor: (Width: 80); + } + + Group { FlexWeight: 1; } + + // Accept button + TextButton #AcceptBtn { + Text: "ACCEPT"; + Anchor: (Width: 80, Height: 28); + Style: $S.@GreenButtonStyle; + } + + Group { Anchor: (Width: 6); } + + // Deny button + TextButton #DenyBtn { + Text: "DENY"; + Anchor: (Width: 70, Height: 28); + Style: $S.@RedButtonStyle; + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/teleport/tpa_page.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/teleport/tpa_page.ui new file mode 100644 index 0000000..caa0042 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/teleport/tpa_page.ui @@ -0,0 +1,75 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "../shared/nav_bar.ui"; + +$C.@PageOverlay { + $Nav.@HyperEssentialsNavBar #NavBar {} + + $C.@Container { + Anchor: (Width: 540, Height: 420); + + #Title { + $C.@Title { + @Text = "TPA Requests"; + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); + + // TPA toggle bar + Group { + Anchor: (Height: 32, Bottom: 8); + LayoutMode: Left; + Background: (Color: #1a2a3a); + Padding: (Left: 12, Right: 12, Top: 4, Bottom: 4); + + Label #ToggleLabel { + Text: "Accepting TPA requests"; + Style: (FontSize: 12, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 200); + } + + Group { FlexWeight: 1; } + + TextButton #ToggleBtn { + Text: "ENABLED"; + Anchor: (Width: 90, Height: 24); + Style: $S.@GreenButtonStyle; + } + } + + // Header with count + Group { + Anchor: (Height: 24, Bottom: 6); + LayoutMode: Left; + + Label #RequestCount { + Text: "0 incoming requests"; + Style: (FontSize: 12, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 200); + } + } + + // Divider + Group { + Anchor: (Height: 1, Bottom: 8); + Background: (Color: #FFAA00); + } + + // Scrollable request list + Group #RequestListContainer { + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + FlexWeight: 1; + + Group #RequestList { + LayoutMode: Top; + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/warps/warp_category_header.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/warps/warp_category_header.ui new file mode 100644 index 0000000..844d84c --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/warps/warp_category_header.ui @@ -0,0 +1,13 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + Anchor: (Height: 26, Bottom: 4, Top: 4); + Padding: (Left: 8); + + Label #CategoryName { + Text: "Category"; + Style: (FontSize: 12, TextColor: #FFAA00, RenderBold: true, VerticalAlignment: Center); + Anchor: (Height: 22); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/warps/warp_entry.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/warps/warp_entry.ui new file mode 100644 index 0000000..5d698ab --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/warps/warp_entry.ui @@ -0,0 +1,62 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 48, Bottom: 4); + Background: (Color: #1a2a3a); + Padding: (Left: 12, Right: 12, Top: 6, Bottom: 6); + + // Aqua indicator bar + Group { + Anchor: (Width: 3, Top: 4, Bottom: 4); + Background: (Color: #55FFFF); + } + + Group { Anchor: (Width: 8); } + + // Name and category + Group { + Anchor: (Width: 160); + LayoutMode: Top; + + Label #WarpName { + Text: "Warp"; + Style: (FontSize: 13, TextColor: #FFFFFF, RenderBold: true, VerticalAlignment: Center); + Anchor: (Height: 20); + } + + Label #WarpCategory { + Text: "general"; + Style: (FontSize: 10, TextColor: #55FFFF, VerticalAlignment: Center); + Anchor: (Height: 16); + } + } + + // World and coordinates + Group { + Anchor: (Width: 140); + LayoutMode: Top; + + Label #WarpWorld { + Text: "World"; + Style: (FontSize: 10, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Height: 16); + } + + Label #WarpCoords { + Text: "0, 0, 0"; + Style: (FontSize: 11, TextColor: #888888, VerticalAlignment: Center); + Anchor: (Height: 20); + } + } + + Group { FlexWeight: 1; } + + // Teleport button + TextButton #TeleportBtn { + Text: "TELEPORT"; + Anchor: (Width: 90, Height: 28); + Style: $S.@AquaButtonStyle; + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperEssentials/warps/warps_page.ui b/src/main/resources/Common/UI/Custom/HyperEssentials/warps/warps_page.ui new file mode 100644 index 0000000..d452094 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperEssentials/warps/warps_page.ui @@ -0,0 +1,55 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "../shared/nav_bar.ui"; + +$C.@PageOverlay { + $Nav.@HyperEssentialsNavBar #NavBar {} + + $C.@Container { + Anchor: (Width: 580, Height: 450); + + #Title { + $C.@Title { + @Text = "Warps"; + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); + + // Header with count + Group { + Anchor: (Height: 28, Bottom: 6); + LayoutMode: Left; + + Label #WarpCount { + Text: "0 warps"; + Style: (FontSize: 13, TextColor: #7c8b99, VerticalAlignment: Center); + Anchor: (Width: 200); + } + + Group { FlexWeight: 1; } + } + + // Divider + Group { + Anchor: (Height: 1, Bottom: 8); + Background: (Color: #FFAA00); + } + + // Scrollable warp list + Group #WarpListContainer { + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + FlexWeight: 1; + + Group #WarpList { + LayoutMode: Top; + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/manifest.json b/src/main/resources/manifest.json index a64aa57..8e52b38 100644 --- a/src/main/resources/manifest.json +++ b/src/main/resources/manifest.json @@ -6,6 +6,6 @@ "Website": "https://github.com/HyperSystems-Development/HyperEssentials", "Main": "com.hyperessentials.platform.HyperEssentialsPlugin", "ServerVersion": "${serverVersion}", - "SoftDependencies": ["HyperPerms"], - "IncludesAssetPack": true + "SoftDependencies": ["HyperPerms", "VaultUnlocked", "LuckPerms", "HyperFactions"], + "IncludesAssetPack": false }