diff --git a/CHANGELOG.md b/CHANGELOG.md index c6f9fcdc..7df47652 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +**Admin GUI: Runtime Config Editor ([#40](https://github.com/HyperSystems-Development/HyperFactions/issues/40))** +- 11-tab config editor covering all HyperFactions settings: Server, Chat, Announcements, Economy, Factions, Faction Perms, Worldmap, Worlds, Backup, Debug, Gravestones +- Size-adaptive layouts: narrow (520px, 1-col), standard (780px, 2-col), wide (1020px, 4-col) — template switches automatically per tab +- Inline editing with boolean toggles, integer/double steppers, text fields, color pickers, enum dropdowns, and locale selectors +- Faction permissions editor with parent/child toggling (disabling parent auto-disables children), Default/Lock checkboxes per flag +- World overrides editor with add/remove worlds and tri-state per-world settings (Default/Allow/Deny) +- Upkeep scaling tiers modal with add/remove/reorder tiers, promote/demote disable on first/last, and live cost example +- Edit session caching — pending changes survive page close/reopen and modal round-trips +- Input validation with per-field error highlighting, debounced text updates, and save-blocked-on-invalid state +- Per-tab label fixes: "Requires Faction", "Show to Factionless", shortened section names +- ConfigSnapshot for applying changes, ConfigValidator for input bounds, ConfigV7→V8 migration + +**Admin GUI: Backup Manager ([#41](https://github.com/HyperSystems-Development/HyperFactions/issues/41))** +- Paginated backup list with expand/collapse detail view per entry +- Create manual backups with optional custom name +- Restore backups with two-click confirmation and automatic safety backup +- Delete backups with two-click confirmation +- Backup type filter dropdown (All / Hourly / Daily / Weekly / Manual / Migration) + +**Admin GUI: Updates Page ([#42](https://github.com/HyperSystems-Development/HyperFactions/issues/42))** +- Two-column layout: HyperFactions (left) and HyperProtect Mixin (right) with mirrored version info +- Shows current version, latest version, channel, build date, and update status for both +- Single "Check for Updates" button checks both simultaneously +- Download buttons appear when updates are available +- Changelog display for HyperFactions updates +- Rollback support with two-click confirmation +- HyperProtect detection via ProtectionMixinBridge (works even without update checker) + **Split MessageKeys into Domain-Specific Files** - Split the monolithic `MessageKeys.java` (2,789 lines, 60 inner classes, ~1,298 constants) into 6 focused domain files: - `CommonKeys.java` — shared messages, protection denial, territory notifications, announcements, teleport, chat display diff --git a/src/main/java/com/hyperfactions/HyperFactions.java b/src/main/java/com/hyperfactions/HyperFactions.java index b9a5196a..15157d49 100644 --- a/src/main/java/com/hyperfactions/HyperFactions.java +++ b/src/main/java/com/hyperfactions/HyperFactions.java @@ -1205,6 +1205,28 @@ public WorldMapService getWorldMapService() { return worldMapService; } + /** + * Restarts all interval-based runtime systems to pick up config changes. + * Called after the admin config editor saves changes. + */ + public void reloadRuntimeSystems() { + // Restart periodic tasks (auto-save, mob clear, upkeep, etc.) + if (periodicTaskManager != null) { + periodicTaskManager.cancelAll(); + periodicTaskManager.startAll(); + Logger.info("[Config] Periodic tasks restarted"); + } + + // Restart worldmap refresh scheduler with new mode/intervals + if (worldMapService != null) { + worldMapService.initializeScheduler(ConfigManager.get().worldMap()); + Logger.info("[Config] World map scheduler restarted"); + } + + // Rebuild world settings resolver + ConfigManager.get().getWorldSettingsResolver().rebuild(ConfigManager.get().worlds()); + } + /** * Gets the map player filter service. * diff --git a/src/main/java/com/hyperfactions/command/admin/AdminSubCommand.java b/src/main/java/com/hyperfactions/command/admin/AdminSubCommand.java index 98de0c06..ea21a8ea 100644 --- a/src/main/java/com/hyperfactions/command/admin/AdminSubCommand.java +++ b/src/main/java/com/hyperfactions/command/admin/AdminSubCommand.java @@ -264,7 +264,12 @@ private void dispatchCommand(@NotNull CommandContext ctx, @Nullable Store 0 ? subArgs[0] : null; + if (tab != null) { + hyperFactions.getGuiManager().openAdminConfig(playerEntity, ref, store, player, tab); + } else { + hyperFactions.getGuiManager().openAdminConfig(playerEntity, ref, store, player); + } } } case "backups" -> { diff --git a/src/main/java/com/hyperfactions/command/admin/handler/AdminWorldHandler.java b/src/main/java/com/hyperfactions/command/admin/handler/AdminWorldHandler.java index f9636235..eb53277c 100644 --- a/src/main/java/com/hyperfactions/command/admin/handler/AdminWorldHandler.java +++ b/src/main/java/com/hyperfactions/command/admin/handler/AdminWorldHandler.java @@ -140,9 +140,6 @@ private void handleList(CommandContext ctx, @Nullable PlayerRef player) { ctx.sendMessage(line); } - if (!config.getClaimBlacklist().isEmpty()) { - ctx.sendMessage(msg(" Claim blacklist: " + String.join(", ", config.getClaimBlacklist()), COLOR_GRAY)); - } } /** diff --git a/src/main/java/com/hyperfactions/config/ConfigFile.java b/src/main/java/com/hyperfactions/config/ConfigFile.java index cc2cf88c..78c9bfae 100644 --- a/src/main/java/com/hyperfactions/config/ConfigFile.java +++ b/src/main/java/com/hyperfactions/config/ConfigFile.java @@ -121,6 +121,14 @@ public void reload() { load(); } + /** + * Resets to factory defaults and saves. + */ + public void resetDefaults() { + createDefaults(); + save(); + } + /** * Loads configuration values from the parsed JSON object. * diff --git a/src/main/java/com/hyperfactions/config/ConfigManager.java b/src/main/java/com/hyperfactions/config/ConfigManager.java index bb431cb1..19c990cf 100644 --- a/src/main/java/com/hyperfactions/config/ConfigManager.java +++ b/src/main/java/com/hyperfactions/config/ConfigManager.java @@ -264,6 +264,30 @@ public void reloadAll() { Logger.info("[Config] Configuration reloaded"); } + /** + * Resets all configuration files to factory defaults and saves. + */ + public void resetAllDefaults() { + Logger.info("[Config] Resetting all configuration to defaults..."); + + factionsConfig.resetDefaults(); + serverConfig.resetDefaults(); + backupConfig.resetDefaults(); + chatConfig.resetDefaults(); + debugConfig.resetDefaults(); + economyConfig.resetDefaults(); + factionPermissionsConfig.resetDefaults(); + worldMapConfig.resetDefaults(); + announcementConfig.resetDefaults(); + gravestoneConfig.resetDefaults(); + worldsConfig.resetDefaults(); + + worldSettingsResolver.rebuild(worldsConfig); + validateAll(); + + Logger.info("[Config] Configuration reset to defaults"); + } + /** * Saves all configuration files. */ diff --git a/src/main/java/com/hyperfactions/config/WorldSettingsResolver.java b/src/main/java/com/hyperfactions/config/WorldSettingsResolver.java index 87a3cfdd..f606c02b 100644 --- a/src/main/java/com/hyperfactions/config/WorldSettingsResolver.java +++ b/src/main/java/com/hyperfactions/config/WorldSettingsResolver.java @@ -34,8 +34,7 @@ public class WorldSettingsResolver { /** The default policy when no match is found. */ private boolean defaultAllow = true; - /** Claim blacklist (always blocked, regardless of per-world settings). */ - private Set claimBlacklist = new HashSet<>(); + // claimBlacklist removed in v8 — migrated to per-world claiming=false entries /** Record for a wildcard pattern with its priority. */ private record WildcardEntry(String key, Pattern pattern, int wildcardCount, WorldSettings settings) {} @@ -50,7 +49,6 @@ public void rebuild(@NotNull WorldsConfig config) { exactMatches.clear(); wildcardPatterns.clear(); defaultAllow = "allow".equals(config.getDefaultPolicy()); - claimBlacklist = new HashSet<>(config.getClaimBlacklist()); for (Map.Entry entry : config.getWorlds().entrySet()) { String key = entry.getKey(); @@ -71,8 +69,8 @@ public void rebuild(@NotNull WorldsConfig config) { // Sort wildcards: fewer wildcards = higher priority (more specific) wildcardPatterns.sort(Comparator.comparingInt(WildcardEntry::wildcardCount)); - Logger.debug("[Worlds] Resolver rebuilt: %d exact, %d wildcard, defaultAllow=%s, blacklist=%d", - exactMatches.size(), wildcardPatterns.size(), defaultAllow, claimBlacklist.size()); + Logger.debug("[Worlds] Resolver rebuilt: %d exact, %d wildcard, defaultAllow=%s", + exactMatches.size(), wildcardPatterns.size(), defaultAllow); } /** @@ -109,11 +107,6 @@ public WorldSettings resolve(@NotNull String worldName) { * @return true if claiming is allowed */ public boolean isClaimingAllowed(@NotNull String worldName) { - // Claim blacklist always takes precedence - if (claimBlacklist.contains(worldName)) { - return false; - } - WorldSettings settings = resolve(worldName); if (settings != null && settings.claiming() != null) { return settings.claiming(); @@ -180,13 +173,4 @@ public boolean isDefaultAllow() { return defaultAllow; } - /** - * Gets the claim blacklist. - * - * @return unmodifiable set of blacklisted world names - */ - @NotNull - public Set getClaimBlacklist() { - return Collections.unmodifiableSet(claimBlacklist); - } } diff --git a/src/main/java/com/hyperfactions/config/modules/AnnouncementConfig.java b/src/main/java/com/hyperfactions/config/modules/AnnouncementConfig.java index 195f72e0..995f47f8 100644 --- a/src/main/java/com/hyperfactions/config/modules/AnnouncementConfig.java +++ b/src/main/java/com/hyperfactions/config/modules/AnnouncementConfig.java @@ -196,6 +196,38 @@ public boolean isTerritoryNotificationsEnabled() { return territoryNotificationsEnabled; } + // === Setters (for admin config editor) === + + /** Sets territory notifications enabled. */ + public void setTerritoryNotificationsEnabled(boolean value) { this.territoryNotificationsEnabled = value; } + + /** Sets wilderness on leave zone enabled. */ + public void setWildernessOnLeaveZoneEnabled(boolean value) { this.wildernessOnLeaveZoneEnabled = value; } + + /** Sets wilderness on leave claim enabled. */ + public void setWildernessOnLeaveClaimEnabled(boolean value) { this.wildernessOnLeaveClaimEnabled = value; } + + /** Sets faction created. */ + public void setFactionCreated(boolean value) { this.factionCreated = value; } + + /** Sets faction disbanded. */ + public void setFactionDisbanded(boolean value) { this.factionDisbanded = value; } + + /** Sets leadership transfer. */ + public void setLeadershipTransfer(boolean value) { this.leadershipTransfer = value; } + + /** Sets overclaim. */ + public void setOverclaim(boolean value) { this.overclaim = value; } + + /** Sets war declared. */ + public void setWarDeclared(boolean value) { this.warDeclared = value; } + + /** Sets alliance formed. */ + public void setAllianceFormed(boolean value) { this.allianceFormed = value; } + + /** Sets alliance broken. */ + public void setAllianceBroken(boolean value) { this.allianceBroken = value; } + // === Wilderness notification getters === public boolean isWildernessOnLeaveZoneEnabled() { @@ -225,4 +257,16 @@ public String getWildernessOnLeaveClaimUpper() { public String getWildernessOnLeaveClaimLower() { return wildernessOnLeaveClaimLower; } + + /** Sets wilderness on leave zone upper text. */ + public void setWildernessOnLeaveZoneUpper(@NotNull String value) { this.wildernessOnLeaveZoneUpper = value; } + + /** Sets wilderness on leave zone lower text. */ + public void setWildernessOnLeaveZoneLower(@NotNull String value) { this.wildernessOnLeaveZoneLower = value; } + + /** Sets wilderness on leave claim upper text. */ + public void setWildernessOnLeaveClaimUpper(@NotNull String value) { this.wildernessOnLeaveClaimUpper = value; } + + /** Sets wilderness on leave claim lower text. */ + public void setWildernessOnLeaveClaimLower(@NotNull String value) { this.wildernessOnLeaveClaimLower = value; } } diff --git a/src/main/java/com/hyperfactions/config/modules/BackupConfig.java b/src/main/java/com/hyperfactions/config/modules/BackupConfig.java index 568dacc7..f262f152 100644 --- a/src/main/java/com/hyperfactions/config/modules/BackupConfig.java +++ b/src/main/java/com/hyperfactions/config/modules/BackupConfig.java @@ -131,6 +131,26 @@ public int getShutdownRetention() { return shutdownRetention; } + // === Setters (for admin config editor) === + + /** Sets hourly retention. */ + public void setHourlyRetention(int value) { this.hourlyRetention = value; } + + /** Sets daily retention. */ + public void setDailyRetention(int value) { this.dailyRetention = value; } + + /** Sets weekly retention. */ + public void setWeeklyRetention(int value) { this.weeklyRetention = value; } + + /** Sets manual retention. */ + public void setManualRetention(int value) { this.manualRetention = value; } + + /** Sets on shutdown. */ + public void setOnShutdown(boolean value) { this.onShutdown = value; } + + /** Sets shutdown retention. */ + public void setShutdownRetention(int value) { this.shutdownRetention = value; } + // === Validation === /** Validates . */ diff --git a/src/main/java/com/hyperfactions/config/modules/ChatConfig.java b/src/main/java/com/hyperfactions/config/modules/ChatConfig.java index 3d35505b..a82744d7 100644 --- a/src/main/java/com/hyperfactions/config/modules/ChatConfig.java +++ b/src/main/java/com/hyperfactions/config/modules/ChatConfig.java @@ -326,6 +326,71 @@ public int getHistoryCleanupIntervalMinutes() { return historyCleanupIntervalMinutes; } + // === Setters (for admin config editor) === + + /** Sets format. */ + public void setFormat(@NotNull String value) { this.format = value; } + + /** Sets tag display. */ + public void setTagDisplay(@NotNull String value) { this.tagDisplay = value; } + + /** Sets tag format. */ + public void setTagFormat(@NotNull String value) { this.tagFormat = value; } + + /** Sets no faction tag. */ + public void setNoFactionTag(@NotNull String value) { this.noFactionTag = value; } + + /** Sets no faction tag color. */ + public void setNoFactionTagColor(@NotNull String value) { this.noFactionTagColor = value; } + + /** Sets player name color. */ + public void setPlayerNameColor(@NotNull String value) { this.playerNameColor = value; } + + /** Sets priority. */ + public void setPriority(@NotNull String value) { this.priority = value; } + + /** Sets relation color own. */ + public void setRelationColorOwn(@NotNull String value) { this.relationColorOwn = value; } + + /** Sets relation color ally. */ + public void setRelationColorAlly(@NotNull String value) { this.relationColorAlly = value; } + + /** Sets relation color neutral. */ + public void setRelationColorNeutral(@NotNull String value) { this.relationColorNeutral = value; } + + /** Sets relation color enemy. */ + public void setRelationColorEnemy(@NotNull String value) { this.relationColorEnemy = value; } + + /** Sets faction chat color. */ + public void setFactionChatColor(@NotNull String value) { this.factionChatColor = value; } + + /** Sets faction chat prefix. */ + public void setFactionChatPrefix(@NotNull String value) { this.factionChatPrefix = value; } + + /** Sets ally chat color. */ + public void setAllyChatColor(@NotNull String value) { this.allyChatColor = value; } + + /** Sets ally chat prefix. */ + public void setAllyChatPrefix(@NotNull String value) { this.allyChatPrefix = value; } + + /** Sets sender name color. */ + public void setSenderNameColor(@NotNull String value) { this.senderNameColor = value; } + + /** Sets message color. */ + public void setMessageColor(@NotNull String value) { this.messageColor = value; } + + /** Sets history enabled. */ + public void setHistoryEnabled(boolean value) { this.historyEnabled = value; } + + /** Sets history max messages. */ + public void setHistoryMaxMessages(int value) { this.historyMaxMessages = value; } + + /** Sets history retention days. */ + public void setHistoryRetentionDays(int value) { this.historyRetentionDays = value; } + + /** Sets history cleanup interval minutes. */ + public void setHistoryCleanupIntervalMinutes(int value) { this.historyCleanupIntervalMinutes = value; } + // === Validation === /** Validates . */ diff --git a/src/main/java/com/hyperfactions/config/modules/DebugConfig.java b/src/main/java/com/hyperfactions/config/modules/DebugConfig.java index 5ea58915..661342da 100644 --- a/src/main/java/com/hyperfactions/config/modules/DebugConfig.java +++ b/src/main/java/com/hyperfactions/config/modules/DebugConfig.java @@ -383,6 +383,18 @@ public void setSentryEnabled(boolean enabled) { this.sentryEnabled = enabled; } + /** Sets enabled by default. */ + public void setEnabledByDefault(boolean value) { this.enabledByDefault = value; } + + /** Sets log to console. */ + public void setLogToConsole(boolean value) { this.logToConsole = value; applyToLogger(); } + + /** Sets sentry debug mode. */ + public void setSentryDebug(boolean value) { this.sentryDebug = value; } + + /** Sets sentry traces sample rate. */ + public void setSentryTracesSampleRate(double value) { this.sentryTracesSampleRate = value; } + // === Setters (for runtime toggle) === /** diff --git a/src/main/java/com/hyperfactions/config/modules/EconomyConfig.java b/src/main/java/com/hyperfactions/config/modules/EconomyConfig.java index 4340096d..b0265274 100644 --- a/src/main/java/com/hyperfactions/config/modules/EconomyConfig.java +++ b/src/main/java/com/hyperfactions/config/modules/EconomyConfig.java @@ -426,6 +426,23 @@ public int getUpkeepWarningHours() { return upkeepScalingTiers; } + // === Setters (for admin config editor) === + + /** Sets currency name. */ + public void setCurrencyName(@NotNull String value) { this.currencyName = value; } + + /** Sets currency name plural. */ + public void setCurrencyNamePlural(@NotNull String value) { this.currencyNamePlural = value; } + + /** Sets currency symbol. */ + public void setCurrencySymbol(@NotNull String value) { this.currencySymbol = value; } + + /** Sets starting balance. */ + public void setStartingBalance(@NotNull BigDecimal value) { this.startingBalance = value; } + + /** Sets disband refund to leader. */ + public void setDisbandRefundToLeader(boolean value) { this.disbandRefundToLeader = value; } + // === Upkeep Setters (for admin GUI / commands) === /** Sets upkeep enabled. */ @@ -461,6 +478,33 @@ public int getUpkeepWarningHours() { /** Sets upkeep scaling tiers. */ public void setUpkeepScalingTiers(@NotNull List value) { upkeepScalingTiers = value; needsSave = true; } + /** Sets currency symbol position. */ + public void setCurrencySymbolPosition(@NotNull String value) { this.currencySymbolPosition = value; } + + /** Sets default max withdraw amount. */ + public void setDefaultMaxWithdrawAmount(@NotNull BigDecimal value) { this.defaultMaxWithdrawAmount = value; } + + /** Sets default max withdraw per period. */ + public void setDefaultMaxWithdrawPerPeriod(@NotNull BigDecimal value) { this.defaultMaxWithdrawPerPeriod = value; } + + /** Sets default max transfer amount. */ + public void setDefaultMaxTransferAmount(@NotNull BigDecimal value) { this.defaultMaxTransferAmount = value; } + + /** Sets default max transfer per period. */ + public void setDefaultMaxTransferPerPeriod(@NotNull BigDecimal value) { this.defaultMaxTransferPerPeriod = value; } + + /** Sets default limit period hours. */ + public void setDefaultLimitPeriodHours(int value) { this.defaultLimitPeriodHours = value; } + + /** Sets deposit fee percent. */ + public void setDepositFeePercent(@NotNull BigDecimal value) { this.depositFeePercent = value; } + + /** Sets withdraw fee percent. */ + public void setWithdrawFeePercent(@NotNull BigDecimal value) { this.withdrawFeePercent = value; } + + /** Sets transfer fee percent. */ + public void setTransferFeePercent(@NotNull BigDecimal value) { this.transferFeePercent = value; } + /** * Creates a TreasuryLimits instance from the server-configured defaults. * diff --git a/src/main/java/com/hyperfactions/config/modules/FactionPermissionsConfig.java b/src/main/java/com/hyperfactions/config/modules/FactionPermissionsConfig.java index 931c8709..b91f1e71 100644 --- a/src/main/java/com/hyperfactions/config/modules/FactionPermissionsConfig.java +++ b/src/main/java/com/hyperfactions/config/modules/FactionPermissionsConfig.java @@ -303,4 +303,34 @@ public FactionPermissions getEffectiveFactionPermissions(@NotNull FactionPermiss public boolean isPermissionLocked(@NotNull String permissionName) { return locks.getOrDefault(permissionName, false); } + + /** + * Gets the default value of a permission flag. + * + * @param flag the flag name + * @return true if the flag is enabled by default + */ + public boolean getDefault(@NotNull String flag) { + return defaults.getOrDefault(flag, false); + } + + /** + * Sets the default value of a permission flag. + * + * @param flag the flag name + * @param value true to enable, false to disable + */ + public void setDefault(@NotNull String flag, boolean value) { + defaults.put(flag, value); + } + + /** + * Sets the lock state of a permission flag. + * + * @param flag the flag name + * @param locked true to lock, false to unlock + */ + public void setLocked(@NotNull String flag, boolean locked) { + locks.put(flag, locked); + } } diff --git a/src/main/java/com/hyperfactions/config/modules/FactionsConfig.java b/src/main/java/com/hyperfactions/config/modules/FactionsConfig.java index 21ef0404..7d5c4ba2 100644 --- a/src/main/java/com/hyperfactions/config/modules/FactionsConfig.java +++ b/src/main/java/com/hyperfactions/config/modules/FactionsConfig.java @@ -657,6 +657,158 @@ public int getStuckCooldownSeconds() { return stuckCooldownSeconds; } + // === Setters (for admin config editor) === + + /** Sets max members. */ + public void setMaxMembers(int value) { this.maxMembers = value; } + + /** Sets max name length. */ + public void setMaxNameLength(int value) { this.maxNameLength = value; } + + /** Sets min name length. */ + public void setMinNameLength(int value) { this.minNameLength = value; } + + /** Sets allow colors. */ + public void setAllowColors(boolean value) { this.allowColors = value; } + + /** Sets max player power. */ + public void setMaxPlayerPower(double value) { this.maxPlayerPower = value; } + + /** Sets starting power. */ + public void setStartingPower(double value) { this.startingPower = value; } + + /** Sets power per claim. */ + public void setPowerPerClaim(double value) { this.powerPerClaim = value; } + + /** Sets death penalty. */ + public void setDeathPenalty(double value) { this.deathPenalty = value; } + + /** Sets kill reward. */ + public void setKillReward(double value) { this.killReward = value; } + + /** Sets kill reward requires faction. */ + public void setKillRewardRequiresFaction(boolean value) { this.killRewardRequiresFaction = value; } + + /** Sets power loss on mob death. */ + public void setPowerLossOnMobDeath(boolean value) { this.powerLossOnMobDeath = value; } + + /** Sets power loss on environmental death. */ + public void setPowerLossOnEnvironmentalDeath(boolean value) { this.powerLossOnEnvironmentalDeath = value; } + + /** Sets regen per minute. */ + public void setRegenPerMinute(double value) { this.regenPerMinute = value; } + + /** Sets regen when offline. */ + public void setRegenWhenOffline(boolean value) { this.regenWhenOffline = value; } + + /** Sets hardcore mode. */ + public void setHardcoreMode(boolean value) { this.hardcoreMode = value; } + + /** Sets max claims. */ + public void setMaxClaims(int value) { this.maxClaims = value; } + + /** Sets only adjacent. */ + public void setOnlyAdjacent(boolean value) { this.onlyAdjacent = value; } + + /** Sets prevent disconnect. */ + public void setPreventDisconnect(boolean value) { this.preventDisconnect = value; } + + /** Sets decay enabled. */ + public void setDecayEnabled(boolean value) { this.decayEnabled = value; } + + /** Sets decay days inactive. */ + public void setDecayDaysInactive(int value) { this.decayDaysInactive = value; } + + /** Sets outsider pickup allowed. */ + public void setOutsiderPickupAllowed(boolean value) { this.outsiderPickupAllowed = value; } + + /** Sets outsider drop allowed. */ + public void setOutsiderDropAllowed(boolean value) { this.outsiderDropAllowed = value; } + + /** Sets factionless explosions allowed. */ + public void setFactionlessExplosionsAllowed(boolean value) { this.factionlessExplosionsAllowed = value; } + + /** Sets enemy explosions allowed. */ + public void setEnemyExplosionsAllowed(boolean value) { this.enemyExplosionsAllowed = value; } + + /** Sets neutral explosions allowed. */ + public void setNeutralExplosionsAllowed(boolean value) { this.neutralExplosionsAllowed = value; } + + /** Sets fire spread allowed. */ + public void setFireSpreadAllowed(boolean value) { this.fireSpreadAllowed = value; } + + /** Sets factionless damage allowed. */ + public void setFactionlessDamageAllowed(boolean value) { this.factionlessDamageAllowed = value; } + + /** Sets enemy damage allowed. */ + public void setEnemyDamageAllowed(boolean value) { this.enemyDamageAllowed = value; } + + /** Sets neutral damage allowed. */ + public void setNeutralDamageAllowed(boolean value) { this.neutralDamageAllowed = value; } + + /** Sets tag duration seconds. */ + public void setTagDurationSeconds(int value) { this.tagDurationSeconds = value; } + + /** Sets ally damage. */ + public void setAllyDamage(boolean value) { this.allyDamage = value; } + + /** Sets faction damage. */ + public void setFactionDamage(boolean value) { this.factionDamage = value; } + + /** Sets tagged logout penalty. */ + public void setTaggedLogoutPenalty(boolean value) { this.taggedLogoutPenalty = value; } + + /** Sets logout power loss. */ + public void setLogoutPowerLoss(double value) { this.logoutPowerLoss = value; } + + /** Sets neutral attack penalty. */ + public void setNeutralAttackPenalty(double value) { this.neutralAttackPenalty = value; } + + /** Sets spawn protection enabled. */ + public void setSpawnProtectionEnabled(boolean value) { this.spawnProtectionEnabled = value; } + + /** Sets spawn protection duration seconds. */ + public void setSpawnProtectionDurationSeconds(int value) { this.spawnProtectionDurationSeconds = value; } + + /** Sets spawn protection break on attack. */ + public void setSpawnProtectionBreakOnAttack(boolean value) { this.spawnProtectionBreakOnAttack = value; } + + /** Sets spawn protection break on move. */ + public void setSpawnProtectionBreakOnMove(boolean value) { this.spawnProtectionBreakOnMove = value; } + + /** Sets max allies. */ + public void setMaxAllies(int value) { this.maxAllies = value; } + + /** Sets max enemies. */ + public void setMaxEnemies(int value) { this.maxEnemies = value; } + + /** Sets invite expiration minutes. */ + public void setInviteExpirationMinutes(int value) { this.inviteExpirationMinutes = value; } + + /** Sets join request expiration hours. */ + public void setJoinRequestExpirationHours(int value) { this.joinRequestExpirationHours = value; } + + /** Sets stuck min radius. */ + public void setStuckMinRadius(int value) { this.stuckMinRadius = value; } + + /** Sets stuck radius increase. */ + public void setStuckRadiusIncrease(int value) { this.stuckRadiusIncrease = value; } + + /** Sets stuck max attempts. */ + public void setStuckMaxAttempts(int value) { this.stuckMaxAttempts = value; } + + /** Sets stuck warmup seconds. */ + public void setStuckWarmupSeconds(int value) { this.stuckWarmupSeconds = value; } + + /** Sets stuck cooldown seconds. */ + public void setStuckCooldownSeconds(int value) { this.stuckCooldownSeconds = value; } + + /** Sets max membership history. */ + public void setMaxMembershipHistory(int value) { this.maxMembershipHistory = value; } + + /** Sets decay claims per cycle. */ + public void setDecayClaimsPerCycle(int value) { this.decayClaimsPerCycle = value; } + // === Utility Methods === /** diff --git a/src/main/java/com/hyperfactions/config/modules/GravestoneConfig.java b/src/main/java/com/hyperfactions/config/modules/GravestoneConfig.java index 021b08d9..b809f609 100644 --- a/src/main/java/com/hyperfactions/config/modules/GravestoneConfig.java +++ b/src/main/java/com/hyperfactions/config/modules/GravestoneConfig.java @@ -170,4 +170,42 @@ public boolean isAllowLootDuringRaid() { public boolean isAllowLootDuringWar() { return allowLootDuringWar; } + + // === Setters (for admin config editor) === + + /** Sets protect in own territory. */ + public void setProtectInOwnTerritory(boolean value) { this.protectInOwnTerritory = value; } + + /** Sets faction members can access. */ + public void setFactionMembersCanAccess(boolean value) { this.factionMembersCanAccess = value; } + + /** Sets allies can access. */ + public void setAlliesCanAccess(boolean value) { this.alliesCanAccess = value; } + + /** Sets protect in safe zone. */ + public void setProtectInSafeZone(boolean value) { this.protectInSafeZone = value; } + + /** Sets protect in war zone. */ + public void setProtectInWarZone(boolean value) { this.protectInWarZone = value; } + + /** Sets protect in wilderness. */ + public void setProtectInWilderness(boolean value) { this.protectInWilderness = value; } + + /** Sets announce death location. */ + public void setAnnounceDeathLocation(boolean value) { this.announceDeathLocation = value; } + + /** Sets protect in enemy territory. */ + public void setProtectInEnemyTerritory(boolean value) { this.protectInEnemyTerritory = value; } + + /** Sets protect in neutral territory. */ + public void setProtectInNeutralTerritory(boolean value) { this.protectInNeutralTerritory = value; } + + /** Sets enemies can loot in own territory. */ + public void setEnemiesCanLootInOwnTerritory(boolean value) { this.enemiesCanLootInOwnTerritory = value; } + + /** Sets allow loot during raid. */ + public void setAllowLootDuringRaid(boolean value) { this.allowLootDuringRaid = value; } + + /** Sets allow loot during war. */ + public void setAllowLootDuringWar(boolean value) { this.allowLootDuringWar = value; } } diff --git a/src/main/java/com/hyperfactions/config/modules/ServerConfig.java b/src/main/java/com/hyperfactions/config/modules/ServerConfig.java index 28836632..aec7ce02 100644 --- a/src/main/java/com/hyperfactions/config/modules/ServerConfig.java +++ b/src/main/java/com/hyperfactions/config/modules/ServerConfig.java @@ -21,7 +21,7 @@ public class ServerConfig extends ModuleConfig { // Config version (for migration tracking — lives here after V5→V6) - private int configVersion = 7; + private int configVersion = 8; // Teleport settings private int warmupSeconds = 5; @@ -104,7 +104,7 @@ public String getModuleName() { @Override protected void createDefaults() { enabled = true; - configVersion = 7; + configVersion = 8; } /** Loads module settings. */ @@ -427,6 +427,74 @@ public void setHyperProtectAutoDownload(boolean value) { this.hyperProtectAutoDownload = value; } + // === Setters (for admin config editor) === + + /** Sets warmup seconds. */ + public void setWarmupSeconds(int value) { this.warmupSeconds = value; } + + /** Sets cooldown seconds. */ + public void setCooldownSeconds(int value) { this.cooldownSeconds = value; } + + /** Sets cancel on move. */ + public void setCancelOnMove(boolean value) { this.cancelOnMove = value; } + + /** Sets cancel on damage. */ + public void setCancelOnDamage(boolean value) { this.cancelOnDamage = value; } + + /** Sets auto save enabled. */ + public void setAutoSaveEnabled(boolean value) { this.autoSaveEnabled = value; } + + /** Sets auto save interval minutes. */ + public void setAutoSaveIntervalMinutes(int value) { this.autoSaveIntervalMinutes = value; } + + /** Sets prefix text. */ + public void setPrefixText(@NotNull String value) { this.prefixText = value; } + + /** Sets prefix color. */ + public void setPrefixColor(@NotNull String value) { this.prefixColor = value; } + + /** Sets prefix bracket color. */ + public void setPrefixBracketColor(@NotNull String value) { this.prefixBracketColor = value; } + + /** Sets primary color. */ + public void setPrimaryColor(@NotNull String value) { this.primaryColor = value; } + + /** Sets gui title. */ + public void setGuiTitle(@NotNull String value) { this.guiTitle = value; } + + /** Sets terrain map enabled. */ + public void setTerrainMapEnabled(boolean value) { this.terrainMapEnabled = value; } + + /** Sets admin requires op. */ + public void setAdminRequiresOp(boolean value) { this.adminRequiresOp = value; } + + /** Sets allow without permission mod. */ + public void setAllowWithoutPermissionMod(boolean value) { this.allowWithoutPermissionMod = value; } + + /** Sets update check enabled. */ + public void setUpdateCheckEnabled(boolean value) { this.updateCheckEnabled = value; } + + /** Sets release channel. */ + public void setReleaseChannel(@NotNull String value) { this.releaseChannel = value; } + + /** Sets mob clear enabled. */ + public void setMobClearEnabled(boolean value) { this.mobClearEnabled = value; } + + /** Sets mob clear interval seconds. */ + public void setMobClearIntervalSeconds(int value) { this.mobClearIntervalSeconds = value; } + + /** Sets default language. */ + public void setDefaultLanguage(@NotNull String value) { this.defaultLanguage = value; } + + /** Sets use player language. */ + public void setUsePlayerLanguage(boolean value) { this.usePlayerLanguage = value; } + + /** Sets leaderboard K/D refresh seconds. */ + public void setLeaderboardKdRefreshSeconds(int value) { this.leaderboardKdRefreshSeconds = value; } + + /** Sets hyper protect auto update. */ + public void setHyperProtectAutoUpdate(boolean value) { this.hyperProtectAutoUpdate = value; } + // === Validation === /** Validates . */ diff --git a/src/main/java/com/hyperfactions/config/modules/WorldMapConfig.java b/src/main/java/com/hyperfactions/config/modules/WorldMapConfig.java index e630216c..7c93d1bb 100644 --- a/src/main/java/com/hyperfactions/config/modules/WorldMapConfig.java +++ b/src/main/java/com/hyperfactions/config/modules/WorldMapConfig.java @@ -391,6 +391,56 @@ public boolean isAutoFallbackOnError() { return autoFallbackOnError; } + // === Setters (for admin config editor) === + + /** Sets show faction tags. */ + public void setShowFactionTags(boolean value) { this.showFactionTags = value; } + + /** Sets player visibility enabled. */ + public void setPlayerVisibilityEnabled(boolean value) { this.playerVisibilityEnabled = value; } + + /** Sets show own faction. */ + public void setShowOwnFaction(boolean value) { this.showOwnFaction = value; } + + /** Sets show allies. */ + public void setShowAllies(boolean value) { this.showAllies = value; } + + /** Sets show neutrals. */ + public void setShowNeutrals(boolean value) { this.showNeutrals = value; } + + /** Sets show enemies. */ + public void setShowEnemies(boolean value) { this.showEnemies = value; } + + /** Sets show factionless players. */ + public void setShowFactionlessPlayers(boolean value) { this.showFactionlessPlayers = value; } + + /** Sets proximity chunk radius. */ + public void setProximityChunkRadius(int value) { this.proximityChunkRadius = value; } + + /** Sets proximity batch interval ticks. */ + public void setProximityBatchIntervalTicks(int value) { this.proximityBatchIntervalTicks = value; } + + /** Sets proximity max chunks per batch. */ + public void setProximityMaxChunksPerBatch(int value) { this.proximityMaxChunksPerBatch = value; } + + /** Sets incremental batch interval ticks. */ + public void setIncrementalBatchIntervalTicks(int value) { this.incrementalBatchIntervalTicks = value; } + + /** Sets incremental max chunks per batch. */ + public void setIncrementalMaxChunksPerBatch(int value) { this.incrementalMaxChunksPerBatch = value; } + + /** Sets debounced delay seconds. */ + public void setDebouncedDelaySeconds(int value) { this.debouncedDelaySeconds = value; } + + /** Sets auto fallback on error. */ + public void setAutoFallbackOnError(boolean value) { this.autoFallbackOnError = value; } + + /** Sets show factionless to factionless. */ + public void setShowFactionlessToFactionless(boolean value) { this.showFactionlessToFactionless = value; } + + /** Sets faction wide refresh threshold. */ + public void setFactionWideRefreshThreshold(int value) { this.factionWideRefreshThreshold = value; } + /** * Checks if faction tags should be shown on the world map. * When enabled, claimed chunks display faction tag text in the corner. diff --git a/src/main/java/com/hyperfactions/config/modules/WorldsConfig.java b/src/main/java/com/hyperfactions/config/modules/WorldsConfig.java index c360738c..0ceae783 100644 --- a/src/main/java/com/hyperfactions/config/modules/WorldsConfig.java +++ b/src/main/java/com/hyperfactions/config/modules/WorldsConfig.java @@ -25,7 +25,6 @@ * "events": { "claiming": false, "powerLoss": false, "friendlyFireFaction": true }, * "arena_%": { "claiming": false, "powerLoss": false, "friendlyFireFaction": true, "friendlyFireAlly": true } * }, - * "claimBlacklist": [] * } * */ @@ -53,7 +52,7 @@ public record WorldSettings( private final Map worlds = new LinkedHashMap<>(); - private List claimBlacklist = new ArrayList<>(); + // claimBlacklist removed in v8 — migrated to per-world claiming=false entries /** Creates a new WorldsConfig. */ public WorldsConfig(@NotNull Path filePath) { @@ -77,14 +76,12 @@ protected void createDefaults() { worlds.put("instance-%", new WorldSettings(false, null, null, null)); // Example entry showing all available options (non-matching name won't affect real worlds) worlds.put("example-world-abc", new WorldSettings(true, true, false, false)); - claimBlacklist = new ArrayList<>(); } /** Loads module settings. */ @Override protected void loadModuleSettings(@NotNull JsonObject root) { defaultPolicy = getString(root, "defaultPolicy", defaultPolicy); - claimBlacklist = getStringList(root, "claimBlacklist"); worlds.clear(); if (root.has("worlds") && root.get("worlds").isJsonObject()) { @@ -128,8 +125,6 @@ protected void writeModuleSettings(@NotNull JsonObject root) { worldsObj.add(entry.getKey(), worldObj); } root.add("worlds", worldsObj); - - root.add("claimBlacklist", toJsonArray(claimBlacklist)); } // === Getters === @@ -140,18 +135,17 @@ public String getDefaultPolicy() { return defaultPolicy; } + /** Sets the default policy ("allow" or "deny"). */ + public void setDefaultPolicy(@NotNull String policy) { + this.defaultPolicy = policy; + } + /** Returns the worlds. */ @NotNull public Map getWorlds() { return Collections.unmodifiableMap(worlds); } - /** Returns the claim blacklist. */ - @NotNull - public List getClaimBlacklist() { - return claimBlacklist; - } - /** * Gets the settings for a specific world key (exact key match, no wildcard resolution). * diff --git a/src/main/java/com/hyperfactions/gui/AdminPageOpener.java b/src/main/java/com/hyperfactions/gui/AdminPageOpener.java index 70ca052c..db99338a 100644 --- a/src/main/java/com/hyperfactions/gui/AdminPageOpener.java +++ b/src/main/java/com/hyperfactions/gui/AdminPageOpener.java @@ -606,6 +606,43 @@ public void openAdminConfig(Player player, Ref ref, } } + /** + * Opens the Admin Config page to a specific tab. + */ + public void openAdminConfig(Player player, Ref ref, + Store store, PlayerRef playerRef, String tab) { + Logger.debug("[GUI] Opening AdminConfigPage for %s (tab: %s)", playerRef.getUsername(), tab); + try { + PageManager pageManager = player.getPageManager(); + AdminConfigPage page = new AdminConfigPage(playerRef, guiManager, tab); + pageManager.openCustomPage(ref, store, page); + Logger.debug("[GUI] AdminConfigPage opened successfully"); + } catch (Exception e) { + ErrorHandler.report("[GUI] Failed to open AdminConfigPage", e); + } + } + + /** + * Opens the Scaling Tiers modal for editing upkeep scaling tiers. + * + * @param player The Player entity + * @param ref The entity reference + * @param store The entity store + * @param playerRef The PlayerRef component + */ + public void openScalingTiersModal(Player player, Ref ref, + Store store, PlayerRef playerRef) { + Logger.debug("[GUI] Opening ScalingTiersModalPage for %s", playerRef.getUsername()); + try { + PageManager pageManager = player.getPageManager(); + ScalingTiersModalPage page = new ScalingTiersModalPage(playerRef, guiManager); + pageManager.openCustomPage(ref, store, page); + Logger.debug("[GUI] ScalingTiersModalPage opened successfully"); + } catch (Exception e) { + ErrorHandler.report("[GUI] Failed to open ScalingTiersModalPage", e); + } + } + /** * Opens the Admin Backups page (placeholder). * Requires hyperfactions.admin permission. @@ -620,7 +657,7 @@ public void openAdminBackups(Player player, Ref ref, Logger.debug("[GUI] Opening AdminBackupsPage for %s", playerRef.getUsername()); try { PageManager pageManager = player.getPageManager(); - AdminBackupsPage page = new AdminBackupsPage(playerRef, guiManager); + AdminBackupsPage page = new AdminBackupsPage(playerRef, guiManager, guiManager.getPlugin().get()); pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminBackupsPage opened successfully"); } catch (Exception e) { @@ -668,7 +705,7 @@ public void openAdminUpdates(Player player, Ref ref, Logger.debug("[GUI] Opening AdminUpdatesPage for %s", playerRef.getUsername()); try { PageManager pageManager = player.getPageManager(); - AdminUpdatesPage page = new AdminUpdatesPage(playerRef, guiManager); + AdminUpdatesPage page = new AdminUpdatesPage(playerRef, guiManager, guiManager.getPlugin().get()); pageManager.openCustomPage(ref, store, page); Logger.debug("[GUI] AdminUpdatesPage opened successfully"); } catch (Exception e) { diff --git a/src/main/java/com/hyperfactions/gui/GuiManager.java b/src/main/java/com/hyperfactions/gui/GuiManager.java index da734049..fd11aa16 100644 --- a/src/main/java/com/hyperfactions/gui/GuiManager.java +++ b/src/main/java/com/hyperfactions/gui/GuiManager.java @@ -517,13 +517,13 @@ private void registerAdminPages() { 6 )); - // Backups page (placeholder) + // Backups page registry.registerEntry(new AdminPageRegistry.Entry( "backups", AdminKeys.AdminNav.BACKUPS, null, (player, ref, store, playerRef, guiManager) -> - new AdminBackupsPage(playerRef, guiManager), + new AdminBackupsPage(playerRef, guiManager, guiManager.getPlugin().get()), true, 7 )); @@ -539,13 +539,13 @@ private void registerAdminPages() { 8 )); - // Updates page (placeholder) + // Updates page registry.registerEntry(new AdminPageRegistry.Entry( "updates", AdminKeys.AdminNav.UPDATES, null, (player, ref, store, playerRef, guiManager) -> - new AdminUpdatesPage(playerRef, guiManager), + new AdminUpdatesPage(playerRef, guiManager, guiManager.getPlugin().get()), true, 9 )); @@ -909,6 +909,18 @@ public void openAdminConfig(Player player, Ref ref, adminPageOpener.openAdminConfig(player, ref, store, playerRef); } + /** Opens the admin config page to a specific tab. */ + public void openAdminConfig(Player player, Ref ref, + Store store, PlayerRef playerRef, String tab) { + adminPageOpener.openAdminConfig(player, ref, store, playerRef, tab); + } + + /** Opens the scaling tiers modal. */ + public void openScalingTiersModal(Player player, Ref ref, + Store store, PlayerRef playerRef) { + adminPageOpener.openScalingTiersModal(player, ref, store, playerRef); + } + /** Opens the admin backups page. */ public void openAdminBackups(Player player, Ref ref, Store store, PlayerRef playerRef) { diff --git a/src/main/java/com/hyperfactions/gui/UIPaths.java b/src/main/java/com/hyperfactions/gui/UIPaths.java index 9457da63..24cfd6bd 100644 --- a/src/main/java/com/hyperfactions/gui/UIPaths.java +++ b/src/main/java/com/hyperfactions/gui/UIPaths.java @@ -203,7 +203,45 @@ private UIPaths() {} public static final String ADMIN_ACTIONS = BASE + "admin/admin_actions.ui"; - public static final String ADMIN_CONFIG = BASE + "admin/admin_config.ui"; + public static final String ADMIN_CONFIG_NARROW = BASE + "admin/admin_config_narrow.ui"; + + public static final String ADMIN_CONFIG_STANDARD = BASE + "admin/admin_config_standard.ui"; + + public static final String ADMIN_CONFIG_WIDE = BASE + "admin/admin_config_wide.ui"; + + public static final String ADMIN_CONFIG_BOOL_ROW = BASE + "admin/admin_config_bool_row.ui"; + + public static final String ADMIN_CONFIG_NUM_ROW = BASE + "admin/admin_config_num_row.ui"; + + public static final String ADMIN_CONFIG_SECTION = BASE + "admin/admin_config_section.ui"; + + public static final String ADMIN_CONFIG_ENUM_ROW = BASE + "admin/admin_config_enum_row.ui"; + + public static final String ADMIN_CONFIG_STR_ROW = BASE + "admin/admin_config_str_row.ui"; + + public static final String ADMIN_CONFIG_COLOR_ROW = BASE + "admin/admin_config_color_row.ui"; + + public static final String ADMIN_CONFIG_ACTION_BTN = BASE + "admin/admin_config_action_btn.ui"; + + public static final String ADMIN_CONFIG_STR_WIDE_ROW = BASE + "admin/admin_config_str_wide_row.ui"; + + public static final String ADMIN_CONFIG_FACPERM_HEADER = BASE + "admin/admin_config_facperm_header.ui"; + + public static final String ADMIN_CONFIG_FACPERM_ROW = BASE + "admin/admin_config_facperm_row.ui"; + + public static final String ADMIN_CONFIG_FACPERM_CHILD_ROW = BASE + "admin/admin_config_facperm_child_row.ui"; + + public static final String ADMIN_CONFIG_SCALING_MODAL = BASE + "admin/admin_config_scaling_modal.ui"; + + public static final String ADMIN_CONFIG_SCALING_ENTRY = BASE + "admin/admin_config_scaling_entry.ui"; + + public static final String ADMIN_CONFIG_ADD_ROW = BASE + "admin/admin_config_add_row.ui"; + + public static final String ADMIN_CONFIG_TRISTATE_ROW = BASE + "admin/admin_config_tristate_row.ui"; + + public static final String ADMIN_CONFIG_BLACKLIST_ENTRY = BASE + "admin/admin_config_blacklist_entry.ui"; + + public static final String ADMIN_CONFIG_WORLD_ENTRY = BASE + "admin/admin_config_world_entry.ui"; public static final String ADMIN_VERSION = BASE + "admin/admin_version.ui"; @@ -211,6 +249,8 @@ private UIPaths() {} public static final String ADMIN_BACKUPS = BASE + "admin/admin_backups.ui"; + public static final String ADMIN_BACKUP_ENTRY = BASE + "admin/admin_backup_entry.ui"; + public static final String ADMIN_FACTIONS = BASE + "admin/admin_factions.ui"; public static final String ADMIN_FACTION_ENTRY = BASE + "admin/admin_faction_entry.ui"; diff --git a/src/main/java/com/hyperfactions/gui/admin/ConfigSnapshot.java b/src/main/java/com/hyperfactions/gui/admin/ConfigSnapshot.java new file mode 100644 index 00000000..c0ba519b --- /dev/null +++ b/src/main/java/com/hyperfactions/gui/admin/ConfigSnapshot.java @@ -0,0 +1,323 @@ +package com.hyperfactions.gui.admin; + +import com.hyperfactions.config.ConfigManager; +import com.hyperfactions.config.modules.*; +import com.hyperfactions.util.Logger; +import java.math.BigDecimal; + +/** + * Helper for the admin config editor GUI. + * + *

+ * Provides methods to apply setting changes by key to the appropriate + * config class, using the setters on each module config. + */ +public final class ConfigSnapshot { + + private ConfigSnapshot() {} + + /** + * The type of a config setting for UI rendering. + */ + public enum SettingType { + BOOLEAN, + INT, + DOUBLE, + STRING, + COLOR + } + + /** + * Applies a changed value to the appropriate config field. + * + * @param key the dotted setting key (e.g. "server.warmupSeconds") + * @param value the new value + */ + public static void applyChange(String key, Object value) { + ConfigManager cfg = ConfigManager.get(); + try { + switch (key) { + // === ServerConfig === + case "server.warmupSeconds" -> cfg.server().setWarmupSeconds(toInt(value)); + case "server.cooldownSeconds" -> cfg.server().setCooldownSeconds(toInt(value)); + case "server.cancelOnMove" -> cfg.server().setCancelOnMove(toBool(value)); + case "server.cancelOnDamage" -> cfg.server().setCancelOnDamage(toBool(value)); + case "server.autoSaveEnabled" -> cfg.server().setAutoSaveEnabled(toBool(value)); + case "server.autoSaveIntervalMinutes" -> cfg.server().setAutoSaveIntervalMinutes(toInt(value)); + case "server.prefixText" -> cfg.server().setPrefixText(toStr(value)); + case "server.prefixColor" -> cfg.server().setPrefixColor(toStr(value)); + case "server.prefixBracketColor" -> cfg.server().setPrefixBracketColor(toStr(value)); + case "server.primaryColor" -> cfg.server().setPrimaryColor(toStr(value)); + case "server.guiTitle" -> cfg.server().setGuiTitle(toStr(value)); + case "server.terrainMapEnabled" -> cfg.server().setTerrainMapEnabled(toBool(value)); + case "server.leaderboardKdRefreshSeconds" -> cfg.server().setLeaderboardKdRefreshSeconds(toInt(value)); + case "server.adminRequiresOp" -> cfg.server().setAdminRequiresOp(toBool(value)); + case "server.allowWithoutPermissionMod" -> cfg.server().setAllowWithoutPermissionMod(toBool(value)); + case "server.updateCheckEnabled" -> cfg.server().setUpdateCheckEnabled(toBool(value)); + case "server.releaseChannel" -> cfg.server().setReleaseChannel(toStr(value)); + case "server.mobClearEnabled" -> cfg.server().setMobClearEnabled(toBool(value)); + case "server.mobClearIntervalSeconds" -> cfg.server().setMobClearIntervalSeconds(toInt(value)); + case "server.defaultLanguage" -> cfg.server().setDefaultLanguage(toStr(value)); + case "server.usePlayerLanguage" -> cfg.server().setUsePlayerLanguage(toBool(value)); + case "server.hyperProtectAutoDownload" -> cfg.server().setHyperProtectAutoDownload(toBool(value)); + case "server.hyperProtectAutoUpdate" -> cfg.server().setHyperProtectAutoUpdate(toBool(value)); + + // === FactionsConfig === + case "factions.maxMembers" -> cfg.factions().setMaxMembers(toInt(value)); + case "factions.maxNameLength" -> cfg.factions().setMaxNameLength(toInt(value)); + case "factions.minNameLength" -> cfg.factions().setMinNameLength(toInt(value)); + case "factions.allowColors" -> cfg.factions().setAllowColors(toBool(value)); + case "factions.maxMembershipHistory" -> cfg.factions().setMaxMembershipHistory(toInt(value)); + case "factions.maxPlayerPower" -> cfg.factions().setMaxPlayerPower(toDouble(value)); + case "factions.startingPower" -> cfg.factions().setStartingPower(toDouble(value)); + case "factions.powerPerClaim" -> cfg.factions().setPowerPerClaim(toDouble(value)); + case "factions.deathPenalty" -> cfg.factions().setDeathPenalty(toDouble(value)); + case "factions.killReward" -> cfg.factions().setKillReward(toDouble(value)); + case "factions.killRewardRequiresFaction" -> cfg.factions().setKillRewardRequiresFaction(toBool(value)); + case "factions.powerLossOnMobDeath" -> cfg.factions().setPowerLossOnMobDeath(toBool(value)); + case "factions.powerLossOnEnvironmentalDeath" -> cfg.factions().setPowerLossOnEnvironmentalDeath(toBool(value)); + case "factions.regenPerMinute" -> cfg.factions().setRegenPerMinute(toDouble(value)); + case "factions.regenWhenOffline" -> cfg.factions().setRegenWhenOffline(toBool(value)); + case "factions.hardcoreMode" -> cfg.factions().setHardcoreMode(toBool(value)); + case "factions.maxClaims" -> cfg.factions().setMaxClaims(toInt(value)); + case "factions.onlyAdjacent" -> cfg.factions().setOnlyAdjacent(toBool(value)); + case "factions.preventDisconnect" -> cfg.factions().setPreventDisconnect(toBool(value)); + case "factions.decayEnabled" -> cfg.factions().setDecayEnabled(toBool(value)); + case "factions.decayDaysInactive" -> cfg.factions().setDecayDaysInactive(toInt(value)); + case "factions.decayClaimsPerCycle" -> cfg.factions().setDecayClaimsPerCycle(toInt(value)); + case "factions.outsiderPickupAllowed" -> cfg.factions().setOutsiderPickupAllowed(toBool(value)); + case "factions.outsiderDropAllowed" -> cfg.factions().setOutsiderDropAllowed(toBool(value)); + case "factions.factionlessExplosionsAllowed" -> cfg.factions().setFactionlessExplosionsAllowed(toBool(value)); + case "factions.enemyExplosionsAllowed" -> cfg.factions().setEnemyExplosionsAllowed(toBool(value)); + case "factions.neutralExplosionsAllowed" -> cfg.factions().setNeutralExplosionsAllowed(toBool(value)); + case "factions.fireSpreadAllowed" -> cfg.factions().setFireSpreadAllowed(toBool(value)); + case "factions.factionlessDamageAllowed" -> cfg.factions().setFactionlessDamageAllowed(toBool(value)); + case "factions.enemyDamageAllowed" -> cfg.factions().setEnemyDamageAllowed(toBool(value)); + case "factions.neutralDamageAllowed" -> cfg.factions().setNeutralDamageAllowed(toBool(value)); + case "factions.tagDurationSeconds" -> cfg.factions().setTagDurationSeconds(toInt(value)); + case "factions.allyDamage" -> cfg.factions().setAllyDamage(toBool(value)); + case "factions.factionDamage" -> cfg.factions().setFactionDamage(toBool(value)); + case "factions.taggedLogoutPenalty" -> cfg.factions().setTaggedLogoutPenalty(toBool(value)); + case "factions.logoutPowerLoss" -> cfg.factions().setLogoutPowerLoss(toDouble(value)); + case "factions.neutralAttackPenalty" -> cfg.factions().setNeutralAttackPenalty(toDouble(value)); + case "factions.spawnProtectionEnabled" -> cfg.factions().setSpawnProtectionEnabled(toBool(value)); + case "factions.spawnProtectionDurationSeconds" -> cfg.factions().setSpawnProtectionDurationSeconds(toInt(value)); + case "factions.spawnProtectionBreakOnAttack" -> cfg.factions().setSpawnProtectionBreakOnAttack(toBool(value)); + case "factions.spawnProtectionBreakOnMove" -> cfg.factions().setSpawnProtectionBreakOnMove(toBool(value)); + case "factions.maxAllies" -> cfg.factions().setMaxAllies(toInt(value)); + case "factions.maxEnemies" -> cfg.factions().setMaxEnemies(toInt(value)); + case "factions.inviteExpirationMinutes" -> cfg.factions().setInviteExpirationMinutes(toInt(value)); + case "factions.joinRequestExpirationHours" -> cfg.factions().setJoinRequestExpirationHours(toInt(value)); + case "factions.stuckMinRadius" -> cfg.factions().setStuckMinRadius(toInt(value)); + case "factions.stuckRadiusIncrease" -> cfg.factions().setStuckRadiusIncrease(toInt(value)); + case "factions.stuckMaxAttempts" -> cfg.factions().setStuckMaxAttempts(toInt(value)); + case "factions.stuckWarmupSeconds" -> cfg.factions().setStuckWarmupSeconds(toInt(value)); + case "factions.stuckCooldownSeconds" -> cfg.factions().setStuckCooldownSeconds(toInt(value)); + + // === BackupConfig === + case "backup.enabled" -> cfg.backup().setEnabled(toBool(value)); + case "backup.hourlyRetention" -> cfg.backup().setHourlyRetention(toInt(value)); + case "backup.dailyRetention" -> cfg.backup().setDailyRetention(toInt(value)); + case "backup.weeklyRetention" -> cfg.backup().setWeeklyRetention(toInt(value)); + case "backup.manualRetention" -> cfg.backup().setManualRetention(toInt(value)); + case "backup.onShutdown" -> cfg.backup().setOnShutdown(toBool(value)); + case "backup.shutdownRetention" -> cfg.backup().setShutdownRetention(toInt(value)); + + // === ChatConfig === + case "chat.enabled" -> cfg.chat().setEnabled(toBool(value)); + case "chat.format" -> cfg.chat().setFormat(toStr(value)); + case "chat.tagDisplay" -> cfg.chat().setTagDisplay(toStr(value)); + case "chat.tagFormat" -> cfg.chat().setTagFormat(toStr(value)); + case "chat.noFactionTag" -> cfg.chat().setNoFactionTag(toStr(value)); + case "chat.noFactionTagColor" -> cfg.chat().setNoFactionTagColor(toStr(value)); + case "chat.playerNameColor" -> cfg.chat().setPlayerNameColor(toStr(value)); + case "chat.relationColorOwn" -> cfg.chat().setRelationColorOwn(toStr(value)); + case "chat.relationColorAlly" -> cfg.chat().setRelationColorAlly(toStr(value)); + case "chat.relationColorNeutral" -> cfg.chat().setRelationColorNeutral(toStr(value)); + case "chat.relationColorEnemy" -> cfg.chat().setRelationColorEnemy(toStr(value)); + case "chat.factionChatColor" -> cfg.chat().setFactionChatColor(toStr(value)); + case "chat.factionChatPrefix" -> cfg.chat().setFactionChatPrefix(toStr(value)); + case "chat.allyChatColor" -> cfg.chat().setAllyChatColor(toStr(value)); + case "chat.allyChatPrefix" -> cfg.chat().setAllyChatPrefix(toStr(value)); + case "chat.senderNameColor" -> cfg.chat().setSenderNameColor(toStr(value)); + case "chat.messageColor" -> cfg.chat().setMessageColor(toStr(value)); + case "chat.historyEnabled" -> cfg.chat().setHistoryEnabled(toBool(value)); + case "chat.historyMaxMessages" -> cfg.chat().setHistoryMaxMessages(toInt(value)); + case "chat.historyRetentionDays" -> cfg.chat().setHistoryRetentionDays(toInt(value)); + case "chat.historyCleanupIntervalMinutes" -> cfg.chat().setHistoryCleanupIntervalMinutes(toInt(value)); + + // === AnnouncementConfig === + case "announce.territoryNotificationsEnabled" -> cfg.announcements().setTerritoryNotificationsEnabled(toBool(value)); + case "announce.factionCreated" -> cfg.announcements().setFactionCreated(toBool(value)); + case "announce.factionDisbanded" -> cfg.announcements().setFactionDisbanded(toBool(value)); + case "announce.leadershipTransfer" -> cfg.announcements().setLeadershipTransfer(toBool(value)); + case "announce.overclaim" -> cfg.announcements().setOverclaim(toBool(value)); + case "announce.warDeclared" -> cfg.announcements().setWarDeclared(toBool(value)); + case "announce.allianceFormed" -> cfg.announcements().setAllianceFormed(toBool(value)); + case "announce.allianceBroken" -> cfg.announcements().setAllianceBroken(toBool(value)); + case "announce.wildernessOnLeaveZoneEnabled" -> cfg.announcements().setWildernessOnLeaveZoneEnabled(toBool(value)); + case "announce.wildernessOnLeaveZoneUpper" -> cfg.announcements().setWildernessOnLeaveZoneUpper(toStr(value)); + case "announce.wildernessOnLeaveZoneLower" -> cfg.announcements().setWildernessOnLeaveZoneLower(toStr(value)); + case "announce.wildernessOnLeaveClaimEnabled" -> cfg.announcements().setWildernessOnLeaveClaimEnabled(toBool(value)); + case "announce.wildernessOnLeaveClaimUpper" -> cfg.announcements().setWildernessOnLeaveClaimUpper(toStr(value)); + case "announce.wildernessOnLeaveClaimLower" -> cfg.announcements().setWildernessOnLeaveClaimLower(toStr(value)); + + // === EconomyConfig === + case "economy.enabled" -> cfg.economy().setEnabled(toBool(value)); + case "economy.currencyName" -> cfg.economy().setCurrencyName(toStr(value)); + case "economy.currencyNamePlural" -> cfg.economy().setCurrencyNamePlural(toStr(value)); + case "economy.currencySymbol" -> cfg.economy().setCurrencySymbol(toStr(value)); + case "economy.currencySymbolPosition" -> cfg.economy().setCurrencySymbolPosition(toStr(value)); + case "economy.startingBalance" -> cfg.economy().setStartingBalance(toBigDecimal(value)); + case "economy.disbandRefundToLeader" -> cfg.economy().setDisbandRefundToLeader(toBool(value)); + case "economy.defaultMaxWithdrawAmount" -> cfg.economy().setDefaultMaxWithdrawAmount(toBigDecimal(value)); + case "economy.defaultMaxWithdrawPerPeriod" -> cfg.economy().setDefaultMaxWithdrawPerPeriod(toBigDecimal(value)); + case "economy.defaultMaxTransferAmount" -> cfg.economy().setDefaultMaxTransferAmount(toBigDecimal(value)); + case "economy.defaultMaxTransferPerPeriod" -> cfg.economy().setDefaultMaxTransferPerPeriod(toBigDecimal(value)); + case "economy.defaultLimitPeriodHours" -> cfg.economy().setDefaultLimitPeriodHours(toInt(value)); + case "economy.depositFeePercent" -> cfg.economy().setDepositFeePercent(toBigDecimal(value)); + case "economy.withdrawFeePercent" -> cfg.economy().setWithdrawFeePercent(toBigDecimal(value)); + case "economy.transferFeePercent" -> cfg.economy().setTransferFeePercent(toBigDecimal(value)); + case "economy.upkeepEnabled" -> cfg.economy().setUpkeepEnabled(toBool(value)); + case "economy.upkeepCostPerChunk" -> cfg.economy().setUpkeepCostPerChunk(toBigDecimal(value)); + case "economy.upkeepIntervalHours" -> cfg.economy().setUpkeepIntervalHours(toInt(value)); + case "economy.upkeepGracePeriodHours" -> cfg.economy().setUpkeepGracePeriodHours(toInt(value)); + case "economy.upkeepAutoPayDefault" -> cfg.economy().setUpkeepAutoPayDefault(toBool(value)); + case "economy.upkeepFreeChunks" -> cfg.economy().setUpkeepFreeChunks(toInt(value)); + case "economy.upkeepClaimLossPerCycle" -> cfg.economy().setUpkeepClaimLossPerCycle(toInt(value)); + case "economy.upkeepWarningHours" -> cfg.economy().setUpkeepWarningHours(toInt(value)); + case "economy.upkeepMaxCostCap" -> cfg.economy().setUpkeepMaxCostCap(toBigDecimal(value)); + case "economy.upkeepScalingMode" -> cfg.economy().setUpkeepScalingMode(toStr(value)); + + // === WorldMapConfig === + case "worldmap.enabled" -> cfg.worldMap().setEnabled(toBool(value)); + case "worldmap.refreshMode" -> cfg.worldMap().setRefreshMode(WorldMapConfig.RefreshMode.fromString(toStr(value))); + case "worldmap.showFactionTags" -> cfg.worldMap().setShowFactionTags(toBool(value)); + case "worldmap.playerVisibilityEnabled" -> cfg.worldMap().setPlayerVisibilityEnabled(toBool(value)); + case "worldmap.showOwnFaction" -> cfg.worldMap().setShowOwnFaction(toBool(value)); + case "worldmap.showAllies" -> cfg.worldMap().setShowAllies(toBool(value)); + case "worldmap.showNeutrals" -> cfg.worldMap().setShowNeutrals(toBool(value)); + case "worldmap.showEnemies" -> cfg.worldMap().setShowEnemies(toBool(value)); + case "worldmap.showFactionlessPlayers" -> cfg.worldMap().setShowFactionlessPlayers(toBool(value)); + case "worldmap.showFactionlessToFactionless" -> cfg.worldMap().setShowFactionlessToFactionless(toBool(value)); + case "worldmap.autoFallbackOnError" -> cfg.worldMap().setAutoFallbackOnError(toBool(value)); + case "worldmap.proximityChunkRadius" -> cfg.worldMap().setProximityChunkRadius(toInt(value)); + case "worldmap.proximityBatchIntervalTicks" -> cfg.worldMap().setProximityBatchIntervalTicks(toInt(value)); + case "worldmap.proximityMaxChunksPerBatch" -> cfg.worldMap().setProximityMaxChunksPerBatch(toInt(value)); + case "worldmap.incrementalBatchIntervalTicks" -> cfg.worldMap().setIncrementalBatchIntervalTicks(toInt(value)); + case "worldmap.incrementalMaxChunksPerBatch" -> cfg.worldMap().setIncrementalMaxChunksPerBatch(toInt(value)); + case "worldmap.debouncedDelaySeconds" -> cfg.worldMap().setDebouncedDelaySeconds(toInt(value)); + case "worldmap.factionWideRefreshThreshold" -> cfg.worldMap().setFactionWideRefreshThreshold(toInt(value)); + + // === DebugConfig === + case "debug.enabledByDefault" -> cfg.debug().setEnabledByDefault(toBool(value)); + case "debug.logToConsole" -> cfg.debug().setLogToConsole(toBool(value)); + case "debug.power" -> cfg.debug().setPower(toBool(value)); + case "debug.claim" -> cfg.debug().setClaim(toBool(value)); + case "debug.combat" -> cfg.debug().setCombat(toBool(value)); + case "debug.protection" -> cfg.debug().setProtection(toBool(value)); + case "debug.relation" -> cfg.debug().setRelation(toBool(value)); + case "debug.territory" -> cfg.debug().setTerritory(toBool(value)); + case "debug.worldmap" -> cfg.debug().setWorldmap(toBool(value)); + case "debug.interaction" -> cfg.debug().setInteraction(toBool(value)); + case "debug.mixin" -> cfg.debug().setMixin(toBool(value)); + case "debug.spawning" -> cfg.debug().setSpawning(toBool(value)); + case "debug.integration" -> cfg.debug().setIntegration(toBool(value)); + case "debug.economy" -> cfg.debug().setEconomy(toBool(value)); + case "debug.sentryEnabled" -> cfg.debug().setSentryEnabled(toBool(value)); + case "debug.sentryDebug" -> cfg.debug().setSentryDebug(toBool(value)); + case "debug.sentryTracesSampleRate" -> cfg.debug().setSentryTracesSampleRate(toDouble(value)); + + // === GravestoneConfig === + case "gravestone.protectInOwnTerritory" -> cfg.gravestones().setProtectInOwnTerritory(toBool(value)); + case "gravestone.factionMembersCanAccess" -> cfg.gravestones().setFactionMembersCanAccess(toBool(value)); + case "gravestone.alliesCanAccess" -> cfg.gravestones().setAlliesCanAccess(toBool(value)); + case "gravestone.protectInSafeZone" -> cfg.gravestones().setProtectInSafeZone(toBool(value)); + case "gravestone.protectInWarZone" -> cfg.gravestones().setProtectInWarZone(toBool(value)); + case "gravestone.protectInWilderness" -> cfg.gravestones().setProtectInWilderness(toBool(value)); + case "gravestone.announceDeathLocation" -> cfg.gravestones().setAnnounceDeathLocation(toBool(value)); + case "gravestone.protectInEnemyTerritory" -> cfg.gravestones().setProtectInEnemyTerritory(toBool(value)); + case "gravestone.protectInNeutralTerritory" -> cfg.gravestones().setProtectInNeutralTerritory(toBool(value)); + case "gravestone.enemiesCanLootInOwnTerritory" -> cfg.gravestones().setEnemiesCanLootInOwnTerritory(toBool(value)); + case "gravestone.allowLootDuringRaid" -> cfg.gravestones().setAllowLootDuringRaid(toBool(value)); + case "gravestone.allowLootDuringWar" -> cfg.gravestones().setAllowLootDuringWar(toBool(value)); + + // === WorldsConfig === + case "worlds.defaultPolicy" -> cfg.worlds().setDefaultPolicy(toStr(value)); + + // === FactionPermissionsConfig === + default -> { + if (key.startsWith("facperm.default.")) { + String flag = key.substring("facperm.default.".length()); + cfg.factionPermissions().setDefault(flag, toBool(value)); + } else if (key.startsWith("facperm.lock.")) { + String flag = key.substring("facperm.lock.".length()); + cfg.factionPermissions().setLocked(flag, toBool(value)); + } else { + Logger.warn("[ConfigEditor] Unknown setting key: %s", key); + } + } + } + } catch (Exception e) { + Logger.warn("[ConfigEditor] Failed to apply change for key '%s': %s", key, e.getMessage()); + } + } + + /** + * Returns the step size for an integer setting key. + * + * @param key the setting key + * @return the step size + */ + public static int getIntStep(String key) { + return switch (key) { + case "server.cooldownSeconds", "factions.stuckCooldownSeconds" -> 10; + case "server.leaderboardKdRefreshSeconds" -> 30; + case "chat.historyMaxMessages" -> 10; + case "worldmap.proximityBatchIntervalTicks", "worldmap.incrementalBatchIntervalTicks" -> 5; + case "worldmap.proximityMaxChunksPerBatch", "worldmap.incrementalMaxChunksPerBatch" -> 10; + case "worldmap.factionWideRefreshThreshold" -> 50; + default -> 1; + }; + } + + /** + * Returns the step size for a double setting key. + * + * @param key the setting key + * @return the step size + */ + public static double getDoubleStep(String key) { + return switch (key) { + case "factions.regenPerMinute", "debug.sentryTracesSampleRate" -> 0.1; + case "factions.powerPerClaim", "factions.deathPenalty", "factions.killReward", + "factions.logoutPowerLoss", "factions.neutralAttackPenalty" -> 0.5; + default -> 1.0; + }; + } + + private static boolean toBool(Object value) { + if (value instanceof Boolean b) return b; + return Boolean.parseBoolean(String.valueOf(value)); + } + + private static int toInt(Object value) { + if (value instanceof Number n) return n.intValue(); + return Integer.parseInt(String.valueOf(value)); + } + + private static double toDouble(Object value) { + if (value instanceof Number n) return n.doubleValue(); + return Double.parseDouble(String.valueOf(value)); + } + + private static String toStr(Object value) { + return String.valueOf(value); + } + + private static BigDecimal toBigDecimal(Object value) { + if (value instanceof BigDecimal bd) return bd; + if (value instanceof Number n) return BigDecimal.valueOf(n.doubleValue()); + return new BigDecimal(String.valueOf(value)); + } +} diff --git a/src/main/java/com/hyperfactions/gui/admin/ConfigValidator.java b/src/main/java/com/hyperfactions/gui/admin/ConfigValidator.java new file mode 100644 index 00000000..9d649e19 --- /dev/null +++ b/src/main/java/com/hyperfactions/gui/admin/ConfigValidator.java @@ -0,0 +1,207 @@ +package com.hyperfactions.gui.admin; + +/** + * Input validation for the admin config editor. + * Provides per-field min/max bounds and type checking. + */ +public final class ConfigValidator { + + private ConfigValidator() {} + + /** + * Parses and clamps an integer input string within bounds. + * + * @param input the raw string input + * @param current the current value (returned if input is invalid) + * @param min minimum allowed value + * @param max maximum allowed value + * @return the clamped integer value + */ + public static int clampInt(String input, int current, int min, int max) { + if (input == null || input.isBlank()) return current; + try { + int val = Integer.parseInt(input.trim()); + return Math.max(min, Math.min(max, val)); + } catch (NumberFormatException e) { + return current; + } + } + + /** + * Parses and clamps a double input string within bounds. + * + * @param input the raw string input + * @param current the current value (returned if input is invalid) + * @param min minimum allowed value + * @param max maximum allowed value + * @return the clamped double value + */ + public static double clampDouble(String input, double current, double min, double max) { + if (input == null || input.isBlank()) return current; + try { + double val = Double.parseDouble(input.trim()); + if (Double.isNaN(val) || Double.isInfinite(val)) return current; + val = Math.max(min, Math.min(max, val)); + return Math.round(val * 100.0) / 100.0; + } catch (NumberFormatException e) { + return current; + } + } + + /** + * Returns the min bound for an integer setting key. + */ + public static int getIntMin(String key) { + return switch (key) { + case "factions.maxMembers", "factions.minNameLength", "factions.maxNameLength" -> 1; + case "factions.maxClaims" -> 0; + case "factions.tagDurationSeconds", "factions.spawnProtectionDurationSeconds" -> 0; + case "server.warmupSeconds", "server.cooldownSeconds" -> 0; + case "server.autoSaveIntervalMinutes" -> 1; + case "server.mobClearIntervalSeconds" -> 5; + case "server.leaderboardKdRefreshSeconds" -> 30; + case "factions.stuckMinRadius", "factions.stuckRadiusIncrease", "factions.stuckMaxAttempts" -> 1; + case "factions.stuckWarmupSeconds", "factions.stuckCooldownSeconds" -> 0; + case "factions.inviteExpirationMinutes", "factions.joinRequestExpirationHours" -> 1; + case "factions.maxAllies", "factions.maxEnemies" -> -1; + case "factions.decayDaysInactive" -> 1; + case "factions.decayClaimsPerCycle" -> 1; + case "factions.maxMembershipHistory" -> 1; + case "backup.hourlyRetention", "backup.dailyRetention", "backup.weeklyRetention", + "backup.manualRetention", "backup.shutdownRetention" -> 0; + case "chat.historyMaxMessages" -> 10; + case "chat.historyRetentionDays" -> 1; + case "chat.historyCleanupIntervalMinutes" -> 1; + case "economy.upkeepIntervalHours" -> 1; + case "economy.upkeepGracePeriodHours" -> 0; + case "economy.upkeepFreeChunks" -> 0; + case "economy.upkeepClaimLossPerCycle" -> 1; + case "economy.upkeepWarningHours" -> 0; + case "economy.defaultLimitPeriodHours" -> 1; + case "worldmap.proximityChunkRadius" -> 1; + case "worldmap.proximityBatchIntervalTicks", "worldmap.incrementalBatchIntervalTicks" -> 1; + case "worldmap.proximityMaxChunksPerBatch", "worldmap.incrementalMaxChunksPerBatch" -> 1; + case "worldmap.debouncedDelaySeconds" -> 1; + case "worldmap.factionWideRefreshThreshold" -> 10; + default -> 0; + }; + } + + /** + * Returns the max bound for an integer setting key. + */ + public static int getIntMax(String key) { + return switch (key) { + case "factions.maxMembers" -> 1000; + case "factions.minNameLength" -> 32; + case "factions.maxNameLength" -> 64; + case "factions.maxClaims" -> 10000; + case "factions.tagDurationSeconds" -> 600; + case "factions.spawnProtectionDurationSeconds" -> 600; + case "server.warmupSeconds" -> 3600; + case "server.cooldownSeconds" -> 86400; + case "server.autoSaveIntervalMinutes" -> 60; + case "server.mobClearIntervalSeconds" -> 3600; + case "server.leaderboardKdRefreshSeconds" -> 86400; + case "factions.stuckMinRadius" -> 100; + case "factions.stuckRadiusIncrease" -> 50; + case "factions.stuckMaxAttempts" -> 100; + case "factions.stuckWarmupSeconds" -> 600; + case "factions.stuckCooldownSeconds" -> 86400; + case "factions.inviteExpirationMinutes" -> 1440; + case "factions.joinRequestExpirationHours" -> 168; + case "factions.maxAllies", "factions.maxEnemies" -> 100; + case "factions.decayDaysInactive" -> 365; + case "factions.decayClaimsPerCycle" -> 100; + case "factions.maxMembershipHistory" -> 100; + case "backup.hourlyRetention" -> 168; + case "backup.dailyRetention" -> 90; + case "backup.weeklyRetention" -> 52; + case "backup.manualRetention" -> 100; + case "backup.shutdownRetention" -> 100; + case "chat.historyMaxMessages" -> 10000; + case "chat.historyRetentionDays" -> 365; + case "chat.historyCleanupIntervalMinutes" -> 1440; + case "economy.upkeepIntervalHours" -> 168; + case "economy.upkeepGracePeriodHours" -> 720; + case "economy.upkeepFreeChunks" -> 1000; + case "economy.upkeepClaimLossPerCycle" -> 100; + case "economy.upkeepWarningHours" -> 168; + case "economy.defaultLimitPeriodHours" -> 720; + case "worldmap.proximityChunkRadius" -> 128; + case "worldmap.proximityBatchIntervalTicks", "worldmap.incrementalBatchIntervalTicks" -> 200; + case "worldmap.proximityMaxChunksPerBatch", "worldmap.incrementalMaxChunksPerBatch" -> 500; + case "worldmap.debouncedDelaySeconds" -> 60; + case "worldmap.factionWideRefreshThreshold" -> 10000; + default -> 999999; + }; + } + + /** + * Returns the min bound for a double setting key. + */ + public static double getDoubleMin(String key) { + return switch (key) { + case "factions.maxPlayerPower", "factions.startingPower", "factions.powerPerClaim" -> 0.0; + case "factions.deathPenalty", "factions.killReward" -> 0.0; + case "factions.logoutPowerLoss", "factions.neutralAttackPenalty" -> 0.0; + case "factions.regenPerMinute" -> 0.0; + case "debug.sentryTracesSampleRate" -> 0.0; + case "economy.startingBalance", "economy.upkeepCostPerChunk", "economy.upkeepMaxCostCap" -> 0.0; + case "economy.depositFeePercent", "economy.withdrawFeePercent", "economy.transferFeePercent" -> 0.0; + case "economy.defaultMaxWithdrawAmount", "economy.defaultMaxWithdrawPerPeriod", + "economy.defaultMaxTransferAmount", "economy.defaultMaxTransferPerPeriod" -> 0.0; + default -> 0.0; + }; + } + + /** + * Returns the max bound for a double setting key. + */ + public static double getDoubleMax(String key) { + return switch (key) { + case "factions.maxPlayerPower" -> 10000.0; + case "factions.startingPower" -> 10000.0; + case "factions.powerPerClaim" -> 1000.0; + case "factions.deathPenalty", "factions.killReward" -> 1000.0; + case "factions.logoutPowerLoss", "factions.neutralAttackPenalty" -> 1000.0; + case "factions.regenPerMinute" -> 100.0; + case "debug.sentryTracesSampleRate" -> 1.0; + case "economy.startingBalance" -> 999999999.0; + case "economy.upkeepCostPerChunk" -> 999999.0; + case "economy.upkeepMaxCostCap" -> 999999999.0; + case "economy.depositFeePercent", "economy.withdrawFeePercent", "economy.transferFeePercent" -> 100.0; + case "economy.defaultMaxWithdrawAmount", "economy.defaultMaxWithdrawPerPeriod", + "economy.defaultMaxTransferAmount", "economy.defaultMaxTransferPerPeriod" -> 999999999.0; + default -> 999999.0; + }; + } + + /** + * Validates a color hex string. Must match #RRGGBB format. + * + * @param input the raw input + * @param current the current value (returned if invalid) + * @return the validated color string + */ + public static String validateColor(String input, String current) { + if (input == null || input.isBlank()) return current; + String trimmed = input.trim(); + if (trimmed.matches("#[0-9A-Fa-f]{6}")) return trimmed; + return current; + } + + /** + * Validates a string input with max length. + * + * @param input the raw input + * @param maxLen maximum allowed length + * @return the validated string + */ + public static String validateString(String input, int maxLen) { + if (input == null) return ""; + String trimmed = input.strip(); + if (trimmed.length() > maxLen) return trimmed.substring(0, maxLen); + return trimmed; + } +} diff --git a/src/main/java/com/hyperfactions/gui/admin/data/AdminBackupsData.java b/src/main/java/com/hyperfactions/gui/admin/data/AdminBackupsData.java index bdb16c58..2aeb1af0 100644 --- a/src/main/java/com/hyperfactions/gui/admin/data/AdminBackupsData.java +++ b/src/main/java/com/hyperfactions/gui/admin/data/AdminBackupsData.java @@ -6,7 +6,7 @@ import org.jetbrains.annotations.Nullable; /** - * Event data for the Admin Backups page (placeholder). + * Event data for the Admin Backups page. */ public class AdminBackupsData implements AdminNavAwareData { @@ -16,6 +16,18 @@ public class AdminBackupsData implements AdminNavAwareData { /** Admin nav bar target (for navigation). */ public String adminNavBar; + /** Backup name for expand/restore/delete targets. */ + public String backupName; + + /** Dynamic text field value for manual backup name. */ + public String backupInputName; + + /** Page number for pagination (as String, parsed to int). */ + public int page; + + /** Filter dropdown value for type filtering. */ + public String filterValue; + /** Codec for serialization/deserialization. */ public static final BuilderCodec CODEC = BuilderCodec .builder(AdminBackupsData.class, AdminBackupsData::new) @@ -29,6 +41,32 @@ public class AdminBackupsData implements AdminNavAwareData { (data, value) -> data.adminNavBar = value, data -> data.adminNavBar ) + .addField( + new KeyedCodec<>("BackupName", Codec.STRING), + (data, value) -> data.backupName = value, + data -> data.backupName + ) + .addField( + new KeyedCodec<>("@BackupInputName", Codec.STRING), + (data, value) -> data.backupInputName = value, + data -> data.backupInputName + ) + .addField( + new KeyedCodec<>("Page", Codec.STRING), + (data, value) -> { + try { + data.page = Integer.parseInt(value); + } catch (NumberFormatException e) { + data.page = 0; + } + }, + data -> String.valueOf(data.page) + ) + .addField( + new KeyedCodec<>("@filterValue", Codec.STRING), + (data, value) -> data.filterValue = value, + data -> data.filterValue + ) .build(); /** Creates a new AdminBackupsData. */ diff --git a/src/main/java/com/hyperfactions/gui/admin/data/AdminConfigData.java b/src/main/java/com/hyperfactions/gui/admin/data/AdminConfigData.java index 4ec4edcc..2a3bc27a 100644 --- a/src/main/java/com/hyperfactions/gui/admin/data/AdminConfigData.java +++ b/src/main/java/com/hyperfactions/gui/admin/data/AdminConfigData.java @@ -6,7 +6,7 @@ import org.jetbrains.annotations.Nullable; /** - * Event data for the Admin Config page (placeholder). + * Event data for the Admin Config editor page. */ public class AdminConfigData implements AdminNavAwareData { @@ -16,6 +16,30 @@ public class AdminConfigData implements AdminNavAwareData { /** Admin nav bar target (for navigation). */ public String adminNavBar; + /** The tab being switched to. */ + public String tab; + + /** The setting key being changed. */ + public String settingKey; + + /** The setting value (for text input). */ + public String settingValue; + + /** Dynamic text input from text fields. */ + public String textInput; + + /** Dynamic numeric input from number text fields. */ + public String numInput; + + /** Dynamic enum/dropdown selection value. */ + public String enumValue; + + /** Dynamic string input from text fields. */ + public String strInput; + + /** Dynamic color value from ColorPickerDropdownBox. */ + public String colorValue; + /** Codec for serialization/deserialization. */ public static final BuilderCodec CODEC = BuilderCodec .builder(AdminConfigData.class, AdminConfigData::new) @@ -29,6 +53,46 @@ public class AdminConfigData implements AdminNavAwareData { (data, value) -> data.adminNavBar = value, data -> data.adminNavBar ) + .addField( + new KeyedCodec<>("Tab", Codec.STRING), + (data, value) -> data.tab = value, + data -> data.tab + ) + .addField( + new KeyedCodec<>("SettingKey", Codec.STRING), + (data, value) -> data.settingKey = value, + data -> data.settingKey + ) + .addField( + new KeyedCodec<>("SettingValue", Codec.STRING), + (data, value) -> data.settingValue = value, + data -> data.settingValue + ) + .addField( + new KeyedCodec<>("@textInput", Codec.STRING), + (data, value) -> data.textInput = value, + data -> data.textInput + ) + .addField( + new KeyedCodec<>("@numInput", Codec.STRING), + (data, value) -> data.numInput = value, + data -> data.numInput + ) + .addField( + new KeyedCodec<>("@enumValue", Codec.STRING), + (data, value) -> data.enumValue = value, + data -> data.enumValue + ) + .addField( + new KeyedCodec<>("@strInput", Codec.STRING), + (data, value) -> data.strInput = value, + data -> data.strInput + ) + .addField( + new KeyedCodec<>("@colorValue", Codec.STRING), + (data, value) -> data.colorValue = value, + data -> data.colorValue + ) .build(); /** Creates a new AdminConfigData. */ diff --git a/src/main/java/com/hyperfactions/gui/admin/data/ScalingTiersData.java b/src/main/java/com/hyperfactions/gui/admin/data/ScalingTiersData.java new file mode 100644 index 00000000..1861492b --- /dev/null +++ b/src/main/java/com/hyperfactions/gui/admin/data/ScalingTiersData.java @@ -0,0 +1,52 @@ +package com.hyperfactions.gui.admin.data; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; + +/** + * Event data for the Scaling Tiers modal page. + */ +public class ScalingTiersData { + + /** The button/action that triggered the event. */ + public String button; + + /** The tier index being acted on. */ + public String tierIndex; + + /** Dynamic chunk count input. */ + public String chunkInput; + + /** Dynamic cost input. */ + public String costInput; + + /** Codec for serialization/deserialization. */ + public static final BuilderCodec CODEC = BuilderCodec + .builder(ScalingTiersData.class, ScalingTiersData::new) + .addField( + new KeyedCodec<>("Button", Codec.STRING), + (data, value) -> data.button = value, + data -> data.button + ) + .addField( + new KeyedCodec<>("TierIndex", Codec.STRING), + (data, value) -> data.tierIndex = value, + data -> data.tierIndex + ) + .addField( + new KeyedCodec<>("@chunkInput", Codec.STRING), + (data, value) -> data.chunkInput = value, + data -> data.chunkInput + ) + .addField( + new KeyedCodec<>("@costInput", Codec.STRING), + (data, value) -> data.costInput = value, + data -> data.costInput + ) + .build(); + + /** Creates a new ScalingTiersData. */ + public ScalingTiersData() { + } +} diff --git a/src/main/java/com/hyperfactions/gui/admin/page/AdminBackupsPage.java b/src/main/java/com/hyperfactions/gui/admin/page/AdminBackupsPage.java index d6cfd352..947aa70c 100644 --- a/src/main/java/com/hyperfactions/gui/admin/page/AdminBackupsPage.java +++ b/src/main/java/com/hyperfactions/gui/admin/page/AdminBackupsPage.java @@ -1,53 +1,256 @@ package com.hyperfactions.gui.admin.page; +import com.hyperfactions.HyperFactions; +import com.hyperfactions.backup.BackupManager; +import com.hyperfactions.backup.BackupMetadata; +import com.hyperfactions.backup.BackupType; import com.hyperfactions.gui.GuiManager; import com.hyperfactions.gui.UIPaths; import com.hyperfactions.gui.admin.AdminNavBarHelper; import com.hyperfactions.gui.admin.data.AdminBackupsData; -import com.hyperfactions.util.HFMessages; import com.hyperfactions.util.AdminGuiKeys; +import com.hyperfactions.util.HFMessages; +import com.hyperfactions.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.DropdownEntryInfo; +import com.hypixel.hytale.server.core.ui.LocalizableString; +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 java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import org.jetbrains.annotations.Nullable; /** - * Admin Backups page - placeholder for backup management. + * Admin Backups page - lists, creates, restores, and deletes faction data backups. */ public class AdminBackupsPage extends InteractiveCustomUIPage { + private static final int BACKUPS_PER_PAGE = 8; + private final PlayerRef playerRef; private final GuiManager guiManager; + private final HyperFactions plugin; + + private int currentPage = 0; + + private final Set expandedBackups = new HashSet<>(); + + private String confirmingRestore = null; + + private String confirmingDelete = null; + + private String statusMessage = ""; + + private boolean creating = false; + + /** Current filter type — null means "All". */ + @Nullable + private BackupType filterType = null; + /** Creates a new AdminBackupsPage. */ - public AdminBackupsPage(PlayerRef playerRef, GuiManager guiManager) { + public AdminBackupsPage(PlayerRef playerRef, GuiManager guiManager, HyperFactions plugin) { super(playerRef, CustomPageLifetime.CanDismiss, AdminBackupsData.CODEC); this.playerRef = playerRef; this.guiManager = guiManager; + this.plugin = plugin; } - /** Builds . */ + /** Builds the backups page. */ @Override public void build(Ref ref, UICommandBuilder cmd, UIEventBuilder events, Store store) { - // Load the placeholder template first (nav bar elements must exist before setupBar) cmd.append(UIPaths.ADMIN_BACKUPS); - // Setup admin nav bar (must be after template load) AdminNavBarHelper.setupBar(playerRef, "backups", cmd, events); - // Localize page title and labels + // Static labels — set once, persist across sendUpdate(false) calls cmd.set("#PageTitle.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_TITLE_BACKUPS)); - cmd.set("#ComingSoon.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_BACKUP_HEADING)); - cmd.set("#ComingSoonSub.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_COMING_SOON)); - cmd.set("#Description.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_BACKUP_DESC1)); - cmd.set("#Description2.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_BACKUP_DESC2)); + cmd.set("#NameLabel.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_NAME_PLACEHOLDER)); + + // Dynamic content — changes on each rebuild + buildBackupList(cmd, events); + } + + /** Builds (or rebuilds) all dynamic content: count, status, backup entries, pagination. */ + private void buildBackupList(UICommandBuilder cmd, UIEventBuilder events) { + BackupManager manager = plugin.getBackupManager(); + List allBackups = manager != null ? manager.listBackups() : List.of(); + + // Apply filter + List filteredBackups = filterType == null ? allBackups + : allBackups.stream().filter(b -> b.type() == filterType).toList(); + + // Filter dropdown + cmd.set("#FilterDropdown.Entries", List.of( + new DropdownEntryInfo(LocalizableString.fromString("All"), "all"), + new DropdownEntryInfo(LocalizableString.fromString("Hourly"), "hourly"), + new DropdownEntryInfo(LocalizableString.fromString("Daily"), "daily"), + new DropdownEntryInfo(LocalizableString.fromString("Weekly"), "weekly"), + new DropdownEntryInfo(LocalizableString.fromString("Manual"), "manual"), + new DropdownEntryInfo(LocalizableString.fromString("Migration"), "migration") + )); + cmd.set("#FilterDropdown.Value", filterType == null ? "all" : filterType.getPrefix()); + events.addEventBinding(CustomUIEventBindingType.ValueChanged, "#FilterDropdown", + EventData.of("Button", "FilterChanged") + .append("@filterValue", "#FilterDropdown.Value"), false); + + // Header with count + String countText = filterType == null + ? HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_TOTAL_COUNT, allBackups.size()) + : HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_TOTAL_COUNT, filteredBackups.size()) + + " / " + allBackups.size(); + cmd.set("#BackupCount.Text", countText); + + // Create button + cmd.set("#CreateBackupBtn.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_BTN_CREATE)); + if (!creating) { + events.addEventBinding(CustomUIEventBindingType.Activating, "#CreateBackupBtn", + EventData.of("Button", "Create") + .append("@BackupInputName", "#BackupNameInput.Value"), false); + } + + // Status message + cmd.set("#StatusMessage.Text", statusMessage); + + // Pagination calculation + int totalPages = Math.max(1, (int) Math.ceil((double) filteredBackups.size() / BACKUPS_PER_PAGE)); + if (currentPage >= totalPages) { + currentPage = totalPages - 1; + } + if (currentPage < 0) { + currentPage = 0; + } + + int start = currentPage * BACKUPS_PER_PAGE; + int end = Math.min(start + BACKUPS_PER_PAGE, filteredBackups.size()); + List pageBackups = filteredBackups.subList(start, end); + + // Clear and rebuild backup list using IndexCards pattern + cmd.clear("#BackupListContainer"); + + if (pageBackups.isEmpty()) { + cmd.appendInline("#BackupListContainer", "Label { Text: \"" + + HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_EMPTY) + + "\"; Style: (FontSize: 11, TextColor: #666666, HorizontalAlignment: Center); Anchor: (Height: 40); }"); + } else { + cmd.appendInline("#BackupListContainer", "Group #IndexCards { LayoutMode: Top; }"); + for (int i = 0; i < pageBackups.size(); i++) { + buildBackupEntry(cmd, events, i, pageBackups.get(i)); + } + } + + // Pagination controls + cmd.set("#PageLabel.Text", totalPages > 1 + ? (currentPage + 1) + " / " + totalPages : ""); + + cmd.set("#PrevBtn.Visible", currentPage > 0); + if (currentPage > 0) { + events.addEventBinding(CustomUIEventBindingType.Activating, "#PrevBtn", + EventData.of("Button", "PrevPage").append("Page", String.valueOf(currentPage - 1)), false); + } + + cmd.set("#NextBtn.Visible", currentPage < totalPages - 1); + if (currentPage < totalPages - 1) { + events.addEventBinding(CustomUIEventBindingType.Activating, "#NextBtn", + EventData.of("Button", "NextPage").append("Page", String.valueOf(currentPage + 1)), false); + } + } + + private void buildBackupEntry(UICommandBuilder cmd, UIEventBuilder events, + int index, BackupMetadata backup) { + String name = backup.name(); + boolean expanded = expandedBackups.contains(name); + + cmd.append("#IndexCards", UIPaths.ADMIN_BACKUP_ENTRY); + String idx = "#IndexCards[" + index + "]"; + + // Header info via indexed child selectors + cmd.set(idx + " #BackupNameLabel.Text", formatBackupName(name)); + cmd.set(idx + " #BackupSizeLabel.Text", backup.getFormattedSize()); + cmd.set(idx + " #BackupTypeTag.Text", getTypeLabel(backup.type())); + + // Expand/collapse button + cmd.set(idx + " #ExpandBtn.Text", expanded ? "v" : ">"); + events.addEventBinding(CustomUIEventBindingType.Activating, idx + " #ExpandBtn", + EventData.of("Button", "Toggle").append("BackupName", name), false); + + if (expanded) { + cmd.set(idx + " #DetailSection.Visible", true); + cmd.set(idx + " #DetailTypeLabel.Text", + HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_DETAIL_TYPE) + " " + getTypeLabel(backup.type())); + cmd.set(idx + " #DetailCreatedLabel.Text", + HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_DETAIL_CREATED) + " " + backup.getFormattedTimestamp()); + cmd.set(idx + " #DetailSizeLabel.Text", + HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_DETAIL_SIZE) + " " + backup.getFormattedSize()); + + // Restore button + if (name.equals(confirmingRestore)) { + cmd.set(idx + " #RestoreWarning.Visible", true); + cmd.set(idx + " #RestoreWarning.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_RESTORE_WARNING)); + cmd.set(idx + " #RestoreBtn.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_RESTORE_CONFIRM)); + } else { + cmd.set(idx + " #RestoreBtn.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_BTN_RESTORE)); + } + events.addEventBinding(CustomUIEventBindingType.Activating, idx + " #RestoreBtn", + EventData.of("Button", "Restore").append("BackupName", name), false); + + // Delete button + if (name.equals(confirmingDelete)) { + cmd.set(idx + " #DeleteBtn.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_DELETE_CONFIRM)); + } else { + cmd.set(idx + " #DeleteBtn.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_BTN_DELETE)); + } + events.addEventBinding(CustomUIEventBindingType.Activating, idx + " #DeleteBtn", + EventData.of("Button", "Delete").append("BackupName", name), false); + } + } + + private String formatBackupName(String name) { + // Remove "backup_" prefix for cleaner display + if (name.startsWith("backup_")) { + return name.substring(7); + } + return name; + } + + private String getTypeLabel(BackupType type) { + return switch (type) { + case HOURLY -> HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_TYPE_HOURLY); + case DAILY -> HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_TYPE_DAILY); + case WEEKLY -> HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_TYPE_WEEKLY); + case MANUAL -> HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_TYPE_MANUAL); + case MIGRATION -> HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_TYPE_MIGRATION); + }; + } + + @Nullable + private World resolveWorld() { + UUID worldUuid = playerRef.getWorldUuid(); + if (worldUuid == null) { + return null; + } + return Universe.get().getWorld(worldUuid); + } + + private void rebuildOnWorldThread() { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + buildBackupList(cmd, events); + sendUpdate(cmd, events, false); } /** Handles data event. */ @@ -63,17 +266,181 @@ public void handleDataEvent(Ref ref, Store store, return; } - // Handle admin nav bar navigation if (AdminNavBarHelper.handleNavEvent(data, player, ref, store, playerRef, guiManager)) { return; } - // Handle other button events (placeholder for future implementation) - if (data.button != null) { - switch (data.button) { - case "Back" -> guiManager.closePage(player, ref, store); - default -> throw new IllegalStateException("Unexpected value"); + if (data.button == null) { + return; + } + + switch (data.button) { + case "Create" -> handleCreate(data); + case "Toggle" -> handleToggle(data); + case "Restore" -> handleRestore(data); + case "Delete" -> handleDelete(data); + case "PrevPage", "NextPage" -> { + currentPage = data.page; + expandedBackups.clear(); + confirmingRestore = null; + confirmingDelete = null; + rebuildOnWorldThread(); + } + case "FilterChanged" -> { + if (data.filterValue != null) { + filterType = "all".equals(data.filterValue) ? null : BackupType.fromPrefix(data.filterValue); + currentPage = 0; + expandedBackups.clear(); + confirmingRestore = null; + confirmingDelete = null; + rebuildOnWorldThread(); + } } + case "Back" -> guiManager.closePage(player, ref, store); + default -> { } + } + } + + private void handleCreate(AdminBackupsData data) { + if (creating) { + return; } + + BackupManager manager = plugin.getBackupManager(); + if (manager == null) { + statusMessage = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_CREATE_FAILED); + rebuildOnWorldThread(); + return; + } + + creating = true; + String customName = data.backupInputName; + statusMessage = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_CREATING); + rebuildOnWorldThread(); + + manager.createBackup(BackupType.MANUAL, customName, playerRef.getUuid()).thenAccept(result -> { + World world = resolveWorld(); + if (world == null) { + creating = false; + return; + } + world.execute(() -> { + creating = false; + if (result instanceof BackupManager.BackupResult.Success success) { + statusMessage = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_CREATED, + success.metadata().name()); + Logger.info("[Backups] %s created manual backup: %s", + playerRef.getUsername(), success.metadata().name()); + } else if (result instanceof BackupManager.BackupResult.Failure failure) { + statusMessage = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_CREATE_FAILED) + + ": " + failure.error(); + } + rebuildOnWorldThread(); + }); + }); + } + + private void handleToggle(AdminBackupsData data) { + if (data.backupName == null) { + return; + } + if (expandedBackups.contains(data.backupName)) { + expandedBackups.remove(data.backupName); + } else { + expandedBackups.add(data.backupName); + } + // Reset confirm states when toggling + confirmingRestore = null; + confirmingDelete = null; + rebuildOnWorldThread(); + } + + private void handleRestore(AdminBackupsData data) { + if (data.backupName == null) { + return; + } + + BackupManager manager = plugin.getBackupManager(); + if (manager == null) { + return; + } + + // First click — ask for confirmation + if (!data.backupName.equals(confirmingRestore)) { + confirmingRestore = data.backupName; + confirmingDelete = null; + rebuildOnWorldThread(); + return; + } + + // Second click — confirmed, create safety backup first then restore + confirmingRestore = null; + statusMessage = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_RESTORING); + rebuildOnWorldThread(); + + // Safety backup before restore + manager.createBackup(BackupType.MANUAL, "pre-restore-safety", playerRef.getUuid()) + .thenCompose(safetyResult -> manager.restoreBackup(data.backupName)) + .thenAccept(result -> { + World world = resolveWorld(); + if (world == null) { + return; + } + world.execute(() -> { + if (result instanceof BackupManager.RestoreResult.Success success) { + statusMessage = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_RESTORED, + success.backupName(), success.filesRestored()); + Logger.info("[Backups] %s restored backup: %s (%d files)", + playerRef.getUsername(), success.backupName(), success.filesRestored()); + // Append reload note + statusMessage += " " + HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_RELOAD_REQUIRED); + } else if (result instanceof BackupManager.RestoreResult.Failure failure) { + statusMessage = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_RESTORE_FAILED) + + ": " + failure.error(); + } + rebuildOnWorldThread(); + }); + }); + } + + private void handleDelete(AdminBackupsData data) { + if (data.backupName == null) { + return; + } + + BackupManager manager = plugin.getBackupManager(); + if (manager == null) { + return; + } + + // First click — ask for confirmation + if (!data.backupName.equals(confirmingDelete)) { + confirmingDelete = data.backupName; + confirmingRestore = null; + rebuildOnWorldThread(); + return; + } + + // Second click — confirmed + confirmingDelete = null; + statusMessage = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_DELETING); + rebuildOnWorldThread(); + + manager.deleteBackup(data.backupName).thenAccept(success -> { + World world = resolveWorld(); + if (world == null) { + return; + } + world.execute(() -> { + if (success) { + statusMessage = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_DELETED, data.backupName); + expandedBackups.remove(data.backupName); + Logger.info("[Backups] %s deleted backup: %s", playerRef.getUsername(), data.backupName); + } else { + statusMessage = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.BKP_DELETE_FAILED); + } + rebuildOnWorldThread(); + }); + }); } } diff --git a/src/main/java/com/hyperfactions/gui/admin/page/AdminConfigPage.java b/src/main/java/com/hyperfactions/gui/admin/page/AdminConfigPage.java index a7b453a3..582bb064 100644 --- a/src/main/java/com/hyperfactions/gui/admin/page/AdminConfigPage.java +++ b/src/main/java/com/hyperfactions/gui/admin/page/AdminConfigPage.java @@ -1,79 +1,1749 @@ package com.hyperfactions.gui.admin.page; +import com.hyperfactions.config.ConfigManager; +import com.hyperfactions.config.modules.*; +import com.hyperfactions.data.FactionPermissions; import com.hyperfactions.gui.GuiManager; import com.hyperfactions.gui.UIPaths; import com.hyperfactions.gui.admin.AdminNavBarHelper; +import com.hyperfactions.gui.admin.ConfigSnapshot; +import com.hyperfactions.gui.admin.ConfigValidator; import com.hyperfactions.gui.admin.data.AdminConfigData; -import com.hyperfactions.util.HFMessages; import com.hyperfactions.util.AdminGuiKeys; +import com.hyperfactions.util.HFMessages; +import com.hyperfactions.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.server.core.entity.entities.Player; import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; +import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; +import com.hypixel.hytale.server.core.ui.DropdownEntryInfo; +import com.hypixel.hytale.server.core.ui.LocalizableString; +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 java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; /** - * Admin Config page - placeholder for configuration management. + * Admin Config Editor page with 11 tabbed sections and two-column layout. + * + *

+ * Allows admins to view and edit all HyperFactions configuration settings + * directly from an in-game GUI. Changes are tracked as pending until saved. + * Uses targeted updates (no full page rebuild) for toggle/increment/text changes. */ public class AdminConfigPage extends InteractiveCustomUIPage { - private final PlayerRef playerRef; + // ================================================================ + // Edit Session Cache + // ================================================================ + + /** Cached edit sessions per player — survives page close/reopen. */ + private static final ConcurrentHashMap editSessions = new ConcurrentHashMap<>(); + + /** Snapshot of pending edits that survives page close/reopen. */ + private record EditSession( + String tab, + Map pendingChanges, + Map originalValues, + LinkedHashMap pendingWorldOverrides + ) {} + + /** Removes the cached edit session for a player. */ + public static void clearSession(UUID playerId) { + editSessions.remove(playerId); + } + + private static final String[] TABS = { + "server", "chat", "announcements", "economy", "factions", + "factionPerms", "worldmap", "worlds", "backup", "debug", "gravestones" + }; + + private static final String[] TAB_KEYS = { + AdminGuiKeys.AdminGui.CFG_TAB_SERVER, + AdminGuiKeys.AdminGui.CFG_TAB_CHAT, + AdminGuiKeys.AdminGui.CFG_TAB_ANNOUNCEMENTS, + AdminGuiKeys.AdminGui.CFG_TAB_ECONOMY, + AdminGuiKeys.AdminGui.CFG_TAB_FACTIONS, + AdminGuiKeys.AdminGui.CFG_TAB_FACTION_PERMS, + AdminGuiKeys.AdminGui.CFG_TAB_WORLDMAP, + AdminGuiKeys.AdminGui.CFG_TAB_WORLDS, + AdminGuiKeys.AdminGui.CFG_TAB_BACKUP, + AdminGuiKeys.AdminGui.CFG_TAB_DEBUG, + AdminGuiKeys.AdminGui.CFG_TAB_GRAVESTONES + }; + + /** Setting type for targeted updates. */ + private enum SettingKind { BOOL, INT, DOUBLE, STRING, COLOR, ENUM } + + /** Layout size for template selection. */ + private enum LayoutSize { NARROW, STANDARD, WIDE } + private static LayoutSize getLayoutSize(String tab) { + return switch (tab) { + case "backup", "gravestones", "worlds" -> LayoutSize.NARROW; + case "factions", "factionPerms" -> LayoutSize.WIDE; + default -> LayoutSize.STANDARD; + }; + } + + private final PlayerRef playerRef; private final GuiManager guiManager; + private String currentTab = "server"; + private final ConcurrentHashMap pendingChanges = new ConcurrentHashMap<>(); + private final ConcurrentHashMap originalValues = new ConcurrentHashMap<>(); + private boolean saveConfirmActive = false; + private boolean resetConfirmActive = false; + private final java.util.Set expandedWorlds = ConcurrentHashMap.newKeySet(); + + /** Fields with invalid input that must be fixed before saving. */ + private final java.util.Set invalidFields = ConcurrentHashMap.newKeySet(); + + /** Pending world overrides map — null means not yet modified from config. */ + private java.util.LinkedHashMap pendingWorldOverrides = null; + + /** Maps settingKey → container selector (e.g. "#LeftContainer[3]") for targeted updates. */ + private final Map settingSelectors = new HashMap<>(); + + /** Maps settingKey → its SettingKind for targeted updates. */ + private final Map settingKinds = new HashMap<>(); + + /** Debounce timestamp for text input — only the latest scheduled refresh runs. */ + private static final long DEBOUNCE_MS = 600; + private volatile long lastTextInputNanos = 0; + + /** Row counter for indexed selectors. Reset each build cycle. */ + private int leftRowIdx; + private int rightRowIdx; + private boolean appendingToLeft = true; + + /** 4-column mode for fac perms tab. */ + private boolean fourColumnMode = false; + private int[] colRowIdx = new int[4]; + private int activeCol = 0; + /** Creates a new AdminConfigPage. */ public AdminConfigPage(PlayerRef playerRef, GuiManager guiManager) { + this(playerRef, guiManager, "server"); + } + + /** Creates a new AdminConfigPage opened to a specific tab. */ + public AdminConfigPage(PlayerRef playerRef, GuiManager guiManager, String initialTab) { super(playerRef, CustomPageLifetime.CanDismiss, AdminConfigData.CODEC); this.playerRef = playerRef; this.guiManager = guiManager; + this.currentTab = resolveTab(initialTab); + restoreSession(); + } + + /** Restores cached edit session if one exists for this player. */ + private void restoreSession() { + EditSession session = editSessions.remove(playerRef.getUuid()); + if (session != null) { + this.currentTab = session.tab(); + this.pendingChanges.putAll(session.pendingChanges()); + this.originalValues.putAll(session.originalValues()); + this.pendingWorldOverrides = session.pendingWorldOverrides(); + } + } + + /** Saves current edit session to cache if there are pending changes. */ + private void saveSession() { + boolean hasChanges = !pendingChanges.isEmpty() || pendingWorldOverrides != null; + if (hasChanges) { + editSessions.put(playerRef.getUuid(), new EditSession( + currentTab, + new HashMap<>(pendingChanges), + new HashMap<>(originalValues), + pendingWorldOverrides != null ? new LinkedHashMap<>(pendingWorldOverrides) : null + )); + } else { + editSessions.remove(playerRef.getUuid()); + } + } + + /** Resolves a tab name or alias to a valid tab ID. */ + private static String resolveTab(String input) { + if (input == null) return "server"; + return switch (input.toLowerCase()) { + case "server", "srv" -> "server"; + case "chat" -> "chat"; + case "announcements", "announce", "ann" -> "announcements"; + case "economy", "eco" -> "economy"; + case "factions", "faction", "fac" -> "factions"; + case "factionperms", "facperms", "perms", "permissions" -> "factionPerms"; + case "worldmap", "map" -> "worldmap"; + case "worlds", "world" -> "worlds"; + case "backup", "backups" -> "backup"; + case "debug", "dbg" -> "debug"; + case "gravestones", "graves", "grave" -> "gravestones"; + default -> "server"; + }; } - /** Builds . */ @Override public void build(Ref ref, UICommandBuilder cmd, - UIEventBuilder events, Store store) { - // Load the placeholder template first (nav bar elements must exist before setupBar) - cmd.append(UIPaths.ADMIN_CONFIG); - - // Setup admin nav bar (must be after template load) + UIEventBuilder events, Store store) { + String template = switch (getLayoutSize(currentTab)) { + case NARROW -> UIPaths.ADMIN_CONFIG_NARROW; + case STANDARD -> UIPaths.ADMIN_CONFIG_STANDARD; + case WIDE -> UIPaths.ADMIN_CONFIG_WIDE; + }; + cmd.append(template); AdminNavBarHelper.setupBar(playerRef, "config", cmd, events); - // Localize page title and labels - cmd.set("#PageTitle.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_TITLE_CONFIG)); - cmd.set("#ComingSoon.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_CONFIG_HEADING)); - cmd.set("#ComingSoonSub.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_COMING_SOON)); - cmd.set("#Description.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_CONFIG_DESC1)); - cmd.set("#Description2.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_CONFIG_DESC2)); + cmd.set("#SaveBtn.Text", loc(AdminGuiKeys.AdminGui.CFG_BTN_SAVE)); + cmd.set("#RevertBtn.Text", loc(AdminGuiKeys.AdminGui.CFG_BTN_REVERT)); + cmd.set("#ResetBtn.Text", loc(AdminGuiKeys.AdminGui.CFG_BTN_RESET)); + + buildDynamicContent(cmd, events); + } + + /** Full rebuild of all dynamic content: tabs, settings, status, action bindings. */ + private void buildDynamicContent(UICommandBuilder cmd, UIEventBuilder events) { + for (int i = 0; i < TABS.length; i++) { + String tab = TABS[i]; + String tabId = "#Tab" + tab.substring(0, 1).toUpperCase() + tab.substring(1); + events.addEventBinding(CustomUIEventBindingType.Activating, tabId, + EventData.of("Button", "TabSwitch").append("Tab", tab), false); + } + + updatePageTitle(cmd); + updateStatusLabel(cmd); + + // Determine layout mode from current template + fourColumnMode = getLayoutSize(currentTab) == LayoutSize.WIDE; + + // Clear only the containers that exist in the current template + switch (getLayoutSize(currentTab)) { + case NARROW -> cmd.clear("#LeftContainer"); + case STANDARD -> { cmd.clear("#LeftContainer"); cmd.clear("#RightContainer"); } + case WIDE -> { cmd.clear("#Col1Container"); cmd.clear("#Col2Container"); + cmd.clear("#Col3Container"); cmd.clear("#Col4Container"); } + } + leftRowIdx = 0; + rightRowIdx = 0; + appendingToLeft = true; + colRowIdx = new int[4]; + activeCol = 0; + settingSelectors.clear(); + settingKinds.clear(); + buildTabContent(cmd, events); + + events.addEventBinding(CustomUIEventBindingType.Activating, "#SaveBtn", + EventData.of("Button", "Save"), false); + events.addEventBinding(CustomUIEventBindingType.Activating, "#RevertBtn", + EventData.of("Button", "Revert"), false); + events.addEventBinding(CustomUIEventBindingType.Activating, "#ResetBtn", + EventData.of("Button", "ResetDefaults"), false); + + // Save button: confirm state + disable when invalid + if (!invalidFields.isEmpty()) { + cmd.set("#SaveBtn.Disabled", true); + } + if (saveConfirmActive) { + cmd.set("#SaveBtn.Text", "Confirm Save"); + } else { + cmd.set("#SaveBtn.Text", loc(AdminGuiKeys.AdminGui.CFG_BTN_SAVE)); + } + + if (resetConfirmActive) { + cmd.set("#ResetBtn.Text", loc(AdminGuiKeys.AdminGui.CFG_RESET_CONFIRM)); + } else { + cmd.set("#ResetBtn.Text", loc(AdminGuiKeys.AdminGui.CFG_BTN_RESET)); + } + } + + private void updatePageTitle(UICommandBuilder cmd) { + String tabName = switch (currentTab) { + case "server" -> "Server"; + case "chat" -> "Chat"; + case "announcements" -> "Announcements"; + case "economy" -> "Economy"; + case "factions" -> "Factions"; + case "factionPerms" -> "Faction Perms"; + case "worldmap" -> "Worldmap"; + case "worlds" -> "Worlds"; + case "backup" -> "Backup"; + case "debug" -> "Debug"; + case "gravestones" -> "Gravestones"; + default -> currentTab; + }; + cmd.set("#PageTitle.Text", "Config: " + tabName); + } + + private void updateStatusLabel(UICommandBuilder cmd) { + int errorCount = invalidFields.size(); + int changeCount = pendingChanges.size() + (pendingWorldOverrides != null ? 1 : 0); + if (errorCount > 0) { + cmd.set("#StatusLabel.Text", errorCount + " invalid"); + cmd.set("#StatusLabel.Style.TextColor", "#FF4444"); + } else if (changeCount > 0) { + cmd.set("#StatusLabel.Text", changeCount + " " + loc(AdminGuiKeys.AdminGui.CFG_CHANGES_PENDING)); + cmd.set("#StatusLabel.Style.TextColor", "#FFAA00"); + } else { + cmd.set("#StatusLabel.Text", loc(AdminGuiKeys.AdminGui.CFG_NO_CHANGES)); + cmd.set("#StatusLabel.Style.TextColor", "#888888"); + } + } + + /** + * Targeted update: just update the label color for a single setting + status bar. + * No full page rebuild, no clearing containers, no re-binding events. + */ + private void updateSettingAndStatus(Ref ref, Store store, String key) { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + + String selector = settingSelectors.get(key); + if (selector != null) { + boolean pending = pendingChanges.containsKey(key); + String color = pending ? "#FFAA00" : "#CCCCCC"; + cmd.set(selector + " #SettingLabel.Style.TextColor", color); + + SettingKind kind = settingKinds.get(key); + + // Update the value display for types where the UI doesn't self-update + if (kind == SettingKind.INT) { + Object orig = originalValues.get(key); + int val = pending ? ((Number) pendingChanges.get(key)).intValue() + : (orig instanceof Number n ? n.intValue() : 0); + cmd.set(selector + " #NumInput.Value", String.valueOf(val)); + cmd.set(selector + " #NumInput.Style.TextColor", pending ? "#FFAA00" : "#FFFFFF"); + } else if (kind == SettingKind.DOUBLE) { + Object orig = originalValues.get(key); + double val = pending ? ((Number) pendingChanges.get(key)).doubleValue() + : (orig instanceof Number n ? n.doubleValue() : 0.0); + cmd.set(selector + " #NumInput.Value", String.format("%.2f", val)); + cmd.set(selector + " #NumInput.Style.TextColor", pending ? "#FFAA00" : "#FFFFFF"); + } + // BOOL: checkbox handles its own visual toggle + // ENUM: dropdown handles its own visual update + // STRING: text field has user input already, don't overwrite + if (kind == SettingKind.COLOR) { + Object orig = originalValues.get(key); + String val = pending ? String.valueOf(pendingChanges.get(key)) + : (orig instanceof String s ? s : "#FFFFFF"); + cmd.set(selector + " #ColorInput.Value", val); + cmd.set(selector + " #ColorInput.Style.TextColor", pending ? "#FFAA00" : "#FFFFFF"); + cmd.set(selector + " #ColorPicker.Color", val); + } + } + + updateStatusLabel(cmd); + sendUpdate(cmd, events, false); + } + + private void buildTabContent(UICommandBuilder cmd, UIEventBuilder events) { + ConfigManager cfg = ConfigManager.get(); + switch (currentTab) { + case "server" -> buildServerTab(cmd, events, cfg); + case "chat" -> buildChatTab(cmd, events, cfg); + case "announcements" -> buildAnnouncementsTab(cmd, events, cfg); + case "economy" -> buildEconomyTab(cmd, events, cfg); + case "factions" -> buildFactionsTab(cmd, events, cfg); + case "factionPerms" -> buildFactionPermsTab(cmd, events, cfg); + case "worldmap" -> buildWorldmapTab(cmd, events, cfg); + case "worlds" -> buildWorldsTab(cmd, events, cfg); + case "backup" -> buildBackupTab(cmd, events, cfg); + case "debug" -> buildDebugTab(cmd, events, cfg); + case "gravestones" -> buildGravestonesTab(cmd, events, cfg); + } + } + + // ================================================================ + // Column Management + // ================================================================ + + private void setColumn(boolean left) { + this.appendingToLeft = left; + } + + /** Sets the active column in 4-column mode (0-3). */ + private void setCol(int col) { + this.activeCol = col; + } + + private String getContainerId() { + if (fourColumnMode) { + return "#Col" + (activeCol + 1) + "Container"; + } + return appendingToLeft ? "#LeftContainer" : "#RightContainer"; + } + + private int getRowIdx() { + if (fourColumnMode) { + return colRowIdx[activeCol]; + } + return appendingToLeft ? leftRowIdx : rightRowIdx; + } + + private void incrementRowIdx() { + if (fourColumnMode) { + colRowIdx[activeCol]++; + } else if (appendingToLeft) { + leftRowIdx++; + } else { + rightRowIdx++; + } + } + + // ================================================================ + // Server Tab + // ================================================================ + + private void buildServerTab(UICommandBuilder cmd, UIEventBuilder events, ConfigManager cfg) { + setColumn(true); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_TELEPORT)); + addIntSetting(cmd, events, "server.warmupSeconds", "Warmup Seconds", cfg.getWarmupSeconds()); + addIntSetting(cmd, events, "server.cooldownSeconds", "Cooldown Seconds", cfg.getCooldownSeconds()); + addBooleanSetting(cmd, events, "server.cancelOnMove", "Cancel on Move", cfg.isCancelOnMove()); + addBooleanSetting(cmd, events, "server.cancelOnDamage", "Cancel on Damage", cfg.isCancelOnDamage()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_AUTOSAVE)); + addBooleanSetting(cmd, events, "server.autoSaveEnabled", "Auto-Save Enabled", cfg.isAutoSaveEnabled()); + addIntSetting(cmd, events, "server.autoSaveIntervalMinutes", "Save Interval (min)", cfg.getAutoSaveIntervalMinutes()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_MESSAGES)); + addStringSetting(cmd, events, "server.prefixText", "Prefix Text", cfg.getPrefixText()); + addColorSetting(cmd, events, "server.prefixColor", "Prefix Color", cfg.getPrefixColor()); + addColorSetting(cmd, events, "server.prefixBracketColor", "Bracket Color", cfg.server().getPrefixBracketColor()); + addColorSetting(cmd, events, "server.primaryColor", "Primary Color", cfg.getPrimaryColor()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_MOB_CLEAR)); + addBooleanSetting(cmd, events, "server.mobClearEnabled", "Mob Clear Enabled", cfg.isMobClearEnabled()); + addIntSetting(cmd, events, "server.mobClearIntervalSeconds", "Clear Interval (sec)", cfg.getMobClearIntervalSeconds()); + + setColumn(false); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_GUI)); + addStringSetting(cmd, events, "server.guiTitle", "GUI Title", cfg.getGuiTitle()); + addBooleanSetting(cmd, events, "server.terrainMapEnabled", "Terrain Map", cfg.isTerrainMapEnabled()); + addIntSetting(cmd, events, "server.leaderboardKdRefreshSeconds", "K/D Refresh (sec)", cfg.server().getLeaderboardKdRefreshSeconds()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_PERMISSIONS)); + addBooleanSetting(cmd, events, "server.adminRequiresOp", "Admin Requires OP", cfg.isAdminRequiresOp()); + addBooleanSetting(cmd, events, "server.allowWithoutPermissionMod", "Allow Without Perm Mod", cfg.isAllowWithoutPermissionMod()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_LANGUAGE)); + addLocaleSetting(cmd, events, "server.defaultLanguage", "Default Language", cfg.getDefaultLanguage()); + addBooleanSetting(cmd, events, "server.usePlayerLanguage", "Use Player Language", cfg.isUsePlayerLanguage()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_UPDATES)); + addBooleanSetting(cmd, events, "server.updateCheckEnabled", "Update Check", cfg.isUpdateCheckEnabled()); + addEnumSetting(cmd, events, "server.releaseChannel", "Release Channel", cfg.getReleaseChannel(), "stable", "prerelease"); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_MIXIN)); + addBooleanSetting(cmd, events, "server.hyperProtectAutoDownload", "Auto Download", cfg.server().isHyperProtectAutoDownload()); + addBooleanSetting(cmd, events, "server.hyperProtectAutoUpdate", "Auto Update", cfg.server().isHyperProtectAutoUpdate()); + } + + // ================================================================ + // Chat Tab + // ================================================================ + + private void buildChatTab(UICommandBuilder cmd, UIEventBuilder events, ConfigManager cfg) { + setColumn(true); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_FORMAT)); + addBooleanSetting(cmd, events, "chat.enabled", "Chat Formatting", cfg.isChatFormattingEnabled()); + addEnumSetting(cmd, events, "chat.tagDisplay", "Tag Display", cfg.getChatTagDisplay(), "tag", "name", "none"); + addStringSetting(cmd, events, "chat.tagFormat", "Tag Format", cfg.getChatTagFormat()); + addStringSetting(cmd, events, "chat.noFactionTag", "Factionless Tag", cfg.getChatNoFactionTag()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_COLORS)); + addColorSetting(cmd, events, "chat.noFactionTagColor", "Factionless Color", cfg.getChatNoFactionTagColor()); + addColorSetting(cmd, events, "chat.playerNameColor", "Player Name Color", cfg.getChatPlayerNameColor()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_REL_COLORS)); + addColorSetting(cmd, events, "chat.relationColorOwn", "Own Faction", cfg.getChatRelationColorOwn()); + addColorSetting(cmd, events, "chat.relationColorAlly", "Ally", cfg.getChatRelationColorAlly()); + addColorSetting(cmd, events, "chat.relationColorNeutral", "Neutral", cfg.getChatRelationColorNeutral()); + addColorSetting(cmd, events, "chat.relationColorEnemy", "Enemy", cfg.getChatRelationColorEnemy()); + + setColumn(false); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_FACTION_CHAT)); + addColorSetting(cmd, events, "chat.factionChatColor", "Faction Chat Color", cfg.getFactionChatColor()); + addStringSetting(cmd, events, "chat.factionChatPrefix", "Faction Chat Prefix", cfg.getFactionChatPrefix()); + addColorSetting(cmd, events, "chat.allyChatColor", "Ally Chat Color", cfg.getAllyChatColor()); + addStringSetting(cmd, events, "chat.allyChatPrefix", "Ally Chat Prefix", cfg.getAllyChatPrefix()); + addColorSetting(cmd, events, "chat.senderNameColor", "Sender Name Color", cfg.getSenderNameColor()); + addColorSetting(cmd, events, "chat.messageColor", "Message Color", cfg.getMessageColor()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_HISTORY)); + addBooleanSetting(cmd, events, "chat.historyEnabled", "Chat History", cfg.isChatHistoryEnabled()); + addIntSetting(cmd, events, "chat.historyMaxMessages", "Max Messages", cfg.getChatHistoryMaxMessages()); + addIntSetting(cmd, events, "chat.historyRetentionDays", "Retention Days", cfg.getChatHistoryRetentionDays()); + addIntSetting(cmd, events, "chat.historyCleanupIntervalMinutes", "Cleanup (min)", cfg.getChatHistoryCleanupIntervalMinutes()); + } + + // ================================================================ + // Announcements Tab + // ================================================================ + + private void buildAnnouncementsTab(UICommandBuilder cmd, UIEventBuilder events, ConfigManager cfg) { + setColumn(true); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_ANNOUNCE)); + addBooleanSetting(cmd, events, "announce.factionCreated", "Faction Created", cfg.announcements().isFactionCreated()); + addBooleanSetting(cmd, events, "announce.factionDisbanded", "Faction Disbanded", cfg.announcements().isFactionDisbanded()); + addBooleanSetting(cmd, events, "announce.leadershipTransfer", "Leadership Transfer", cfg.announcements().isLeadershipTransfer()); + addBooleanSetting(cmd, events, "announce.overclaim", "Overclaim", cfg.announcements().isOverclaim()); + addBooleanSetting(cmd, events, "announce.warDeclared", "War Declared", cfg.announcements().isWarDeclared()); + addBooleanSetting(cmd, events, "announce.allianceFormed", "Alliance Formed", cfg.announcements().isAllianceFormed()); + addBooleanSetting(cmd, events, "announce.allianceBroken", "Alliance Broken", cfg.announcements().isAllianceBroken()); + + setColumn(false); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_TERRITORY_NOTIFY)); + addBooleanSetting(cmd, events, "announce.territoryNotificationsEnabled", "Territory Notifications", cfg.isTerritoryNotificationsEnabled()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_WILDERNESS)); + addBooleanSetting(cmd, events, "announce.wildernessOnLeaveZoneEnabled", "Show on Leave Zone", cfg.announcements().isWildernessOnLeaveZoneEnabled()); + addWideStringSetting(cmd, events, "announce.wildernessOnLeaveZoneUpper", "Zone Upper Text", cfg.announcements().getWildernessOnLeaveZoneUpper()); + addWideStringSetting(cmd, events, "announce.wildernessOnLeaveZoneLower", "Zone Lower Text", cfg.announcements().getWildernessOnLeaveZoneLower()); + addBooleanSetting(cmd, events, "announce.wildernessOnLeaveClaimEnabled", "Show on Leave Claim", cfg.announcements().isWildernessOnLeaveClaimEnabled()); + addWideStringSetting(cmd, events, "announce.wildernessOnLeaveClaimUpper", "Claim Upper Text", cfg.announcements().getWildernessOnLeaveClaimUpper()); + addWideStringSetting(cmd, events, "announce.wildernessOnLeaveClaimLower", "Claim Lower Text", cfg.announcements().getWildernessOnLeaveClaimLower()); + } + + // ================================================================ + // Economy Tab + // ================================================================ + + private void buildEconomyTab(UICommandBuilder cmd, UIEventBuilder events, ConfigManager cfg) { + EconomyConfig eco = cfg.economy(); + setColumn(true); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_CURRENCY)); + addBooleanSetting(cmd, events, "economy.enabled", "Economy Enabled", eco.isEnabled()); + addStringSetting(cmd, events, "economy.currencyName", "Currency Name", eco.getCurrencyName()); + addStringSetting(cmd, events, "economy.currencyNamePlural", "Currency Plural", eco.getCurrencyNamePlural()); + addStringSetting(cmd, events, "economy.currencySymbol", "Currency Symbol", eco.getCurrencySymbol()); + addEnumSetting(cmd, events, "economy.currencySymbolPosition", "Symbol Position", eco.getCurrencySymbolPosition(), "left", "right"); + addDoubleSetting(cmd, events, "economy.startingBalance", "Starting Balance", eco.getStartingBalance().doubleValue()); + addBooleanSetting(cmd, events, "economy.disbandRefundToLeader", "Disband Refund", eco.isDisbandRefundToLeader()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_TREASURY_LIMITS)); + addDoubleSetting(cmd, events, "economy.defaultMaxWithdrawAmount", "Max Withdraw", eco.getDefaultMaxWithdrawAmount().doubleValue()); + addDoubleSetting(cmd, events, "economy.defaultMaxWithdrawPerPeriod", "Max Withdraw/Period", eco.getDefaultMaxWithdrawPerPeriod().doubleValue()); + addDoubleSetting(cmd, events, "economy.defaultMaxTransferAmount", "Max Transfer", eco.getDefaultMaxTransferAmount().doubleValue()); + addDoubleSetting(cmd, events, "economy.defaultMaxTransferPerPeriod", "Max Transfer/Period", eco.getDefaultMaxTransferPerPeriod().doubleValue()); + addIntSetting(cmd, events, "economy.defaultLimitPeriodHours", "Limit Period (hr)", eco.getDefaultLimitPeriodHours()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_FEES)); + addDoubleSetting(cmd, events, "economy.depositFeePercent", "Deposit Fee %", eco.getDepositFeePercent().doubleValue()); + addDoubleSetting(cmd, events, "economy.withdrawFeePercent", "Withdraw Fee %", eco.getWithdrawFeePercent().doubleValue()); + addDoubleSetting(cmd, events, "economy.transferFeePercent", "Transfer Fee %", eco.getTransferFeePercent().doubleValue()); + + setColumn(false); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_UPKEEP)); + addBooleanSetting(cmd, events, "economy.upkeepEnabled", "Upkeep Enabled", eco.isUpkeepEnabled()); + addDoubleSetting(cmd, events, "economy.upkeepCostPerChunk", "Cost Per Chunk", eco.getUpkeepCostPerChunk().doubleValue()); + addIntSetting(cmd, events, "economy.upkeepIntervalHours", "Interval (hr)", eco.getUpkeepIntervalHours()); + addIntSetting(cmd, events, "economy.upkeepGracePeriodHours", "Grace Period (hr)", eco.getUpkeepGracePeriodHours()); + addBooleanSetting(cmd, events, "economy.upkeepAutoPayDefault", "Auto-Pay Default", eco.isUpkeepAutoPayDefault()); + addIntSetting(cmd, events, "economy.upkeepFreeChunks", "Free Chunks", eco.getUpkeepFreeChunks()); + addIntSetting(cmd, events, "economy.upkeepClaimLossPerCycle", "Claim Loss/Cycle", eco.getUpkeepClaimLossPerCycle()); + addIntSetting(cmd, events, "economy.upkeepWarningHours", "Warning (hr)", eco.getUpkeepWarningHours()); + addDoubleSetting(cmd, events, "economy.upkeepMaxCostCap", "Max Cost Cap", eco.getUpkeepMaxCostCap().doubleValue()); + addEnumSetting(cmd, events, "economy.upkeepScalingMode", "Scaling Mode", eco.getUpkeepScalingMode(), "flat", "progressive"); + String effectiveScaling = pendingChanges.containsKey("economy.upkeepScalingMode") + ? String.valueOf(pendingChanges.get("economy.upkeepScalingMode")) : eco.getUpkeepScalingMode(); + boolean scalingDisabled = "flat".equals(effectiveScaling); + if (!scalingDisabled) { + addActionButton(cmd, events, "EditScalingTiers", + "Edit Tiers (" + eco.getUpkeepScalingTiers().size() + ")", false); + } + } + + // ================================================================ + // Factions Tab + // ================================================================ + + private void buildFactionsTab(UICommandBuilder cmd, UIEventBuilder events, ConfigManager cfg) { + // Col 0: Limits + Power + setCol(0); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_FACTION_LIMITS)); + addIntSetting(cmd, events, "factions.maxMembers", "Max Members", cfg.getMaxMembers()); + addIntSetting(cmd, events, "factions.maxNameLength", "Max Name Len", cfg.getMaxNameLength()); + addIntSetting(cmd, events, "factions.minNameLength", "Min Name Len", cfg.getMinNameLength()); + addBooleanSetting(cmd, events, "factions.allowColors", "Allow Colors", cfg.isAllowColors()); + addIntSetting(cmd, events, "factions.maxMembershipHistory", "History Limit", cfg.factions().getMaxMembershipHistory()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_POWER)); + addDoubleSetting(cmd, events, "factions.maxPlayerPower", "Max Power", cfg.getMaxPlayerPower()); + addDoubleSetting(cmd, events, "factions.startingPower", "Starting Power", cfg.getStartingPower()); + addDoubleSetting(cmd, events, "factions.powerPerClaim", "Power/Claim", cfg.getPowerPerClaim()); + addDoubleSetting(cmd, events, "factions.deathPenalty", "Death Penalty", cfg.getDeathPenalty()); + addDoubleSetting(cmd, events, "factions.killReward", "Kill Reward", cfg.getKillReward()); + addBooleanSetting(cmd, events, "factions.killRewardRequiresFaction", "Requires Faction", cfg.isKillRewardRequiresFaction()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_POWER_LOSS)); + addBooleanSetting(cmd, events, "factions.powerLossOnMobDeath", "Mob Death", cfg.isPowerLossOnMobDeath()); + addBooleanSetting(cmd, events, "factions.powerLossOnEnvironmentalDeath", "Env. Death", cfg.isPowerLossOnEnvironmentalDeath()); + + // Col 1: Regen + Claims + Decay + Stuck + setCol(1); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_REGEN)); + addDoubleSetting(cmd, events, "factions.regenPerMinute", "Regen/Min", cfg.getRegenPerMinute()); + addBooleanSetting(cmd, events, "factions.regenWhenOffline", "Offline Regen", cfg.isRegenWhenOffline()); + addBooleanSetting(cmd, events, "factions.hardcoreMode", "Hardcore", cfg.isHardcoreMode()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_CLAIMS)); + addIntSetting(cmd, events, "factions.maxClaims", "Max Claims", cfg.getMaxClaims()); + addBooleanSetting(cmd, events, "factions.onlyAdjacent", "Only Adjacent", cfg.isOnlyAdjacent()); + addBooleanSetting(cmd, events, "factions.preventDisconnect", "No Disconnect", cfg.isPreventDisconnect()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_DECAY)); + addBooleanSetting(cmd, events, "factions.decayEnabled", "Decay Enabled", cfg.isDecayEnabled()); + addIntSetting(cmd, events, "factions.decayDaysInactive", "Days Inactive", cfg.getDecayDaysInactive()); + addIntSetting(cmd, events, "factions.decayClaimsPerCycle", "Claims/Cycle", cfg.factions().getDecayClaimsPerCycle()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_STUCK)); + addIntSetting(cmd, events, "factions.stuckMinRadius", "Min Radius", cfg.getStuckMinRadius()); + addIntSetting(cmd, events, "factions.stuckRadiusIncrease", "Radius Inc", cfg.getStuckRadiusIncrease()); + addIntSetting(cmd, events, "factions.stuckMaxAttempts", "Max Attempts", cfg.getStuckMaxAttempts()); + addIntSetting(cmd, events, "factions.stuckWarmupSeconds", "Warmup (sec)", cfg.getStuckWarmupSeconds()); + addIntSetting(cmd, events, "factions.stuckCooldownSeconds", "Cooldown (sec)", cfg.getStuckCooldownSeconds()); + + // Col 2: Protection + Friendly Fire + setCol(2); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_PROTECTION)); + addBooleanSetting(cmd, events, "factions.outsiderPickupAllowed", "Outsider Pickup", cfg.isOutsiderPickupAllowed()); + addBooleanSetting(cmd, events, "factions.outsiderDropAllowed", "Outsider Drop", cfg.isOutsiderDropAllowed()); + addBooleanSetting(cmd, events, "factions.factionlessExplosionsAllowed", "F'less Explosions", cfg.isFactionlessExplosionsAllowed()); + addBooleanSetting(cmd, events, "factions.enemyExplosionsAllowed", "Enemy Explosions", cfg.isEnemyExplosionsAllowed()); + addBooleanSetting(cmd, events, "factions.neutralExplosionsAllowed", "Neutral Explosions", cfg.isNeutralExplosionsAllowed()); + addBooleanSetting(cmd, events, "factions.fireSpreadAllowed", "Fire Spread", cfg.isFireSpreadAllowed()); + addBooleanSetting(cmd, events, "factions.factionlessDamageAllowed", "F'less Damage", cfg.isFactionlessDamageAllowed()); + addBooleanSetting(cmd, events, "factions.enemyDamageAllowed", "Enemy Damage", cfg.isEnemyDamageAllowed()); + addBooleanSetting(cmd, events, "factions.neutralDamageAllowed", "Neutral Damage", cfg.isNeutralDamageAllowed()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_FRIENDLY_FIRE)); + addBooleanSetting(cmd, events, "factions.allyDamage", "Ally Damage", cfg.isAllyDamage()); + addBooleanSetting(cmd, events, "factions.factionDamage", "Faction Damage", cfg.isFactionDamage()); + + // Col 3: Combat Tag + Spawn Prot + Relations + Invites + setCol(3); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_COMBAT_TAG)); + addIntSetting(cmd, events, "factions.tagDurationSeconds", "Duration (sec)", cfg.getTagDurationSeconds()); + addBooleanSetting(cmd, events, "factions.taggedLogoutPenalty", "Logout Penalty", cfg.isTaggedLogoutPenalty()); + addDoubleSetting(cmd, events, "factions.logoutPowerLoss", "Logout Loss", cfg.getLogoutPowerLoss()); + addDoubleSetting(cmd, events, "factions.neutralAttackPenalty", "Neutral Pen.", cfg.getNeutralAttackPenalty()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_SPAWN_PROT)); + addBooleanSetting(cmd, events, "factions.spawnProtectionEnabled", "Enabled", cfg.isSpawnProtectionEnabled()); + addIntSetting(cmd, events, "factions.spawnProtectionDurationSeconds", "Duration (sec)", cfg.getSpawnProtectionDurationSeconds()); + addBooleanSetting(cmd, events, "factions.spawnProtectionBreakOnAttack", "Break on Hit", cfg.isSpawnProtectionBreakOnAttack()); + addBooleanSetting(cmd, events, "factions.spawnProtectionBreakOnMove", "Break on Move", cfg.isSpawnProtectionBreakOnMove()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_RELATIONS)); + addIntSetting(cmd, events, "factions.maxAllies", "Max Allies", cfg.getMaxAllies()); + addIntSetting(cmd, events, "factions.maxEnemies", "Max Enemies", cfg.getMaxEnemies()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_INVITES)); + addIntSetting(cmd, events, "factions.inviteExpirationMinutes", "Invite Expiry", cfg.getInviteExpirationMinutes()); + addIntSetting(cmd, events, "factions.joinRequestExpirationHours", "Request Expiry", cfg.getJoinRequestExpirationHours()); + } + + // ================================================================ + // Faction Perms Tab + // ================================================================ + + private void buildFactionPermsTab(UICommandBuilder cmd, UIEventBuilder events, ConfigManager cfg) { + FactionPermissionsConfig permCfg = cfg.factionPermissions(); + + // Col 0: Outsider — Col 1: Ally — Col 2: Member — Col 3: Officer + setCol(0); + addFacPermSection(cmd, events, permCfg, FactionPermissions.LEVEL_OUTSIDER); + + setCol(1); + addFacPermSection(cmd, events, permCfg, FactionPermissions.LEVEL_ALLY); + + setCol(2); + addFacPermSection(cmd, events, permCfg, FactionPermissions.LEVEL_MEMBER); + + setCol(3); + addFacPermSection(cmd, events, permCfg, FactionPermissions.LEVEL_OFFICER); + + // Shared sections — distribute across columns to balance height + setCol(0); + addSectionHeader(cmd, "Global"); + addFacPermColumnHeader(cmd); + addFacPermRow(cmd, events, permCfg, FactionPermissions.PVP_ENABLED, false, false); + addFacPermRow(cmd, events, permCfg, FactionPermissions.OFFICERS_CAN_EDIT, false, false); + + setCol(1); + addSectionHeader(cmd, "Mob Spawning"); + addFacPermColumnHeader(cmd); + addFacPermRow(cmd, events, permCfg, FactionPermissions.MOB_SPAWNING, false, true); + addFacPermRow(cmd, events, permCfg, FactionPermissions.HOSTILE_MOB_SPAWNING, true, false); + addFacPermRow(cmd, events, permCfg, FactionPermissions.PASSIVE_MOB_SPAWNING, true, false); + addFacPermRow(cmd, events, permCfg, FactionPermissions.NEUTRAL_MOB_SPAWNING, true, false); + + setCol(2); + addSectionHeader(cmd, "Treasury"); + addFacPermColumnHeader(cmd); + addFacPermRow(cmd, events, permCfg, FactionPermissions.TREASURY_DEPOSIT, false, false); + addFacPermRow(cmd, events, permCfg, FactionPermissions.TREASURY_WITHDRAW, false, false); + addFacPermRow(cmd, events, permCfg, FactionPermissions.TREASURY_TRANSFER, false, false); + } + + /** Adds a permission section for a specific level with Def/Lock column header. */ + private void addFacPermSection(UICommandBuilder cmd, UIEventBuilder events, + FactionPermissionsConfig permCfg, String level) { + String title = level.substring(0, 1).toUpperCase() + level.substring(1); + addSectionHeader(cmd, title); + addFacPermColumnHeader(cmd); + for (String flag : FactionPermissions.getFlagsForLevel(level)) { + boolean isChild = FactionPermissions.getParentFlag(flag) != null; + boolean isParent = FactionPermissions.isParentFlag(flag); + addFacPermRow(cmd, events, permCfg, flag, isChild, isParent); + } + } + + /** Adds a Def / Lock column header row. */ + private void addFacPermColumnHeader(UICommandBuilder cmd) { + String containerId = getContainerId(); + cmd.append(containerId, UIPaths.ADMIN_CONFIG_FACPERM_HEADER); + incrementRowIdx(); + } + + /** Gets a short display name for a faction permission flag (for the compact 4-col layout). */ + private static String getFacPermDisplayName(String flag) { + // Non-level flags first — must check before level-prefix loop to avoid + // "officer" matching as prefix of "officersCanEdit" → "sCanEdit" + return switch (flag) { + case "mobSpawning" -> "All Mobs"; + case "hostileMobSpawning" -> "Hostile"; + case "passiveMobSpawning" -> "Passive"; + case "neutralMobSpawning" -> "Neutral"; + case "pvpEnabled" -> "PvP"; + case "officersCanEdit" -> "Officers Edit"; + case "treasuryDeposit" -> "Deposit"; + case "treasuryWithdraw" -> "Withdraw"; + case "treasuryTransfer" -> "Transfer"; + default -> getFacPermLevelDisplayName(flag); + }; + } + + /** Strips level prefix from a level-based flag to get the display suffix. */ + private static String getFacPermLevelDisplayName(String flag) { + for (String level : FactionPermissions.ALL_LEVELS) { + if (flag.startsWith(level)) { + String suffix = flag.substring(level.length()); + return switch (suffix) { + case "Break" -> "Break"; + case "Place" -> "Place"; + case "Interact" -> "Interact"; + case "DoorUse" -> "Door Use"; + case "ContainerUse" -> "Container"; + case "BenchUse" -> "Bench Use"; + case "ProcessingUse" -> "Processing"; + case "SeatUse" -> "Seat Use"; + case "TransportUse" -> "Transport"; + case "CrateUse" -> "Crate Use"; + case "NpcTame" -> "NPC Tame"; + case "PveDamage" -> "PvE Damage"; + default -> suffix; + }; + } + } + return flag; + } + + /** Adds a single faction permission row with Default and Lock checkboxes. */ + private void addFacPermRow(UICommandBuilder cmd, UIEventBuilder events, + FactionPermissionsConfig permCfg, String flag, + boolean isChild, boolean isParent) { + String defaultKey = "facperm.default." + flag; + String lockKey = "facperm.lock." + flag; + boolean defaultPending = pendingChanges.containsKey(defaultKey); + boolean lockPending = pendingChanges.containsKey(lockKey); + boolean defaultVal = defaultPending ? (Boolean) pendingChanges.get(defaultKey) : permCfg.getDefault(flag); + boolean lockVal = lockPending ? (Boolean) pendingChanges.get(lockKey) : permCfg.isPermissionLocked(flag); + if (!defaultPending) originalValues.putIfAbsent(defaultKey, permCfg.getDefault(flag)); + if (!lockPending) originalValues.putIfAbsent(lockKey, permCfg.isPermissionLocked(flag)); + + String containerId = getContainerId(); + cmd.append(containerId, isChild ? UIPaths.ADMIN_CONFIG_FACPERM_CHILD_ROW : UIPaths.ADMIN_CONFIG_FACPERM_ROW); + String idx = containerId + "[" + getRowIdx() + "]"; + + // Short display name for the compact 4-column layout + String displayName = getFacPermDisplayName(flag); + String parentFlag = FactionPermissions.getParentFlag(flag); + + String labelColor = (defaultPending || lockPending) ? "#FFAA00" : "#CCCCCC"; + cmd.set(idx + " #SettingLabel.Text", displayName); + cmd.set(idx + " #SettingLabel.Style.TextColor", labelColor); + + // Default checkbox + cmd.set(idx + " #DefaultToggle #CheckBox.Value", defaultVal); + events.addEventBinding(CustomUIEventBindingType.ValueChanged, idx + " #DefaultToggle #CheckBox", + EventData.of("Button", "ToggleSetting").append("SettingKey", defaultKey), false); + settingSelectors.put(defaultKey, idx); + settingKinds.put(defaultKey, SettingKind.BOOL); + + // Lock checkbox + cmd.set(idx + " #LockToggle #CheckBox.Value", lockVal); + events.addEventBinding(CustomUIEventBindingType.ValueChanged, idx + " #LockToggle #CheckBox", + EventData.of("Button", "ToggleSetting").append("SettingKey", lockKey), false); + settingSelectors.put(lockKey, idx); + settingKinds.put(lockKey, SettingKind.BOOL); + + // Disable child Default checkboxes when parent Default is OFF, and force unchecked + if (isChild && parentFlag != null) { + String parentDefaultKey = "facperm.default." + parentFlag; + boolean parentDefault = pendingChanges.containsKey(parentDefaultKey) + ? (Boolean) pendingChanges.get(parentDefaultKey) : permCfg.getDefault(parentFlag); + if (!parentDefault) { + cmd.set(idx + " #DefaultToggle #CheckBox.Value", false); + cmd.set(idx + " #DefaultToggle #CheckBox.Disabled", true); + } + } + + incrementRowIdx(); + } + + // ================================================================ + // Worldmap Tab + // ================================================================ + + private void buildWorldmapTab(UICommandBuilder cmd, UIEventBuilder events, ConfigManager cfg) { + WorldMapConfig wm = cfg.worldMap(); + + // Determine effective refresh mode (pending change or current) + String effectiveMode = pendingChanges.containsKey("worldmap.refreshMode") + ? String.valueOf(pendingChanges.get("worldmap.refreshMode")) + : wm.getRefreshMode().getConfigName(); + + setColumn(true); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_MAP_DISPLAY)); + addBooleanSetting(cmd, events, "worldmap.enabled", "World Map Markers", wm.isEnabled()); + addBooleanSetting(cmd, events, "worldmap.showFactionTags", "Show Faction Tags", wm.isShowFactionTags()); + addEnumSetting(cmd, events, "worldmap.refreshMode", "Refresh Mode", wm.getRefreshMode().getConfigName(), + "proximity", "incremental", "debounced", "immediate", "manual"); + addBooleanSetting(cmd, events, "worldmap.autoFallbackOnError", "Auto Fallback", wm.isAutoFallbackOnError()); + addIntSetting(cmd, events, "worldmap.factionWideRefreshThreshold", "Refresh Threshold", wm.getFactionWideRefreshThreshold()); + + // Conditional settings based on active refresh mode + if ("proximity".equals(effectiveMode)) { + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_PROXIMITY)); + addIntSetting(cmd, events, "worldmap.proximityChunkRadius", "Chunk Radius", wm.getProximityChunkRadius()); + addIntSetting(cmd, events, "worldmap.proximityBatchIntervalTicks", "Batch Interval", wm.getProximityBatchIntervalTicks()); + addIntSetting(cmd, events, "worldmap.proximityMaxChunksPerBatch", "Max Chunks/Batch", wm.getProximityMaxChunksPerBatch()); + } else if ("incremental".equals(effectiveMode)) { + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_INCREMENTAL)); + addIntSetting(cmd, events, "worldmap.incrementalBatchIntervalTicks", "Batch Interval", wm.getIncrementalBatchIntervalTicks()); + addIntSetting(cmd, events, "worldmap.incrementalMaxChunksPerBatch", "Max Chunks/Batch", wm.getIncrementalMaxChunksPerBatch()); + } else if ("debounced".equals(effectiveMode)) { + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_DEBOUNCED)); + addIntSetting(cmd, events, "worldmap.debouncedDelaySeconds", "Delay (sec)", wm.getDebouncedDelaySeconds()); + } + // "immediate" and "manual" have no extra settings + + setColumn(false); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_VISIBILITY)); + addBooleanSetting(cmd, events, "worldmap.playerVisibilityEnabled", "Player Visibility", wm.isPlayerVisibilityEnabled()); + addBooleanSetting(cmd, events, "worldmap.showOwnFaction", "Show Own Faction", wm.isShowOwnFaction()); + addBooleanSetting(cmd, events, "worldmap.showAllies", "Show Allies", wm.isShowAllies()); + addBooleanSetting(cmd, events, "worldmap.showNeutrals", "Show Neutrals", wm.isShowNeutrals()); + addBooleanSetting(cmd, events, "worldmap.showEnemies", "Show Enemies", wm.isShowEnemies()); + addBooleanSetting(cmd, events, "worldmap.showFactionlessPlayers", "Show Factionless", wm.isShowFactionlessPlayers()); + addBooleanSetting(cmd, events, "worldmap.showFactionlessToFactionless", "Show to Factionless", wm.isShowFactionlessToFactionless()); } - /** Handles data event. */ + // ================================================================ + // Worlds Tab (info-only) + // ================================================================ + + private void buildWorldsTab(UICommandBuilder cmd, UIEventBuilder events, ConfigManager cfg) { + WorldsConfig worlds = cfg.worlds(); + + setColumn(true); + addSectionHeader(cmd, "Global Policy"); + String effectivePolicy = pendingChanges.containsKey("worlds.defaultPolicy") + ? String.valueOf(pendingChanges.get("worlds.defaultPolicy")) : worlds.getDefaultPolicy(); + originalValues.putIfAbsent("worlds.defaultPolicy", worlds.getDefaultPolicy()); + addEnumSetting(cmd, events, "worlds.defaultPolicy", "Default Policy", effectivePolicy, "allow", "deny"); + + addSectionHeader(cmd, "Per-World Overrides"); + + // Add world input row at the top + String addWorldContainer = getContainerId(); + cmd.append(addWorldContainer, UIPaths.ADMIN_CONFIG_ADD_ROW); + String addWorldIdx = addWorldContainer + "[" + getRowIdx() + "]"; + cmd.set(addWorldIdx + " #AddBtn.Text", "Add World"); + events.addEventBinding(CustomUIEventBindingType.Activating, addWorldIdx + " #AddBtn", + EventData.of("Button", "AddWorld") + .append("@strInput", addWorldIdx + " #AddInput.Value"), false); + incrementRowIdx(); + + // Spacer between add box and list + addSectionHeader(cmd, ""); + + // World entries + Map worldMap = getEffectiveWorldOverrides(worlds); + int worldIdx = 0; + for (Map.Entry entry : worldMap.entrySet()) { + String worldKey = entry.getKey(); + WorldsConfig.WorldSettings ws = entry.getValue(); + addWorldOverrideEntry(cmd, events, worldKey, ws, worldIdx); + worldIdx++; + } + } + + private void addWorldOverrideEntry(UICommandBuilder cmd, UIEventBuilder events, + String worldKey, WorldsConfig.WorldSettings ws, int worldIdx) { + String containerId = getContainerId(); + cmd.append(containerId, UIPaths.ADMIN_CONFIG_WORLD_ENTRY); + String idx = containerId + "[" + getRowIdx() + "]"; + cmd.set(idx + " #WorldName.Text", worldKey); + + events.addEventBinding(CustomUIEventBindingType.Activating, idx + " #RemoveWorldBtn", + EventData.of("Button", "RemoveWorld").append("SettingKey", worldKey), false); + events.addEventBinding(CustomUIEventBindingType.Activating, idx + " #ExpandBtn", + EventData.of("Button", "ToggleWorldExpand").append("SettingKey", worldKey), false); + + // Check if this world is expanded + boolean expanded = expandedWorlds.contains(worldKey); + if (expanded) { + cmd.set(idx + " #ExpandBtn.Text", "Hide"); + // Add tri-state dropdowns for each setting + String[] settings = { "claiming", "powerLoss", "friendlyFireFaction", "friendlyFireAlly" }; + String[] labels = { "Claiming", "Power Loss", "FF Faction", "FF Ally" }; + Boolean[] values = { ws.claiming(), ws.powerLoss(), ws.friendlyFireFaction(), ws.friendlyFireAlly() }; + + for (int s = 0; s < settings.length; s++) { + String settingKey = "worlds.override." + worldKey + "." + settings[s]; + String effectiveVal = triStateToString(values[s]); + + String settingsContainer = idx + " #WorldSettings"; + cmd.append(settingsContainer, UIPaths.ADMIN_CONFIG_TRISTATE_ROW); + String settingIdx = settingsContainer + "[" + s + "]"; + cmd.set(settingIdx + " #SettingLabel.Text", labels[s]); + cmd.set(settingIdx + " #TristateSelect.Entries", + List.of( + new DropdownEntryInfo(LocalizableString.fromString("Default"), "default"), + new DropdownEntryInfo(LocalizableString.fromString("Allow"), "allow"), + new DropdownEntryInfo(LocalizableString.fromString("Deny"), "deny") + )); + cmd.set(settingIdx + " #TristateSelect.Value", effectiveVal); + events.addEventBinding(CustomUIEventBindingType.ValueChanged, settingIdx + " #TristateSelect", + EventData.of("Button", "WorldSettingChanged").append("SettingKey", settingKey) + .append("@enumValue", settingIdx + " #TristateSelect.Value"), false); + } + } + + incrementRowIdx(); + } + + private static String triStateToString(Boolean value) { + if (value == null) return "default"; + return value ? "allow" : "deny"; + } + + private static Boolean triStateFromString(String value) { + return switch (value) { + case "allow" -> true; + case "deny" -> false; + default -> null; + }; + } + + /** Returns the effective world overrides map (pending or from config). */ + private Map getEffectiveWorldOverrides(WorldsConfig worlds) { + if (pendingWorldOverrides != null) { + return pendingWorldOverrides; + } + return worlds.getWorlds(); + } + + /** Lazily initializes pendingWorldOverrides from config if not yet done. */ + private LinkedHashMap ensurePendingWorldOverrides() { + if (pendingWorldOverrides == null) { + pendingWorldOverrides = new LinkedHashMap<>(ConfigManager.get().worlds().getWorlds()); + } + return pendingWorldOverrides; + } + + // ================================================================ + // Backup Tab + // ================================================================ + + private void buildBackupTab(UICommandBuilder cmd, UIEventBuilder events, ConfigManager cfg) { + setColumn(true); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_BACKUP)); + addBooleanSetting(cmd, events, "backup.enabled", "Backup Enabled", cfg.isBackupEnabled()); + addIntSetting(cmd, events, "backup.hourlyRetention", "Hourly Retention", cfg.getBackupHourlyRetention()); + addIntSetting(cmd, events, "backup.dailyRetention", "Daily Retention", cfg.getBackupDailyRetention()); + addIntSetting(cmd, events, "backup.weeklyRetention", "Weekly Retention", cfg.getBackupWeeklyRetention()); + addIntSetting(cmd, events, "backup.manualRetention", "Manual Retention", cfg.getBackupManualRetention()); + addBooleanSetting(cmd, events, "backup.onShutdown", "Backup on Shutdown", cfg.isBackupOnShutdown()); + addIntSetting(cmd, events, "backup.shutdownRetention", "Shutdown Retention", cfg.getBackupShutdownRetention()); + } + + // ================================================================ + // Debug Tab + // ================================================================ + + private void buildDebugTab(UICommandBuilder cmd, UIEventBuilder events, ConfigManager cfg) { + DebugConfig dbg = cfg.debug(); + setColumn(true); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_DEBUG_GLOBAL)); + addBooleanSetting(cmd, events, "debug.enabledByDefault", "Enabled by Default", dbg.isEnabledByDefault()); + addBooleanSetting(cmd, events, "debug.logToConsole", "Log to Console", dbg.isLogToConsole()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_SENTRY)); + addBooleanSetting(cmd, events, "debug.sentryEnabled", "Sentry Enabled", dbg.isSentryEnabled()); + addBooleanSetting(cmd, events, "debug.sentryDebug", "Sentry Debug", dbg.isSentryDebug()); + addDoubleSetting(cmd, events, "debug.sentryTracesSampleRate", "Traces Sample Rate", dbg.getSentryTracesSampleRate()); + + setColumn(false); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_DEBUG_CATEGORIES)); + addBooleanSetting(cmd, events, "debug.power", "Power", dbg.isPower()); + addBooleanSetting(cmd, events, "debug.claim", "Claim", dbg.isClaim()); + addBooleanSetting(cmd, events, "debug.combat", "Combat", dbg.isCombat()); + addBooleanSetting(cmd, events, "debug.protection", "Protection", dbg.isProtection()); + addBooleanSetting(cmd, events, "debug.relation", "Relation", dbg.isRelation()); + addBooleanSetting(cmd, events, "debug.territory", "Territory", dbg.isTerritory()); + addBooleanSetting(cmd, events, "debug.worldmap", "World Map", dbg.isWorldmap()); + addBooleanSetting(cmd, events, "debug.interaction", "Interaction", dbg.isInteraction()); + addBooleanSetting(cmd, events, "debug.mixin", "Mixin", dbg.isMixin()); + addBooleanSetting(cmd, events, "debug.spawning", "Spawning", dbg.isSpawning()); + addBooleanSetting(cmd, events, "debug.integration", "Integration", dbg.isIntegration()); + addBooleanSetting(cmd, events, "debug.economy", "Economy", dbg.isEconomy()); + } + + // ================================================================ + // Gravestones Tab + // ================================================================ + + private void buildGravestonesTab(UICommandBuilder cmd, UIEventBuilder events, ConfigManager cfg) { + GravestoneConfig gs = cfg.gravestones(); + setColumn(true); + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_GRAVESTONE_PROTECTION)); + addBooleanSetting(cmd, events, "gravestone.protectInOwnTerritory", "Protect Own Territory", gs.isProtectInOwnTerritory()); + addBooleanSetting(cmd, events, "gravestone.protectInSafeZone", "Protect Safe Zone", gs.isProtectInSafeZone()); + addBooleanSetting(cmd, events, "gravestone.protectInWarZone", "Protect War Zone", gs.isProtectInWarZone()); + addBooleanSetting(cmd, events, "gravestone.protectInWilderness", "Protect Wilderness", gs.isProtectInWilderness()); + addBooleanSetting(cmd, events, "gravestone.protectInEnemyTerritory", "Protect Enemy Land", gs.isProtectInEnemyTerritory()); + addBooleanSetting(cmd, events, "gravestone.protectInNeutralTerritory", "Protect Neutral Land", gs.isProtectInNeutralTerritory()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_GRAVESTONE_ACCESS)); + addBooleanSetting(cmd, events, "gravestone.factionMembersCanAccess", "Members Can Access", gs.isFactionMembersCanAccess()); + addBooleanSetting(cmd, events, "gravestone.alliesCanAccess", "Allies Can Access", gs.isAlliesCanAccess()); + addBooleanSetting(cmd, events, "gravestone.enemiesCanLootInOwnTerritory", "Enemies Loot Own", gs.isEnemiesCanLootInOwnTerritory()); + addBooleanSetting(cmd, events, "gravestone.announceDeathLocation", "Announce Death Loc", gs.isAnnounceDeathLocation()); + + addSectionHeader(cmd, loc(AdminGuiKeys.AdminGui.CFG_SEC_GRAVESTONE_LOOT)); + addBooleanSetting(cmd, events, "gravestone.allowLootDuringRaid", "Allow During Raid", gs.isAllowLootDuringRaid()); + addBooleanSetting(cmd, events, "gravestone.allowLootDuringWar", "Allow During War", gs.isAllowLootDuringWar()); + } + + // ================================================================ + // Setting Row Builders + // ================================================================ + + private void addSectionHeader(UICommandBuilder cmd, String label) { + String containerId = getContainerId(); + cmd.append(containerId, UIPaths.ADMIN_CONFIG_SECTION); + cmd.set(containerId + "[" + getRowIdx() + "] #SectionTitle.Text", label); + incrementRowIdx(); + } + + private void addBooleanSetting(UICommandBuilder cmd, UIEventBuilder events, + String key, String label, boolean value) { + boolean pending = pendingChanges.containsKey(key); + boolean effectiveValue = pending ? (Boolean) pendingChanges.get(key) : value; + if (!pending) originalValues.putIfAbsent(key, value); + String color = pending ? "#FFAA00" : "#CCCCCC"; + + String containerId = getContainerId(); + cmd.append(containerId, UIPaths.ADMIN_CONFIG_BOOL_ROW); + String idx = containerId + "[" + getRowIdx() + "]"; + cmd.set(idx + " #SettingLabel.Text", label); + cmd.set(idx + " #SettingLabel.Style.TextColor", color); + cmd.set(idx + " #BoolToggle #CheckBox.Value", effectiveValue); + events.addEventBinding(CustomUIEventBindingType.ValueChanged, idx + " #BoolToggle #CheckBox", + EventData.of("Button", "ToggleSetting").append("SettingKey", key), false); + settingSelectors.put(key, idx); + settingKinds.put(key, SettingKind.BOOL); + incrementRowIdx(); + } + + private void addIntSetting(UICommandBuilder cmd, UIEventBuilder events, + String key, String label, int value) { + boolean pending = pendingChanges.containsKey(key); + int effectiveValue = pending ? ((Number) pendingChanges.get(key)).intValue() : value; + if (!pending) originalValues.putIfAbsent(key, value); + String color = pending ? "#FFAA00" : "#CCCCCC"; + + String containerId = getContainerId(); + cmd.append(containerId, UIPaths.ADMIN_CONFIG_NUM_ROW); + String idx = containerId + "[" + getRowIdx() + "]"; + cmd.set(idx + " #SettingLabel.Text", label); + cmd.set(idx + " #SettingLabel.Style.TextColor", color); + cmd.set(idx + " #NumInput.Value", String.valueOf(effectiveValue)); + if (pending) cmd.set(idx + " #NumInput.Style.TextColor", "#FFAA00"); + events.addEventBinding(CustomUIEventBindingType.Activating, idx + " #DecBtn", + EventData.of("Button", "DecrementSetting").append("SettingKey", key), false); + events.addEventBinding(CustomUIEventBindingType.Activating, idx + " #IncBtn", + EventData.of("Button", "IncrementSetting").append("SettingKey", key), false); + events.addEventBinding(CustomUIEventBindingType.ValueChanged, idx + " #NumInput", + EventData.of("Button", "SetNumericValue").append("SettingKey", key) + .append("@numInput", idx + " #NumInput.Value"), false); + settingSelectors.put(key, idx); + settingKinds.put(key, SettingKind.INT); + incrementRowIdx(); + } + + private void addDoubleSetting(UICommandBuilder cmd, UIEventBuilder events, + String key, String label, double value) { + boolean pending = pendingChanges.containsKey(key); + double effectiveValue = pending ? ((Number) pendingChanges.get(key)).doubleValue() : value; + if (!pending) originalValues.putIfAbsent(key, value); + String color = pending ? "#FFAA00" : "#CCCCCC"; + + String containerId = getContainerId(); + cmd.append(containerId, UIPaths.ADMIN_CONFIG_NUM_ROW); + String idx = containerId + "[" + getRowIdx() + "]"; + cmd.set(idx + " #SettingLabel.Text", label); + cmd.set(idx + " #SettingLabel.Style.TextColor", color); + cmd.set(idx + " #NumInput.Value", String.format("%.2f", effectiveValue)); + if (pending) cmd.set(idx + " #NumInput.Style.TextColor", "#FFAA00"); + events.addEventBinding(CustomUIEventBindingType.Activating, idx + " #DecBtn", + EventData.of("Button", "DecrementSetting").append("SettingKey", key), false); + events.addEventBinding(CustomUIEventBindingType.Activating, idx + " #IncBtn", + EventData.of("Button", "IncrementSetting").append("SettingKey", key), false); + events.addEventBinding(CustomUIEventBindingType.ValueChanged, idx + " #NumInput", + EventData.of("Button", "SetNumericValue").append("SettingKey", key) + .append("@numInput", idx + " #NumInput.Value"), false); + settingSelectors.put(key, idx); + settingKinds.put(key, SettingKind.DOUBLE); + incrementRowIdx(); + } + + private void addStringSetting(UICommandBuilder cmd, UIEventBuilder events, + String key, String label, String value) { + boolean pending = pendingChanges.containsKey(key); + String effectiveValue = pending ? String.valueOf(pendingChanges.get(key)) : value; + if (!pending) originalValues.putIfAbsent(key, value); + String color = pending ? "#FFAA00" : "#CCCCCC"; + + String containerId = getContainerId(); + cmd.append(containerId, UIPaths.ADMIN_CONFIG_STR_ROW); + String idx = containerId + "[" + getRowIdx() + "]"; + cmd.set(idx + " #SettingLabel.Text", label); + cmd.set(idx + " #SettingLabel.Style.TextColor", color); + cmd.set(idx + " #StrInput.Value", effectiveValue); + if (pending) cmd.set(idx + " #StrInput.Style.TextColor", "#FFAA00"); + events.addEventBinding(CustomUIEventBindingType.ValueChanged, idx + " #StrInput", + EventData.of("Button", "SetStringValue").append("SettingKey", key) + .append("@strInput", idx + " #StrInput.Value"), false); + settingSelectors.put(key, idx); + settingKinds.put(key, SettingKind.STRING); + incrementRowIdx(); + } + + private void addColorSetting(UICommandBuilder cmd, UIEventBuilder events, + String key, String label, String value) { + boolean pending = pendingChanges.containsKey(key); + String effectiveValue = pending ? String.valueOf(pendingChanges.get(key)) : value; + if (!pending) originalValues.putIfAbsent(key, value); + String color = pending ? "#FFAA00" : "#CCCCCC"; + + String containerId = getContainerId(); + cmd.append(containerId, UIPaths.ADMIN_CONFIG_COLOR_ROW); + String idx = containerId + "[" + getRowIdx() + "]"; + cmd.set(idx + " #SettingLabel.Text", label); + cmd.set(idx + " #SettingLabel.Style.TextColor", color); + cmd.set(idx + " #ColorPicker.Color", effectiveValue); + cmd.set(idx + " #ColorInput.Value", effectiveValue); + if (pending) cmd.set(idx + " #ColorInput.Style.TextColor", "#FFAA00"); + // Set button: reads the picker's current Color + events.addEventBinding(CustomUIEventBindingType.Activating, idx + " #ApplyColorBtn", + EventData.of("Button", "SetColorValue").append("SettingKey", key) + .append("@colorValue", idx + " #ColorPicker.Color"), false); + // Text field: type a hex color manually (debounced) + events.addEventBinding(CustomUIEventBindingType.ValueChanged, idx + " #ColorInput", + EventData.of("Button", "TypeColorValue").append("SettingKey", key) + .append("@strInput", idx + " #ColorInput.Value"), false); + settingSelectors.put(key, idx); + settingKinds.put(key, SettingKind.COLOR); + incrementRowIdx(); + } + + private void addEnumSetting(UICommandBuilder cmd, UIEventBuilder events, + String key, String label, String value, String... options) { + boolean pending = pendingChanges.containsKey(key); + String effectiveValue = pending ? String.valueOf(pendingChanges.get(key)) : value; + if (!pending) originalValues.putIfAbsent(key, value); + String color = pending ? "#FFAA00" : "#CCCCCC"; + + String containerId = getContainerId(); + cmd.append(containerId, UIPaths.ADMIN_CONFIG_ENUM_ROW); + String idx = containerId + "[" + getRowIdx() + "]"; + cmd.set(idx + " #SettingLabel.Text", label); + cmd.set(idx + " #SettingLabel.Style.TextColor", color); + cmd.set(idx + " #EnumSelect.Entries", + java.util.Arrays.stream(options) + .map(o -> new DropdownEntryInfo(LocalizableString.fromString(o), o)) + .toList()); + cmd.set(idx + " #EnumSelect.Value", effectiveValue); + events.addEventBinding(CustomUIEventBindingType.ValueChanged, idx + " #EnumSelect", + EventData.of("Button", "EnumChanged").append("SettingKey", key) + .append("@enumValue", idx + " #EnumSelect.Value"), false); + settingSelectors.put(key, idx); + settingKinds.put(key, SettingKind.ENUM); + incrementRowIdx(); + } + + private static final List AVAILABLE_LOCALES = List.of( + "en-US", "es-ES", "de-DE", "fr-FR", "pt-BR", + "ru-RU", "pl-PL", "it-IT", "nl-NL", "tl-PH" + ); + + private static String nativeDisplayName(String localeCode) { + Locale locale = Locale.forLanguageTag(localeCode); + String lang = locale.getDisplayLanguage(locale); + if (!lang.isEmpty()) { + lang = Character.toUpperCase(lang.charAt(0)) + lang.substring(1); + } + String country = locale.getCountry(); + return country.isEmpty() ? lang : lang + " (" + country + ")"; + } + + private void addLocaleSetting(UICommandBuilder cmd, UIEventBuilder events, + String key, String label, String value) { + boolean pending = pendingChanges.containsKey(key); + String effectiveValue = pending ? String.valueOf(pendingChanges.get(key)) : value; + if (!pending) originalValues.putIfAbsent(key, value); + String color = pending ? "#FFAA00" : "#CCCCCC"; + + String containerId = getContainerId(); + cmd.append(containerId, UIPaths.ADMIN_CONFIG_ENUM_ROW); + String idx = containerId + "[" + getRowIdx() + "]"; + cmd.set(idx + " #SettingLabel.Text", label); + cmd.set(idx + " #SettingLabel.Style.TextColor", color); + cmd.set(idx + " #EnumSelect.Entries", + AVAILABLE_LOCALES.stream() + .map(code -> new DropdownEntryInfo(LocalizableString.fromString(nativeDisplayName(code)), code)) + .toList()); + cmd.set(idx + " #EnumSelect.Value", effectiveValue); + events.addEventBinding(CustomUIEventBindingType.ValueChanged, idx + " #EnumSelect", + EventData.of("Button", "EnumChanged").append("SettingKey", key) + .append("@enumValue", idx + " #EnumSelect.Value"), false); + settingSelectors.put(key, idx); + settingKinds.put(key, SettingKind.ENUM); + incrementRowIdx(); + } + + private void addWideStringSetting(UICommandBuilder cmd, UIEventBuilder events, + String key, String label, String value) { + boolean pending = pendingChanges.containsKey(key); + String effectiveValue = pending ? String.valueOf(pendingChanges.get(key)) : value; + if (!pending) originalValues.putIfAbsent(key, value); + String color = pending ? "#FFAA00" : "#CCCCCC"; + + String containerId = getContainerId(); + cmd.append(containerId, UIPaths.ADMIN_CONFIG_STR_WIDE_ROW); + String idx = containerId + "[" + getRowIdx() + "]"; + cmd.set(idx + " #SettingLabel.Text", label); + cmd.set(idx + " #SettingLabel.Style.TextColor", color); + cmd.set(idx + " #StrInput.Value", effectiveValue); + if (pending) cmd.set(idx + " #StrInput.Style.TextColor", "#FFAA00"); + events.addEventBinding(CustomUIEventBindingType.ValueChanged, idx + " #StrInput", + EventData.of("Button", "SetStringValue").append("SettingKey", key) + .append("@strInput", idx + " #StrInput.Value"), false); + settingSelectors.put(key, idx); + settingKinds.put(key, SettingKind.STRING); + incrementRowIdx(); + } + + private void addActionButton(UICommandBuilder cmd, UIEventBuilder events, + String action, String label, boolean disabled) { + String containerId = getContainerId(); + cmd.append(containerId, UIPaths.ADMIN_CONFIG_ACTION_BTN); + String idx = containerId + "[" + getRowIdx() + "]"; + cmd.set(idx + " #ActionBtn.Text", label); + if (disabled) { + cmd.set(idx + " #ActionBtn.Disabled", true); + } + events.addEventBinding(CustomUIEventBindingType.Activating, idx + " #ActionBtn", + EventData.of("Button", action), false); + incrementRowIdx(); + } + + // ================================================================ + // Event Handling + // ================================================================ + @Override public void handleDataEvent(Ref ref, Store store, - AdminConfigData data) { + AdminConfigData data) { super.handleDataEvent(ref, store, data); Player player = store.getComponent(ref, Player.getComponentType()); PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType()); - if (player == null || playerRef == null) { - return; - } + if (player == null || playerRef == null) return; - // Handle admin nav bar navigation if (AdminNavBarHelper.handleNavEvent(data, player, ref, store, playerRef, guiManager)) { return; } - // Handle other button events (placeholder for future implementation) if (data.button != null) { switch (data.button) { case "Back" -> guiManager.closePage(player, ref, store); - default -> throw new IllegalStateException("Unexpected value"); + + case "TabSwitch" -> { + if (data.tab != null) { + LayoutSize oldSize = getLayoutSize(currentTab); + currentTab = data.tab; + saveConfirmActive = false; + resetConfirmActive = false; + if (getLayoutSize(currentTab) != oldSize) { + rebuild(); + } else { + refresh(ref, store); + } + } + } + + case "ToggleSetting" -> { + if (data.settingKey != null) { + handleToggle(data.settingKey); + // If toggling a parent facperm default, refresh to rebuild child disabled states + if (data.settingKey.startsWith("facperm.default.")) { + String flag = data.settingKey.substring("facperm.default.".length()); + if (FactionPermissions.isParentFlag(flag)) { + refresh(ref, store); + break; + } + } + updateSettingAndStatus(ref, store, data.settingKey); + } + } + + case "IncrementSetting" -> { + if (data.settingKey != null) { + handleIncrement(data.settingKey, true); + updateSettingAndStatus(ref, store, data.settingKey); + } + } + + case "DecrementSetting" -> { + if (data.settingKey != null) { + handleIncrement(data.settingKey, false); + updateSettingAndStatus(ref, store, data.settingKey); + } + } + + case "SetNumericValue" -> { + if (data.settingKey != null && data.numInput != null) { + handleNumericInput(data.settingKey, data.numInput); + debouncedStatusUpdate(ref, store, data.settingKey); + } + } + + case "SetStringValue" -> { + if (data.settingKey != null && data.strInput != null) { + handleStringInput(data.settingKey, data.strInput); + debouncedStatusUpdate(ref, store, data.settingKey); + } + } + + case "SetColorValue" -> { + if (data.settingKey != null && data.colorValue != null) { + handleColorInput(data.settingKey, data.colorValue); + updateSettingAndStatus(ref, store, data.settingKey); + } + } + + case "TypeColorValue" -> { + if (data.settingKey != null && data.strInput != null) { + handleColorInput(data.settingKey, data.strInput); + debouncedStatusUpdate(ref, store, data.settingKey); + } + } + + case "EnumChanged" -> { + if (data.settingKey != null && data.enumValue != null) { + handleEnumInput(data.settingKey, data.enumValue); + // Worldmap refresh mode and scaling mode changes rebuild the tab + if ("worldmap.refreshMode".equals(data.settingKey) + || "economy.upkeepScalingMode".equals(data.settingKey)) { + refresh(ref, store); + } else { + updateSettingAndStatus(ref, store, data.settingKey); + } + } + } + + case "EditScalingTiers" -> { + ScalingTiersModalPage modal = new ScalingTiersModalPage(playerRef, guiManager, this); + player.getPageManager().openCustomPage(ref, store, modal); + } + + case "RemoveWorld" -> { + if (data.settingKey != null) { + handleRemoveWorld(data.settingKey); + refresh(ref, store); + } + } + + case "AddWorld" -> { + if (data.strInput != null && !data.strInput.trim().isEmpty()) { + handleAddWorld(data.strInput.trim()); + refresh(ref, store); + } + } + + case "ToggleWorldExpand" -> { + if (data.settingKey != null) { + if (expandedWorlds.contains(data.settingKey)) { + expandedWorlds.remove(data.settingKey); + } else { + expandedWorlds.add(data.settingKey); + } + refresh(ref, store); + } + } + + case "WorldSettingChanged" -> { + if (data.settingKey != null && data.enumValue != null) { + handleWorldSettingChanged(data.settingKey, data.enumValue); + refresh(ref, store); + } + } + + case "Save" -> { + if (!invalidFields.isEmpty()) { + // Can't save with invalid fields + break; + } + if (!saveConfirmActive) { + saveConfirmActive = true; + resetConfirmActive = false; + refresh(ref, store); + } else { + applyAndSave(); + saveConfirmActive = false; + resetConfirmActive = false; + editSessions.remove(playerRef.getUuid()); + refresh(ref, store); + } + } + + case "Revert" -> { + pendingChanges.clear(); + invalidFields.clear(); + pendingWorldOverrides = null; + expandedWorlds.clear(); + saveConfirmActive = false; + resetConfirmActive = false; + editSessions.remove(playerRef.getUuid()); + refresh(ref, store); + } + + case "ResetDefaults" -> { + if (!resetConfirmActive) { + resetConfirmActive = true; + refresh(ref, store); + } else { + ConfigManager.get().resetAllDefaults(); + guiManager.getPlugin().get().reloadRuntimeSystems(); + pendingChanges.clear(); + originalValues.clear(); + pendingWorldOverrides = null; + expandedWorlds.clear(); + resetConfirmActive = false; + editSessions.remove(playerRef.getUuid()); + refresh(ref, store); + } + } + + default -> { } } } } + + private void handleToggle(String key) { + Object orig = originalValues.get(key); + boolean current; + if (pendingChanges.containsKey(key)) { + current = (Boolean) pendingChanges.get(key); + } else if (orig instanceof Boolean b) { + current = b; + } else { + return; + } + boolean newVal = !current; + if (orig instanceof Boolean b && newVal == b) { + pendingChanges.remove(key); + } else { + pendingChanges.put(key, newVal); + } + } + + private void handleIncrement(String key, boolean increment) { + Object orig = originalValues.get(key); + if (orig instanceof Integer origInt) { + int step = ConfigSnapshot.getIntStep(key); + int current = pendingChanges.containsKey(key) ? ((Number) pendingChanges.get(key)).intValue() : origInt; + int newVal = increment ? current + step : current - step; + newVal = Math.max(ConfigValidator.getIntMin(key), Math.min(ConfigValidator.getIntMax(key), newVal)); + if (newVal == origInt) { + pendingChanges.remove(key); + } else { + pendingChanges.put(key, newVal); + } + } else if (orig instanceof Double origDbl) { + double step = ConfigSnapshot.getDoubleStep(key); + double current = pendingChanges.containsKey(key) ? ((Number) pendingChanges.get(key)).doubleValue() : origDbl; + double newVal = increment ? current + step : current - step; + newVal = Math.max(ConfigValidator.getDoubleMin(key), Math.min(ConfigValidator.getDoubleMax(key), newVal)); + newVal = Math.round(newVal * 100.0) / 100.0; + if (Math.abs(newVal - origDbl) < 0.001) { + pendingChanges.remove(key); + } else { + pendingChanges.put(key, newVal); + } + } + } + + private void handleNumericInput(String key, String input) { + Object orig = originalValues.get(key); + if (input == null || input.isBlank()) { + invalidFields.remove(key); + return; + } + if (orig instanceof Integer origInt) { + if (!isValidInt(input)) { + invalidFields.add(key); + return; + } + invalidFields.remove(key); + int newVal = ConfigValidator.clampInt(input, origInt, + ConfigValidator.getIntMin(key), ConfigValidator.getIntMax(key)); + if (newVal == origInt) pendingChanges.remove(key); + else pendingChanges.put(key, newVal); + } else if (orig instanceof Double origDbl) { + if (!isValidDouble(input)) { + invalidFields.add(key); + return; + } + invalidFields.remove(key); + double newVal = ConfigValidator.clampDouble(input, origDbl, + ConfigValidator.getDoubleMin(key), ConfigValidator.getDoubleMax(key)); + if (Math.abs(newVal - origDbl) < 0.001) pendingChanges.remove(key); + else pendingChanges.put(key, newVal); + } + } + + private static boolean isValidInt(String input) { + try { Integer.parseInt(input.trim()); return true; } + catch (NumberFormatException e) { return false; } + } + + private static boolean isValidDouble(String input) { + try { + double v = Double.parseDouble(input.trim()); + return !Double.isNaN(v) && !Double.isInfinite(v); + } catch (NumberFormatException e) { return false; } + } + + private void handleStringInput(String key, String input) { + Object orig = originalValues.get(key); + String validated = ConfigValidator.validateString(input, 256); + if (orig instanceof String origStr && validated.equals(origStr)) { + pendingChanges.remove(key); + } else { + pendingChanges.put(key, validated); + } + } + + private void handleColorInput(String key, String rawColor) { + Object orig = originalValues.get(key); + String origStr = orig instanceof String s ? s : "#FFFFFF"; + // ColorPicker returns #RRGGBBAA — strip alpha to get #RRGGBB + String hex = rawColor != null && rawColor.length() >= 7 + ? rawColor.substring(0, 7).toUpperCase() : rawColor; + if (hex != null && !hex.isBlank() && !hex.matches("#[0-9A-Fa-f]{6}")) { + invalidFields.add(key); + return; + } + invalidFields.remove(key); + String validated = ConfigValidator.validateColor(hex, origStr); + if (validated.equals(origStr)) { + pendingChanges.remove(key); + } else { + pendingChanges.put(key, validated); + } + } + + private void handleEnumInput(String key, String value) { + Object orig = originalValues.get(key); + if (orig instanceof String origStr && value.equals(origStr)) { + pendingChanges.remove(key); + } else { + pendingChanges.put(key, value); + } + } + + private void handleRemoveWorld(String worldKey) { + LinkedHashMap overrides = ensurePendingWorldOverrides(); + overrides.remove(worldKey); + // Remove any pending per-setting overrides for this world + pendingChanges.keySet().removeIf(k -> k.startsWith("worlds.override." + worldKey + ".")); + expandedWorlds.remove(worldKey); + } + + private void handleAddWorld(String worldName) { + LinkedHashMap overrides = ensurePendingWorldOverrides(); + if (!overrides.containsKey(worldName)) { + overrides.put(worldName, WorldsConfig.WorldSettings.DEFAULTS); + } + } + + private void handleWorldSettingChanged(String key, String value) { + // key format: worlds.override.{worldName}.{setting} + String remainder = key.substring("worlds.override.".length()); + int dot = remainder.lastIndexOf('.'); + if (dot <= 0) return; + String worldKey = remainder.substring(0, dot); + String setting = remainder.substring(dot + 1); + + LinkedHashMap overrides = ensurePendingWorldOverrides(); + WorldsConfig.WorldSettings current = overrides.getOrDefault(worldKey, WorldsConfig.WorldSettings.DEFAULTS); + Boolean val = triStateFromString(value); + WorldsConfig.WorldSettings updated = switch (setting) { + case "claiming" -> new WorldsConfig.WorldSettings(val, current.powerLoss(), current.friendlyFireFaction(), current.friendlyFireAlly()); + case "powerLoss" -> new WorldsConfig.WorldSettings(current.claiming(), val, current.friendlyFireFaction(), current.friendlyFireAlly()); + case "friendlyFireFaction" -> new WorldsConfig.WorldSettings(current.claiming(), current.powerLoss(), val, current.friendlyFireAlly()); + case "friendlyFireAlly" -> new WorldsConfig.WorldSettings(current.claiming(), current.powerLoss(), current.friendlyFireFaction(), val); + default -> current; + }; + overrides.put(worldKey, updated); + } + + /** + * Debounced status update for text input fields (numeric, string, color). + * Updates only the label color + status bar after the user stops typing. + */ + private void debouncedStatusUpdate(Ref ref, Store store, String key) { + long ts = System.nanoTime(); + lastTextInputNanos = ts; + final var capturedRef = ref; + final var capturedStore = store; + final var capturedKey = key; + CompletableFuture.delayedExecutor(DEBOUNCE_MS, TimeUnit.MILLISECONDS) + .execute(() -> { + if (lastTextInputNanos == ts) { + // Only update label color + status, not the input value + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + String selector = settingSelectors.get(capturedKey); + if (selector != null) { + boolean invalid = invalidFields.contains(capturedKey); + boolean pending = pendingChanges.containsKey(capturedKey); + String labelColor = invalid ? "#FF4444" : (pending ? "#FFAA00" : "#CCCCCC"); + cmd.set(selector + " #SettingLabel.Style.TextColor", labelColor); + + SettingKind kind = settingKinds.get(capturedKey); + // Show red on the input field itself if invalid + if (invalid) { + if (kind == SettingKind.INT || kind == SettingKind.DOUBLE) { + cmd.set(selector + " #NumInput.Style.TextColor", "#FF4444"); + } else if (kind == SettingKind.COLOR) { + cmd.set(selector + " #ColorInput.Style.TextColor", "#FF4444"); + } + } + // Update color picker preview when typing a valid hex color + if (kind == SettingKind.COLOR && pending && !invalid) { + String colorVal = String.valueOf(pendingChanges.get(capturedKey)); + cmd.set(selector + " #ColorPicker.Color", colorVal); + } + } + updateStatusLabel(cmd); + sendUpdate(cmd, events, false); + } + }); + } + + @SuppressWarnings("unchecked") + private void applyAndSave() { + ConfigManager cfg = ConfigManager.get(); + + for (var entry : pendingChanges.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if ("worlds.defaultPolicy".equals(key)) { + cfg.worlds().setDefaultPolicy(String.valueOf(value)); + } else { + ConfigSnapshot.applyChange(key, value); + } + } + // Apply pending world overrides (add/remove worlds) + if (pendingWorldOverrides != null) { + WorldsConfig worldsCfg = cfg.worlds(); + // Clear existing and replace with pending + for (String key : new ArrayList<>(worldsCfg.getWorlds().keySet())) { + worldsCfg.removeWorldSettings(key); + } + for (var entry : pendingWorldOverrides.entrySet()) { + worldsCfg.setWorldSettings(entry.getKey(), entry.getValue()); + } + pendingWorldOverrides = null; + } + + cfg.saveAll(); + + // Restart interval-based systems to pick up changed values immediately + guiManager.getPlugin().get().reloadRuntimeSystems(); + + for (var entry : pendingChanges.entrySet()) { + originalValues.put(entry.getKey(), entry.getValue()); + } + pendingChanges.clear(); + Logger.info("[ConfigEditor] Config changes saved and runtime systems reloaded"); + } + + /** Full page refresh — used for tab switches, save, revert, reset. */ + private void refresh(Ref ref, Store store) { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + buildDynamicContent(cmd, events); + sendUpdate(cmd, events, false); + } + + // ================================================================ + // Utilities + // ================================================================ + + private String loc(String key) { + return HFMessages.get(playerRef, key); + } + + private static String escUi(String text) { + if (text == null) return ""; + return text.replace("\"", "'").replace("\\", ""); + } + + @Override + public void onDismiss(Ref ref, Store store) { + super.onDismiss(ref, store); + saveSession(); + } } diff --git a/src/main/java/com/hyperfactions/gui/admin/page/AdminUpdatesPage.java b/src/main/java/com/hyperfactions/gui/admin/page/AdminUpdatesPage.java index 8b9f1a41..a537d378 100644 --- a/src/main/java/com/hyperfactions/gui/admin/page/AdminUpdatesPage.java +++ b/src/main/java/com/hyperfactions/gui/admin/page/AdminUpdatesPage.java @@ -1,79 +1,372 @@ package com.hyperfactions.gui.admin.page; +import com.hyperfactions.BuildInfo; +import com.hyperfactions.HyperFactions; +import com.hyperfactions.config.ConfigManager; import com.hyperfactions.gui.GuiManager; import com.hyperfactions.gui.UIPaths; import com.hyperfactions.gui.admin.AdminNavBarHelper; import com.hyperfactions.gui.admin.data.AdminUpdatesData; -import com.hyperfactions.util.HFMessages; +import com.hyperfactions.integration.protection.ProtectionMixinBridge; +import com.hyperfactions.update.UpdateChecker; import com.hyperfactions.util.AdminGuiKeys; +import com.hyperfactions.util.HFMessages; +import com.hyperfactions.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.Universe; +import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.jetbrains.annotations.Nullable; /** - * Admin Updates page - placeholder for update management. + * Admin Updates page — two-column layout showing HyperFactions and HyperProtect Mixin + * version info, update status, and download/rollback actions. */ public class AdminUpdatesPage extends InteractiveCustomUIPage { - private final PlayerRef playerRef; + private static final DateTimeFormatter BUILD_DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault()); + private final PlayerRef playerRef; private final GuiManager guiManager; + private final HyperFactions plugin; + + @Nullable private UpdateChecker.UpdateInfo cachedUpdate; + @Nullable private UpdateChecker.UpdateInfo cachedMixinUpdate; + private String hfStatus = ""; + private String hpStatus = ""; + private boolean downloading = false; + private boolean downloadingMixin = false; + private boolean rollbackConfirm = false; + private boolean restartRequired = false; - /** Creates a new AdminUpdatesPage. */ - public AdminUpdatesPage(PlayerRef playerRef, GuiManager guiManager) { + public AdminUpdatesPage(PlayerRef playerRef, GuiManager guiManager, HyperFactions plugin) { super(playerRef, CustomPageLifetime.CanDismiss, AdminUpdatesData.CODEC); this.playerRef = playerRef; this.guiManager = guiManager; + this.plugin = plugin; } - /** Builds . */ @Override public void build(Ref ref, UICommandBuilder cmd, - UIEventBuilder events, Store store) { - // Load the placeholder template first (nav bar elements must exist before setupBar) + UIEventBuilder events, Store store) { cmd.append(UIPaths.ADMIN_UPDATES); - - // Setup admin nav bar (must be after template load) AdminNavBarHelper.setupBar(playerRef, "updates", cmd, events); - - // Localize page title and labels cmd.set("#PageTitle.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_TITLE_UPDATES)); - cmd.set("#ComingSoon.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_UPDATES_HEADING)); - cmd.set("#ComingSoonSub.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_COMING_SOON)); - cmd.set("#Description.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_UPDATES_DESC1)); - cmd.set("#Description2.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.GUI_UPDATES_DESC2)); + buildDynamicContent(cmd, events); + } + + private void buildDynamicContent(UICommandBuilder cmd, UIEventBuilder events) { + buildHyperFactionsColumn(cmd, events); + buildHyperProtectColumn(cmd, events); + buildActionBar(cmd, events); + } + + // ── HyperFactions column ────────────────────────────────────────────────── + + private void buildHyperFactionsColumn(UICommandBuilder cmd, UIEventBuilder events) { + UpdateChecker checker = plugin.getUpdateChecker(); + + // Pre-populate from cached check + if (cachedUpdate == null && checker != null) { + cachedUpdate = checker.getCachedUpdate(); + } + + cmd.set("#HFCurrentVersion.Text", "v" + BuildInfo.VERSION); + cmd.set("#HFChannel.Text", ConfigManager.get().getReleaseChannel()); + cmd.set("#HFBuildDate.Text", BUILD_DATE_FORMATTER.format( + Instant.ofEpochMilli(BuildInfo.BUILD_TIMESTAMP))); + + if (cachedUpdate != null) { + String latestText = "v" + cachedUpdate.version(); + if (cachedUpdate.isPreRelease()) { + latestText += " (pre-release)"; + } + cmd.set("#HFLatestVersion.Text", latestText); + if (hfStatus.isEmpty()) { + hfStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_STATUS_AVAILABLE, + cachedUpdate.version()); + } + + if (!downloading && !restartRequired) { + cmd.set("#DownloadBtn.Visible", true); + cmd.set("#DownloadBtn.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_BTN_DOWNLOAD)); + events.addEventBinding(CustomUIEventBindingType.Activating, "#DownloadBtn", + EventData.of("Button", "Download"), false); + } else { + cmd.set("#DownloadBtn.Visible", false); + } + + if (cachedUpdate.changelog() != null && !cachedUpdate.changelog().isEmpty()) { + cmd.set("#ChangelogSection.Visible", true); + String changelog = cachedUpdate.changelog(); + if (changelog.length() > 500) changelog = changelog.substring(0, 497) + "..."; + cmd.set("#ChangelogText.Text", changelog); + } else { + cmd.set("#ChangelogSection.Visible", false); + } + } else { + cmd.set("#HFLatestVersion.Text", "v" + BuildInfo.VERSION); + cmd.set("#DownloadBtn.Visible", false); + cmd.set("#ChangelogSection.Visible", false); + if (hfStatus.isEmpty()) { + hfStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_STATUS_UP_TO_DATE); + } + } + + cmd.set("#HFStatus.Text", hfStatus); + } + + // ── HyperProtect column ─────────────────────────────────────────────────── + + private void buildHyperProtectColumn(UICommandBuilder cmd, UIEventBuilder events) { + ProtectionMixinBridge.MixinProvider provider = ProtectionMixinBridge.getProvider(); + boolean mixinInstalled = provider == ProtectionMixinBridge.MixinProvider.HYPERPROTECT + || provider == ProtectionMixinBridge.MixinProvider.BOTH; + UpdateChecker mixinChecker = plugin.getHyperProtectUpdateChecker(); + + if (mixinInstalled) { + String hpVersion = System.getProperty("hyperprotect.bridge.version", "unknown"); + cmd.set("#HPCurrentVersion.Text", "v" + hpVersion); + + if (cachedMixinUpdate != null) { + cmd.set("#HPLatestVersion.Text", "v" + cachedMixinUpdate.version()); + if (hpStatus.isEmpty()) { + hpStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_MIXIN_AVAILABLE, + cachedMixinUpdate.version()); + } + + if (!downloadingMixin && !restartRequired) { + cmd.set("#DownloadMixinBtn.Visible", true); + cmd.set("#DownloadMixinBtn.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_BTN_DOWNLOAD)); + events.addEventBinding(CustomUIEventBindingType.Activating, "#DownloadMixinBtn", + EventData.of("Button", "DownloadMixin"), false); + } else { + cmd.set("#DownloadMixinBtn.Visible", false); + } + } else { + cmd.set("#HPLatestVersion.Text", "v" + hpVersion); + cmd.set("#DownloadMixinBtn.Visible", false); + if (hpStatus.isEmpty()) { + hpStatus = mixinChecker != null + ? HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_MIXIN_UP_TO_DATE) + : "Installed"; + } + } + } else { + cmd.set("#HPCurrentVersion.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_MIXIN_NOT_INSTALLED)); + cmd.set("#HPLatestVersion.Text", "-"); + cmd.set("#DownloadMixinBtn.Visible", false); + if (hpStatus.isEmpty()) { + hpStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_MIXIN_NOT_INSTALLED); + } + } + + cmd.set("#HPStatus.Text", hpStatus); + } + + // ── Action bar ──────────────────────────────────────────────────────────── + + private void buildActionBar(UICommandBuilder cmd, UIEventBuilder events) { + // Check for Updates — checks both HF and HP at once + events.addEventBinding(CustomUIEventBindingType.Activating, "#CheckUpdateBtn", + EventData.of("Button", "CheckUpdate"), false); + + // Rollback + UpdateChecker checker = plugin.getUpdateChecker(); + if (checker != null && checker.isRollbackSafe()) { + UpdateChecker.RollbackInfo rollbackInfo = checker.getRollbackInfo(); + if (rollbackInfo != null) { + cmd.set("#RollbackBtn.Visible", true); + String rollbackLabel = rollbackConfirm + ? HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_ROLLBACK_CONFIRM) + : HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_BTN_ROLLBACK) + + " v" + rollbackInfo.fromVersion(); + cmd.set("#RollbackBtn.Text", rollbackLabel); + events.addEventBinding(CustomUIEventBindingType.Activating, "#RollbackBtn", + EventData.of("Button", "Rollback"), false); + } else { + cmd.set("#RollbackBtn.Visible", false); + } + } else { + cmd.set("#RollbackBtn.Visible", false); + } + + // Restart note + if (restartRequired) { + cmd.set("#RestartNote.Visible", true); + cmd.set("#RestartNote.Text", HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_RESTART_REQUIRED)); + } else { + cmd.set("#RestartNote.Visible", false); + } + } + + // ── Event handling ──────────────────────────────────────────────────────── + + @Nullable + private World resolveWorld() { + UUID worldUuid = playerRef.getWorldUuid(); + return worldUuid != null ? Universe.get().getWorld(worldUuid) : null; + } + + private void refresh() { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + buildDynamicContent(cmd, events); + sendUpdate(cmd, events, false); } - /** Handles data event. */ @Override public void handleDataEvent(Ref ref, Store store, - AdminUpdatesData data) { + AdminUpdatesData data) { super.handleDataEvent(ref, store, data); - Player player = store.getComponent(ref, Player.getComponentType()); - PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType()); + PlayerRef pRef = store.getComponent(ref, PlayerRef.getComponentType()); + if (player == null || pRef == null) return; + if (AdminNavBarHelper.handleNavEvent(data, player, ref, store, pRef, guiManager)) return; + if (data.button == null) return; - if (player == null || playerRef == null) { + switch (data.button) { + case "CheckUpdate" -> handleCheckAll(); + case "Download" -> handleDownload(); + case "DownloadMixin" -> handleDownloadMixin(); + case "Rollback" -> handleRollback(player); + case "Back" -> guiManager.closePage(player, ref, store); + default -> { } + } + } + + /** Checks both HF and HP updates simultaneously. */ + private void handleCheckAll() { + hfStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_STATUS_CHECKING); + hpStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_STATUS_CHECKING); + refresh(); + + UpdateChecker checker = plugin.getUpdateChecker(); + UpdateChecker mixinChecker = plugin.getHyperProtectUpdateChecker(); + + CompletableFuture hfFuture = checker != null + ? checker.checkForUpdates(true) : CompletableFuture.completedFuture(null); + CompletableFuture hpFuture = mixinChecker != null + ? mixinChecker.checkForUpdates(true) : CompletableFuture.completedFuture(null); + + hfFuture.thenCombine(hpFuture, (hfInfo, hpInfo) -> { + World world = resolveWorld(); + if (world == null) return null; + world.execute(() -> { + cachedUpdate = hfInfo; + hfStatus = hfInfo != null + ? HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_STATUS_AVAILABLE, hfInfo.version()) + : HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_STATUS_UP_TO_DATE); + + cachedMixinUpdate = hpInfo; + if (mixinChecker != null) { + hpStatus = hpInfo != null + ? HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_MIXIN_AVAILABLE, hpInfo.version()) + : HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_MIXIN_UP_TO_DATE); + } else { + hpStatus = ""; + } + refresh(); + }); + return null; + }); + } + + private void handleDownload() { + UpdateChecker checker = plugin.getUpdateChecker(); + if (checker == null || cachedUpdate == null || downloading) return; + + downloading = true; + hfStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_STATUS_DOWNLOADING); + refresh(); + + checker.downloadUpdate(cachedUpdate).thenAccept(path -> { + World world = resolveWorld(); + if (world == null) { downloading = false; return; } + world.execute(() -> { + downloading = false; + if (path != null) { + hfStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_STATUS_DOWNLOADED); + restartRequired = true; + checker.createRollbackMarker(BuildInfo.VERSION, cachedUpdate.version()); + checker.cleanupOldBackups(BuildInfo.VERSION); + Logger.info("[Updates] %s downloaded update v%s via admin GUI", + playerRef.getUsername(), cachedUpdate.version()); + } else { + hfStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_STATUS_FAILED); + } + refresh(); + }); + }); + } + + private void handleDownloadMixin() { + UpdateChecker mixinChecker = plugin.getHyperProtectUpdateChecker(); + if (mixinChecker == null || cachedMixinUpdate == null || downloadingMixin) return; + + downloadingMixin = true; + hpStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_MIXIN_DOWNLOADING); + refresh(); + + mixinChecker.downloadUpdate(cachedMixinUpdate).thenAccept(path -> { + World world = resolveWorld(); + if (world == null) { downloadingMixin = false; return; } + world.execute(() -> { + downloadingMixin = false; + if (path != null) { + hpStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_MIXIN_DOWNLOADED); + restartRequired = true; + Logger.info("[Updates] %s downloaded mixin update v%s via admin GUI", + playerRef.getUsername(), cachedMixinUpdate.version()); + } else { + hpStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_MIXIN_FAILED); + } + refresh(); + }); + }); + } + + private void handleRollback(Player player) { + UpdateChecker checker = plugin.getUpdateChecker(); + if (checker == null || !checker.isRollbackSafe()) { + hfStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_ROLLBACK_UNSAFE); + refresh(); return; } - // Handle admin nav bar navigation - if (AdminNavBarHelper.handleNavEvent(data, player, ref, store, playerRef, guiManager)) { + if (!rollbackConfirm) { + rollbackConfirm = true; + refresh(); return; } - // Handle other button events (placeholder for future implementation) - if (data.button != null) { - switch (data.button) { - case "Back" -> guiManager.closePage(player, ref, store); - default -> throw new IllegalStateException("Unexpected value"); - } + rollbackConfirm = false; + UpdateChecker.RollbackResult result = checker.performRollback(); + if (result.success()) { + hfStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_ROLLBACK_SUCCESS, + result.restoredVersion()); + restartRequired = true; + Logger.info("[Updates] %s rolled back to v%s via admin GUI", + playerRef.getUsername(), result.restoredVersion()); + } else { + hfStatus = HFMessages.get(playerRef, AdminGuiKeys.AdminGui.UPD_ROLLBACK_FAILED, + result.errorMessage()); } + refresh(); } } diff --git a/src/main/java/com/hyperfactions/gui/admin/page/ScalingTiersModalPage.java b/src/main/java/com/hyperfactions/gui/admin/page/ScalingTiersModalPage.java new file mode 100644 index 00000000..1d12ead5 --- /dev/null +++ b/src/main/java/com/hyperfactions/gui/admin/page/ScalingTiersModalPage.java @@ -0,0 +1,249 @@ +package com.hyperfactions.gui.admin.page; + +import com.hyperfactions.config.ConfigManager; +import com.hyperfactions.config.modules.EconomyConfig; +import com.hyperfactions.gui.GuiManager; +import com.hyperfactions.gui.UIPaths; +import com.hyperfactions.gui.admin.data.ScalingTiersData; +import com.hyperfactions.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 java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; + +/** + * Modal page for editing upkeep scaling tiers. + * Allows add/remove/edit of tier entries, saves directly to config on confirm. + * Shows a live cost example that updates as tiers are edited. + */ +public class ScalingTiersModalPage extends InteractiveCustomUIPage { + + private final PlayerRef playerRef; + private final GuiManager guiManager; + private final List tiers; + private final AdminConfigPage parentPage; + + /** Creates a new ScalingTiersModalPage. */ + public ScalingTiersModalPage(PlayerRef playerRef, GuiManager guiManager) { + this(playerRef, guiManager, null); + } + + /** Creates a new ScalingTiersModalPage that returns to an existing config page. */ + public ScalingTiersModalPage(PlayerRef playerRef, GuiManager guiManager, AdminConfigPage parentPage) { + super(playerRef, CustomPageLifetime.CanDismiss, ScalingTiersData.CODEC); + this.playerRef = playerRef; + this.guiManager = guiManager; + this.parentPage = parentPage; + this.tiers = new ArrayList<>(ConfigManager.get().economy().getUpkeepScalingTiers()); + } + + @Override + public void build(Ref ref, UICommandBuilder cmd, + UIEventBuilder events, Store store) { + cmd.append(UIPaths.ADMIN_CONFIG_SCALING_MODAL); + buildTierList(cmd, events); + updateExample(cmd); + + events.addEventBinding(CustomUIEventBindingType.Activating, "#AddTierBtn", + EventData.of("Button", "AddTier"), false); + events.addEventBinding(CustomUIEventBindingType.Activating, "#SaveBtn", + EventData.of("Button", "Save"), false); + events.addEventBinding(CustomUIEventBindingType.Activating, "#CancelBtn", + EventData.of("Button", "Cancel"), false); + } + + private void buildTierList(UICommandBuilder cmd, UIEventBuilder events) { + cmd.clear("#TierContainer"); + for (int i = 0; i < tiers.size(); i++) { + EconomyConfig.ScalingTier tier = tiers.get(i); + cmd.append("#TierContainer", UIPaths.ADMIN_CONFIG_SCALING_ENTRY); + String idx = "#TierContainer[" + i + "]"; + cmd.set(idx + " #ChunkInput.Value", String.valueOf(tier.chunkCount())); + cmd.set(idx + " #CostInput.Value", tier.costPerChunk().toPlainString()); + + if (i == 0) cmd.set(idx + " #UpBtn.Disabled", true); + events.addEventBinding(CustomUIEventBindingType.Activating, idx + " #UpBtn", + EventData.of("Button", "MoveTierUp").append("TierIndex", String.valueOf(i)), false); + if (i == tiers.size() - 1) cmd.set(idx + " #DownBtn.Disabled", true); + events.addEventBinding(CustomUIEventBindingType.Activating, idx + " #DownBtn", + EventData.of("Button", "MoveTierDown").append("TierIndex", String.valueOf(i)), false); + events.addEventBinding(CustomUIEventBindingType.Activating, idx + " #RemoveBtn", + EventData.of("Button", "RemoveTier").append("TierIndex", String.valueOf(i)), false); + events.addEventBinding(CustomUIEventBindingType.ValueChanged, idx + " #ChunkInput", + EventData.of("Button", "EditChunk").append("TierIndex", String.valueOf(i)) + .append("@chunkInput", idx + " #ChunkInput.Value"), false); + events.addEventBinding(CustomUIEventBindingType.ValueChanged, idx + " #CostInput", + EventData.of("Button", "EditCost").append("TierIndex", String.valueOf(i)) + .append("@costInput", idx + " #CostInput.Value"), false); + } + } + + /** Calculates and updates the cost example label using total tier chunks + 10. */ + private void updateExample(UICommandBuilder cmd) { + int freeChunks = ConfigManager.get().economy().getUpkeepFreeChunks(); + // Dynamic example: sum of all tier chunk counts + 10 overflow + int tierTotal = 0; + for (EconomyConfig.ScalingTier tier : tiers) { + tierTotal += tier.chunkCount(); + } + int exampleChunks = tierTotal + 10 + freeChunks; + int billable = Math.max(0, exampleChunks - freeChunks); + BigDecimal cost = calculateTieredCost(billable); + String symbol = ConfigManager.get().economy().getCurrencySymbol(); + cmd.set("#ExampleLabel.Text", + "Example: " + exampleChunks + " chunks (" + freeChunks + " free, " + + billable + " billable) = " + symbol + + cost.setScale(2, RoundingMode.HALF_UP).toPlainString() + "/cycle"); + } + + /** Calculates the total cost for a given number of billable chunks using current tiers. */ + private BigDecimal calculateTieredCost(int billableChunks) { + BigDecimal total = BigDecimal.ZERO; + int remaining = billableChunks; + + for (EconomyConfig.ScalingTier tier : tiers) { + if (remaining <= 0) break; + int count = tier.chunkCount() > 0 ? Math.min(remaining, tier.chunkCount()) : remaining; + total = total.add(tier.costPerChunk().multiply(BigDecimal.valueOf(count))); + remaining -= count; + } + + // If chunks remain after all tiers, use the last tier's cost + if (remaining > 0 && !tiers.isEmpty()) { + BigDecimal lastCost = tiers.getLast().costPerChunk(); + total = total.add(lastCost.multiply(BigDecimal.valueOf(remaining))); + } + + return total; + } + + @Override + public void handleDataEvent(Ref ref, Store store, + ScalingTiersData data) { + super.handleDataEvent(ref, store, data); + + Player player = store.getComponent(ref, Player.getComponentType()); + PlayerRef pRef = store.getComponent(ref, PlayerRef.getComponentType()); + if (player == null || pRef == null || data.button == null) return; + + switch (data.button) { + case "Cancel" -> { + returnToParent(player, ref, store, pRef); + } + + case "Save" -> { + ConfigManager cfg = ConfigManager.get(); + cfg.economy().setUpkeepScalingTiers(new ArrayList<>(tiers)); + cfg.saveAll(); + Logger.info("[ConfigEditor] Scaling tiers saved by admin (%d tiers)", tiers.size()); + returnToParent(player, ref, store, pRef); + } + + case "AddTier" -> { + tiers.add(new EconomyConfig.ScalingTier(0, new BigDecimal("1.00"))); + refresh(ref, store); + } + + case "MoveTierUp" -> { + int idx = parseIndex(data.tierIndex); + if (idx > 0 && idx < tiers.size()) { + EconomyConfig.ScalingTier tier = tiers.remove(idx); + tiers.add(idx - 1, tier); + refresh(ref, store); + } + } + + case "MoveTierDown" -> { + int idx = parseIndex(data.tierIndex); + if (idx >= 0 && idx < tiers.size() - 1) { + EconomyConfig.ScalingTier tier = tiers.remove(idx); + tiers.add(idx + 1, tier); + refresh(ref, store); + } + } + + case "RemoveTier" -> { + int idx = parseIndex(data.tierIndex); + if (idx >= 0 && idx < tiers.size()) { + tiers.remove(idx); + refresh(ref, store); + } + } + + case "EditChunk" -> { + int idx = parseIndex(data.tierIndex); + if (idx >= 0 && idx < tiers.size() && data.chunkInput != null) { + try { + int chunks = Integer.parseInt(data.chunkInput.trim()); + chunks = Math.max(0, chunks); + EconomyConfig.ScalingTier old = tiers.get(idx); + tiers.set(idx, new EconomyConfig.ScalingTier(chunks, old.costPerChunk())); + } catch (NumberFormatException ignored) {} + } + refreshExample(ref, store); + } + + case "EditCost" -> { + int idx = parseIndex(data.tierIndex); + if (idx >= 0 && idx < tiers.size() && data.costInput != null) { + try { + BigDecimal cost = new BigDecimal(data.costInput.trim()); + if (cost.compareTo(BigDecimal.ZERO) < 0) cost = BigDecimal.ZERO; + EconomyConfig.ScalingTier old = tiers.get(idx); + tiers.set(idx, new EconomyConfig.ScalingTier(old.chunkCount(), cost)); + } catch (NumberFormatException ignored) {} + } + refreshExample(ref, store); + } + } + } + + private void refresh(Ref ref, Store store) { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + buildTierList(cmd, events); + updateExample(cmd); + events.addEventBinding(CustomUIEventBindingType.Activating, "#AddTierBtn", + EventData.of("Button", "AddTier"), false); + events.addEventBinding(CustomUIEventBindingType.Activating, "#SaveBtn", + EventData.of("Button", "Save"), false); + events.addEventBinding(CustomUIEventBindingType.Activating, "#CancelBtn", + EventData.of("Button", "Cancel"), false); + sendUpdate(cmd, events, false); + } + + /** Updates just the example label without rebuilding the tier list. */ + private void refreshExample(Ref ref, Store store) { + UICommandBuilder cmd = new UICommandBuilder(); + UIEventBuilder events = new UIEventBuilder(); + updateExample(cmd); + sendUpdate(cmd, events, false); + } + + /** Returns to the parent config page (preserving pending state) or opens a fresh one. */ + private void returnToParent(Player player, Ref ref, + Store store, PlayerRef pRef) { + if (parentPage != null) { + player.getPageManager().openCustomPage(ref, store, parentPage); + } else { + guiManager.openAdminConfig(player, ref, store, pRef, "economy"); + } + } + + private static int parseIndex(String s) { + if (s == null) return -1; + try { return Integer.parseInt(s); } + catch (NumberFormatException e) { return -1; } + } +} diff --git a/src/main/java/com/hyperfactions/migration/MigrationRegistry.java b/src/main/java/com/hyperfactions/migration/MigrationRegistry.java index 3bd713ea..39b6d8cf 100644 --- a/src/main/java/com/hyperfactions/migration/MigrationRegistry.java +++ b/src/main/java/com/hyperfactions/migration/MigrationRegistry.java @@ -6,6 +6,7 @@ import com.hyperfactions.migration.migrations.config.ConfigV4ToV5Migration; import com.hyperfactions.migration.migrations.config.ConfigV5ToV6Migration; import com.hyperfactions.migration.migrations.config.ConfigV6ToV7Migration; +import com.hyperfactions.migration.migrations.config.ConfigV7ToV8Migration; import com.hyperfactions.migration.migrations.data.DataV0ToV1Migration; import com.hyperfactions.migration.migrations.data.DataV1ToV2Migration; import java.nio.file.Path; @@ -55,6 +56,7 @@ private void registerBuiltInMigrations() { register(new ConfigV4ToV5Migration()); register(new ConfigV5ToV6Migration()); register(new ConfigV6ToV7Migration()); + register(new ConfigV7ToV8Migration()); // Data migrations register(new DataV0ToV1Migration()); diff --git a/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV7ToV8Migration.java b/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV7ToV8Migration.java new file mode 100644 index 00000000..e225a825 --- /dev/null +++ b/src/main/java/com/hyperfactions/migration/migrations/config/ConfigV7ToV8Migration.java @@ -0,0 +1,198 @@ +package com.hyperfactions.migration.migrations.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.hyperfactions.migration.Migration; +import com.hyperfactions.migration.MigrationOptions; +import com.hyperfactions.migration.MigrationResult; +import com.hyperfactions.migration.MigrationType; +import com.hyperfactions.util.ErrorHandler; +import com.hyperfactions.util.Logger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +/** + * Migrates configuration from v7 to v8. + * + *

+ * This migration: + *

    + *
  • Converts {@code claimBlacklist} entries from worlds.json into per-world + * settings with {@code claiming: false}, then removes the claimBlacklist field.
  • + *
+ */ +public class ConfigV7ToV8Migration implements Migration { + + private static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .disableHtmlEscaping() + .create(); + + /** Id. */ + @Override + @NotNull + public String id() { + return "config-v7-to-v8"; + } + + /** Type. */ + @Override + @NotNull + public MigrationType type() { + return MigrationType.CONFIG; + } + + /** Creates from version. */ + @Override + public int fromVersion() { + return 7; + } + + /** Converts to version. */ + @Override + public int toVersion() { + return 8; + } + + /** Description. */ + @Override + @NotNull + public String description() { + return "Convert claimBlacklist entries to per-world settings with claiming disabled"; + } + + /** Checks if applicable. */ + @Override + public boolean isApplicable(@NotNull Path dataDir) { + Path serverFile = dataDir.resolve("config/server.json"); + if (!Files.exists(serverFile)) { + return false; + } + + try { + String json = Files.readString(serverFile); + JsonObject root = JsonParser.parseString(json).getAsJsonObject(); + if (!root.has("configVersion")) { + return false; + } + return root.get("configVersion").getAsInt() == 7; + } catch (Exception e) { + Logger.warn("[Migration] Failed to check config version: %s", e.getMessage()); + return false; + } + } + + /** Executes the migration. */ + @Override + @NotNull + public MigrationResult execute(@NotNull Path dataDir, @NotNull MigrationOptions options) { + Instant startTime = Instant.now(); + List filesModified = new ArrayList<>(); + List warnings = new ArrayList<>(); + + try { + // === Step 1: Migrate claimBlacklist in worlds.json === + options.reportProgress("Migrating claimBlacklist in worlds.json", 1, 3); + + Path worldsFile = dataDir.resolve("config/worlds.json"); + if (Files.exists(worldsFile)) { + String worldsJson = Files.readString(worldsFile); + JsonObject worldsRoot = JsonParser.parseString(worldsJson).getAsJsonObject(); + + if (worldsRoot.has("claimBlacklist") && worldsRoot.get("claimBlacklist").isJsonArray()) { + JsonArray blacklist = worldsRoot.getAsJsonArray("claimBlacklist"); + + if (!blacklist.isEmpty()) { + // Ensure "worlds" object exists + JsonObject worldsObj; + if (worldsRoot.has("worlds") && worldsRoot.get("worlds").isJsonObject()) { + worldsObj = worldsRoot.getAsJsonObject("worlds"); + } else { + worldsObj = new JsonObject(); + worldsRoot.add("worlds", worldsObj); + } + + int migrated = 0; + for (JsonElement entry : blacklist) { + String worldName = entry.getAsString(); + if (!worldsObj.has(worldName)) { + // Create new per-world entry with claiming disabled + JsonObject worldSettings = new JsonObject(); + worldSettings.addProperty("claiming", false); + worldsObj.add(worldName, worldSettings); + migrated++; + Logger.info("[Migration] Converted blacklist entry '%s' to per-world claiming=false", worldName); + } else { + // World already has settings — ensure claiming is false + JsonObject existing = worldsObj.getAsJsonObject(worldName); + if (!existing.has("claiming") || existing.get("claiming").getAsBoolean()) { + existing.addProperty("claiming", false); + migrated++; + Logger.info("[Migration] Updated existing world '%s' to claiming=false (was blacklisted)", worldName); + } + } + } + + Logger.info("[Migration] Migrated %d claimBlacklist entries to per-world settings", migrated); + } + + // Remove the claimBlacklist field + worldsRoot.remove("claimBlacklist"); + Logger.info("[Migration] Removed claimBlacklist from worlds.json"); + + Files.writeString(worldsFile, GSON.toJson(worldsRoot)); + filesModified.add("config/worlds.json"); + } + } + + // === Step 2: Bump configVersion in server.json === + options.reportProgress("Bumping configVersion to 8", 2, 3); + + Path serverFile = dataDir.resolve("config/server.json"); + String serverJson = Files.readString(serverFile); + JsonObject serverRoot = JsonParser.parseString(serverJson).getAsJsonObject(); + serverRoot.addProperty("configVersion", 8); + Files.writeString(serverFile, GSON.toJson(serverRoot)); + filesModified.add("config/server.json"); + + // === Step 3: Done === + options.reportProgress("Migration complete", 3, 3); + + Duration duration = Duration.between(startTime, Instant.now()); + Logger.info("[Migration] Config migration v7->v8 completed in %dms", duration.toMillis()); + + return MigrationResult.success( + id(), + fromVersion(), + toVersion(), + options.backupPath(), + List.of(), + filesModified, + warnings, + duration + ); + + } catch (Exception e) { + Duration duration = Duration.between(startTime, Instant.now()); + ErrorHandler.report("[Migration] Config migration v7->v8 failed", e); + return MigrationResult.failure( + id(), + fromVersion(), + toVersion(), + options.backupPath(), + e.getMessage(), + false, + duration + ); + } + } +} diff --git a/src/main/java/com/hyperfactions/util/AdminGuiKeys.java b/src/main/java/com/hyperfactions/util/AdminGuiKeys.java index e8d31e55..3081efd0 100644 --- a/src/main/java/com/hyperfactions/util/AdminGuiKeys.java +++ b/src/main/java/com/hyperfactions/util/AdminGuiKeys.java @@ -363,6 +363,35 @@ public static final class AdminGui { public static final String GUI_BACKUP_HEADING = "hyperfactions_admin.gui.backup_heading"; public static final String GUI_BACKUP_DESC1 = "hyperfactions_admin.gui.backup_desc1"; public static final String GUI_BACKUP_DESC2 = "hyperfactions_admin.gui.backup_desc2"; + // Backups page labels + public static final String BKP_TITLE = "hyperfactions_admin.backups.title"; + public static final String BKP_TOTAL_COUNT = "hyperfactions_admin.backups.total_count"; + public static final String BKP_EMPTY = "hyperfactions_admin.backups.empty"; + public static final String BKP_BTN_CREATE = "hyperfactions_admin.backups.btn_create"; + public static final String BKP_BTN_RESTORE = "hyperfactions_admin.backups.btn_restore"; + public static final String BKP_BTN_DELETE = "hyperfactions_admin.backups.btn_delete"; + public static final String BKP_NAME_PLACEHOLDER = "hyperfactions_admin.backups.name_placeholder"; + public static final String BKP_CREATING = "hyperfactions_admin.backups.creating"; + public static final String BKP_CREATED = "hyperfactions_admin.backups.created"; + public static final String BKP_CREATE_FAILED = "hyperfactions_admin.backups.create_failed"; + public static final String BKP_TYPE_HOURLY = "hyperfactions_admin.backups.type_hourly"; + public static final String BKP_TYPE_DAILY = "hyperfactions_admin.backups.type_daily"; + public static final String BKP_TYPE_WEEKLY = "hyperfactions_admin.backups.type_weekly"; + public static final String BKP_TYPE_MANUAL = "hyperfactions_admin.backups.type_manual"; + public static final String BKP_TYPE_MIGRATION = "hyperfactions_admin.backups.type_migration"; + public static final String BKP_DETAIL_TYPE = "hyperfactions_admin.backups.detail_type"; + public static final String BKP_DETAIL_CREATED = "hyperfactions_admin.backups.detail_created"; + public static final String BKP_DETAIL_SIZE = "hyperfactions_admin.backups.detail_size"; + public static final String BKP_RESTORE_WARNING = "hyperfactions_admin.backups.restore_warning"; + public static final String BKP_RESTORE_CONFIRM = "hyperfactions_admin.backups.restore_confirm"; + public static final String BKP_RESTORING = "hyperfactions_admin.backups.restoring"; + public static final String BKP_RESTORED = "hyperfactions_admin.backups.restored"; + public static final String BKP_RESTORE_FAILED = "hyperfactions_admin.backups.restore_failed"; + public static final String BKP_DELETE_CONFIRM = "hyperfactions_admin.backups.delete_confirm"; + public static final String BKP_DELETING = "hyperfactions_admin.backups.deleting"; + public static final String BKP_DELETED = "hyperfactions_admin.backups.deleted"; + public static final String BKP_DELETE_FAILED = "hyperfactions_admin.backups.delete_failed"; + public static final String BKP_RELOAD_REQUIRED = "hyperfactions_admin.backups.reload_required"; public static final String GUI_CONFIG_HEADING = "hyperfactions_admin.gui.config_heading"; public static final String GUI_CONFIG_DESC1 = "hyperfactions_admin.gui.config_desc1"; public static final String GUI_CONFIG_DESC2 = "hyperfactions_admin.gui.config_desc2"; @@ -372,6 +401,40 @@ public static final class AdminGui { public static final String GUI_UPDATES_HEADING = "hyperfactions_admin.gui.updates_heading"; public static final String GUI_UPDATES_DESC1 = "hyperfactions_admin.gui.updates_desc1"; public static final String GUI_UPDATES_DESC2 = "hyperfactions_admin.gui.updates_desc2"; + // Updates page labels + public static final String UPD_CURRENT_VERSION = "hyperfactions_admin.updates.current_version"; + public static final String UPD_BUILD_DATE = "hyperfactions_admin.updates.build_date"; + public static final String UPD_LATEST_VERSION = "hyperfactions_admin.updates.latest_version"; + public static final String UPD_STATUS_CHECKING = "hyperfactions_admin.updates.status_checking"; + public static final String UPD_STATUS_UP_TO_DATE = "hyperfactions_admin.updates.status_up_to_date"; + public static final String UPD_STATUS_AVAILABLE = "hyperfactions_admin.updates.status_available"; + public static final String UPD_STATUS_DOWNLOADING = "hyperfactions_admin.updates.status_downloading"; + public static final String UPD_STATUS_DOWNLOADED = "hyperfactions_admin.updates.status_downloaded"; + public static final String UPD_STATUS_FAILED = "hyperfactions_admin.updates.status_failed"; + public static final String UPD_BTN_CHECK = "hyperfactions_admin.updates.btn_check"; + public static final String UPD_BTN_DOWNLOAD = "hyperfactions_admin.updates.btn_download"; + public static final String UPD_BTN_ROLLBACK = "hyperfactions_admin.updates.btn_rollback"; + public static final String UPD_CHANGELOG_TITLE = "hyperfactions_admin.updates.changelog_title"; + public static final String UPD_NO_CHANGELOG = "hyperfactions_admin.updates.no_changelog"; + public static final String UPD_ROLLBACK_CONFIRM = "hyperfactions_admin.updates.rollback_confirm"; + public static final String UPD_ROLLBACK_UNSAFE = "hyperfactions_admin.updates.rollback_unsafe"; + public static final String UPD_ROLLBACK_SUCCESS = "hyperfactions_admin.updates.rollback_success"; + public static final String UPD_ROLLBACK_FAILED = "hyperfactions_admin.updates.rollback_failed"; + public static final String UPD_MIXIN_TITLE = "hyperfactions_admin.updates.mixin_title"; + public static final String UPD_MIXIN_VERSION = "hyperfactions_admin.updates.mixin_version"; + public static final String UPD_MIXIN_CHECK = "hyperfactions_admin.updates.mixin_check"; + public static final String UPD_MIXIN_UP_TO_DATE = "hyperfactions_admin.updates.mixin_up_to_date"; + public static final String UPD_MIXIN_AVAILABLE = "hyperfactions_admin.updates.mixin_available"; + public static final String UPD_MIXIN_DOWNLOADING = "hyperfactions_admin.updates.mixin_downloading"; + public static final String UPD_MIXIN_DOWNLOADED = "hyperfactions_admin.updates.mixin_downloaded"; + public static final String UPD_MIXIN_FAILED = "hyperfactions_admin.updates.mixin_failed"; + public static final String UPD_RESTART_REQUIRED = "hyperfactions_admin.updates.restart_required"; + public static final String UPD_VERSION_INFO = "hyperfactions_admin.updates.version_info"; + public static final String UPD_UPDATE_STATUS = "hyperfactions_admin.updates.update_status"; + public static final String UPD_ROLLBACK_SECTION = "hyperfactions_admin.updates.rollback_section"; + public static final String UPD_PRE_RELEASE = "hyperfactions_admin.updates.pre_release"; + public static final String UPD_CHANNEL = "hyperfactions_admin.updates.channel"; + public static final String UPD_MIXIN_NOT_INSTALLED = "hyperfactions_admin.updates.mixin_not_installed"; // Version page labels public static final String GUI_VER_HYPERFACTIONS = "hyperfactions_admin.gui.ver_hyperfactions"; @@ -680,6 +743,76 @@ public static final class AdminGui { public static final String GUI_CZW_FLAGS_CUSTOMIZE_DESC = "hyperfactions_admin.gui.czw_flags_customize_desc"; public static final String GUI_CZW_FLAGS_CUSTOMIZE = "hyperfactions_admin.gui.czw_flags_customize"; + // Config editor page labels + public static final String CFG_TAB_SERVER = "hyperfactions_admin.config.tab_server"; + public static final String CFG_TAB_CHAT = "hyperfactions_admin.config.tab_chat"; + public static final String CFG_TAB_ANNOUNCEMENTS = "hyperfactions_admin.config.tab_announcements"; + public static final String CFG_TAB_ECONOMY = "hyperfactions_admin.config.tab_economy"; + public static final String CFG_TAB_FACTIONS = "hyperfactions_admin.config.tab_factions"; + public static final String CFG_TAB_FACTION_PERMS = "hyperfactions_admin.config.tab_faction_perms"; + public static final String CFG_TAB_WORLDMAP = "hyperfactions_admin.config.tab_worldmap"; + public static final String CFG_TAB_WORLDS = "hyperfactions_admin.config.tab_worlds"; + public static final String CFG_TAB_BACKUP = "hyperfactions_admin.config.tab_backup"; + public static final String CFG_TAB_DEBUG = "hyperfactions_admin.config.tab_debug"; + public static final String CFG_TAB_GRAVESTONES = "hyperfactions_admin.config.tab_gravestones"; + public static final String CFG_CHANGES_PENDING = "hyperfactions_admin.config.changes_pending"; + public static final String CFG_NO_CHANGES = "hyperfactions_admin.config.no_changes"; + public static final String CFG_BTN_SAVE = "hyperfactions_admin.config.btn_save"; + public static final String CFG_BTN_REVERT = "hyperfactions_admin.config.btn_revert"; + public static final String CFG_BTN_RESET = "hyperfactions_admin.config.btn_reset"; + public static final String CFG_SAVED = "hyperfactions_admin.config.saved"; + public static final String CFG_REVERTED = "hyperfactions_admin.config.reverted"; + public static final String CFG_RESET_CONFIRM = "hyperfactions_admin.config.reset_confirm"; + public static final String CFG_RESET_DONE = "hyperfactions_admin.config.reset_done"; + // Config section headers + public static final String CFG_SEC_TELEPORT = "hyperfactions_admin.config.sec_teleport"; + public static final String CFG_SEC_AUTOSAVE = "hyperfactions_admin.config.sec_autosave"; + public static final String CFG_SEC_MESSAGES = "hyperfactions_admin.config.sec_messages"; + public static final String CFG_SEC_GUI = "hyperfactions_admin.config.sec_gui"; + public static final String CFG_SEC_PERMISSIONS = "hyperfactions_admin.config.sec_permissions"; + public static final String CFG_SEC_LANGUAGE = "hyperfactions_admin.config.sec_language"; + public static final String CFG_SEC_MOB_CLEAR = "hyperfactions_admin.config.sec_mob_clear"; + public static final String CFG_SEC_UPDATES = "hyperfactions_admin.config.sec_updates"; + public static final String CFG_SEC_FACTION_LIMITS = "hyperfactions_admin.config.sec_faction_limits"; + public static final String CFG_SEC_POWER = "hyperfactions_admin.config.sec_power"; + public static final String CFG_SEC_POWER_LOSS = "hyperfactions_admin.config.sec_power_loss"; + public static final String CFG_SEC_REGEN = "hyperfactions_admin.config.sec_regen"; + public static final String CFG_SEC_CLAIMS = "hyperfactions_admin.config.sec_claims"; + public static final String CFG_SEC_DECAY = "hyperfactions_admin.config.sec_decay"; + public static final String CFG_SEC_PROTECTION = "hyperfactions_admin.config.sec_protection"; + public static final String CFG_SEC_COMBAT_TAG = "hyperfactions_admin.config.sec_combat_tag"; + public static final String CFG_SEC_FRIENDLY_FIRE = "hyperfactions_admin.config.sec_friendly_fire"; + public static final String CFG_SEC_SPAWN_PROT = "hyperfactions_admin.config.sec_spawn_prot"; + public static final String CFG_SEC_RELATIONS = "hyperfactions_admin.config.sec_relations"; + public static final String CFG_SEC_INVITES = "hyperfactions_admin.config.sec_invites"; + public static final String CFG_SEC_STUCK = "hyperfactions_admin.config.sec_stuck"; + public static final String CFG_SEC_FORMAT = "hyperfactions_admin.config.sec_format"; + public static final String CFG_SEC_COLORS = "hyperfactions_admin.config.sec_colors"; + public static final String CFG_SEC_REL_COLORS = "hyperfactions_admin.config.sec_rel_colors"; + public static final String CFG_SEC_FACTION_CHAT = "hyperfactions_admin.config.sec_faction_chat"; + public static final String CFG_SEC_HISTORY = "hyperfactions_admin.config.sec_history"; + public static final String CFG_SEC_BACKUP = "hyperfactions_admin.config.sec_backup"; + public static final String CFG_SEC_ECONOMY = "hyperfactions_admin.config.sec_economy"; + public static final String CFG_SEC_ANNOUNCE = "hyperfactions_admin.config.sec_announcements"; + public static final String CFG_SEC_MAP_DISPLAY = "hyperfactions_admin.config.sec_map_display"; + public static final String CFG_SEC_VISIBILITY = "hyperfactions_admin.config.sec_visibility"; + public static final String CFG_SEC_MIXIN = "hyperfactions_admin.config.sec_mixin"; + public static final String CFG_SEC_TERRITORY_NOTIFY = "hyperfactions_admin.config.sec_territory_notify"; + public static final String CFG_SEC_WILDERNESS = "hyperfactions_admin.config.sec_wilderness"; + public static final String CFG_SEC_CURRENCY = "hyperfactions_admin.config.sec_currency"; + public static final String CFG_SEC_TREASURY_LIMITS = "hyperfactions_admin.config.sec_treasury_limits"; + public static final String CFG_SEC_FEES = "hyperfactions_admin.config.sec_fees"; + public static final String CFG_SEC_UPKEEP = "hyperfactions_admin.config.sec_upkeep"; + public static final String CFG_SEC_PROXIMITY = "hyperfactions_admin.config.sec_proximity"; + public static final String CFG_SEC_INCREMENTAL = "hyperfactions_admin.config.sec_incremental"; + public static final String CFG_SEC_DEBOUNCED = "hyperfactions_admin.config.sec_debounced"; + public static final String CFG_SEC_DEBUG_GLOBAL = "hyperfactions_admin.config.sec_debug_global"; + public static final String CFG_SEC_DEBUG_CATEGORIES = "hyperfactions_admin.config.sec_debug_categories"; + public static final String CFG_SEC_SENTRY = "hyperfactions_admin.config.sec_sentry"; + public static final String CFG_SEC_GRAVESTONE_PROTECTION = "hyperfactions_admin.config.sec_gravestone_protection"; + public static final String CFG_SEC_GRAVESTONE_ACCESS = "hyperfactions_admin.config.sec_gravestone_access"; + public static final String CFG_SEC_GRAVESTONE_LOOT = "hyperfactions_admin.config.sec_gravestone_loot"; + // Faction entry labels public static final String GUI_FAC_ENTRY_POWER = "hyperfactions_admin.gui.fac_entry_power"; public static final String GUI_FAC_ENTRY_CLAIMS = "hyperfactions_admin.gui.fac_entry_claims"; diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_backup_entry.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_backup_entry.ui new file mode 100644 index 00000000..657b44ed --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_backup_entry.ui @@ -0,0 +1,95 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Top; + Background: (Color: #0d1520); + Padding: (Left: 8, Right: 8, Top: 6, Bottom: 6); + Anchor: (Bottom: 4); + + Group #HeaderRow { + LayoutMode: Left; + Anchor: (Height: 22); + + TextButton #ExpandBtn { + Style: $S.@ButtonStyle; + Text: ">"; + Anchor: (Width: 28, Height: 22); + } + + Group { Anchor: (Width: 6); } + + Label #BackupNameLabel { + Text: ""; + Style: (FontSize: 11, TextColor: #CCCCCC, RenderBold: true); + Anchor: (Width: 260, Height: 22); + } + + Label #BackupSizeLabel { + Text: ""; + Style: (FontSize: 10, TextColor: #888888, HorizontalAlignment: End); + Anchor: (Width: 80, Height: 22); + } + + Label #BackupTypeTag { + Text: ""; + Style: (FontSize: 10, TextColor: #00FFFF, HorizontalAlignment: End); + Anchor: (Width: 80, Height: 22); + } + } + + Group #DetailSection { + LayoutMode: Top; + Visible: false; + Padding: (Left: 34, Top: 4); + + Group #DetailRow1 { + LayoutMode: Left; + Anchor: (Height: 18); + + Label #DetailTypeLabel { + Text: ""; + Style: (FontSize: 10, TextColor: #888888); + Anchor: (Width: 200, Height: 18); + } + + Label #DetailCreatedLabel { + Text: ""; + Style: (FontSize: 10, TextColor: #888888); + Anchor: (Width: 250, Height: 18); + } + } + + Label #DetailSizeLabel { + Text: ""; + Style: (FontSize: 10, TextColor: #888888); + Anchor: (Height: 18); + } + + Label #RestoreWarning { + Text: ""; + Style: (FontSize: 10, TextColor: #FFAA00); + Anchor: (Height: 18); + Visible: false; + } + + Group #ActionRow { + LayoutMode: Left; + Anchor: (Height: 28, Top: 4); + + TextButton #RestoreBtn { + Style: $S.@ButtonStyle; + Text: "Restore"; + Anchor: (Width: 100, Height: 24); + } + + Group { Anchor: (Width: 8); } + + TextButton #DeleteBtn { + Style: $S.@ButtonStyle; + Text: "Delete"; + Anchor: (Width: 100, Height: 24); + } + } + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_backups.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_backups.ui index 5c17f6cf..97fae857 100644 --- a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_backups.ui +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_backups.ui @@ -18,40 +18,96 @@ $C.@PageOverlay { LayoutMode: Top; Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); - Group #PlaceholderContent { - FlexWeight: 1; - LayoutMode: Top; + Group #HeaderRow { + LayoutMode: Left; + Anchor: (Height: 36, Bottom: 6); + + Label #BackupCount { + Text: ""; + Style: (FontSize: 13, TextColor: #CCCCCC, RenderBold: true, VerticalAlignment: Center); + Anchor: (Width: 260, Height: 32); + } - Label { - Anchor: (Height: 100); + Label { FlexWeight: 1; } + + TextButton #CreateBackupBtn { + Style: $S.@CyanButtonStyle; + Text: "Create Backup"; + Anchor: (Width: 140, Height: 32); } + } + + Group #NameInputRow { + LayoutMode: Left; + Anchor: (Height: 28, Bottom: 6); + + Label #NameLabel { + Text: "Name:"; + Style: (FontSize: 11, TextColor: #888888, VerticalAlignment: Center); + Anchor: (Width: 50, Height: 28); + } + + $C.@TextField #BackupNameInput { + Anchor: (Width: 200, Height: 28); + } + + Group { FlexWeight: 1; } - Label #ComingSoon { - Text: "Backup Management"; - Style: (FontSize: 24, TextColor: #00FFFF, HorizontalAlignment: Center, VerticalAlignment: Center, RenderBold: true); - Anchor: (Height: 40); + Label #FilterLabel { + Text: "Filter:"; + Style: (FontSize: 11, TextColor: #888888, VerticalAlignment: Center); + Anchor: (Width: 40, Height: 28); } - Label #ComingSoonSub { - Text: "Coming Soon"; - Style: (FontSize: 16, TextColor: #888888, HorizontalAlignment: Center, VerticalAlignment: Center); - Anchor: (Height: 30); + DropdownBox #FilterDropdown { + Style: $C.@DefaultDropdownBoxStyle; + Anchor: (Width: 120, Height: 26); } + } + + Label #StatusMessage { + Text: ""; + Style: (FontSize: 10, TextColor: #888888); + Anchor: (Height: 18, Bottom: 4); + } - Label { - Anchor: (Height: 20); + Group #SeparatorLine { + Background: (Color: #333333); + Anchor: (Height: 1, Bottom: 6); + } + + Group #BackupListContainer { + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + FlexWeight: 1; + } + + Group #PaginationRow { + LayoutMode: Left; + Anchor: (Height: 35, Top: 6); + + TextButton #PrevBtn { + Style: $S.@ButtonStyle; + Text: "< Prev"; + Anchor: (Width: 80, Height: 26); + Visible: false; } - Label #Description { - Text: "Create, restore, and manage faction data backups."; - Style: (FontSize: 12, TextColor: #666666, HorizontalAlignment: Center, VerticalAlignment: Center); - Anchor: (Height: 25); + Label { FlexWeight: 1; } + + Label #PageLabel { + Text: ""; + Style: (FontSize: 14, TextColor: #888888, HorizontalAlignment: Center, VerticalAlignment: Center); + Anchor: (Width: 60); } - Label #Description2 { - Text: "Automatic backups are saved to the data/backups folder."; - Style: (FontSize: 12, TextColor: #666666, HorizontalAlignment: Center, VerticalAlignment: Center); - Anchor: (Height: 25); + Label { FlexWeight: 1; } + + TextButton #NextBtn { + Style: $S.@ButtonStyle; + Text: "Next >"; + Anchor: (Width: 80, Height: 26); + Visible: false; } } } diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config.ui deleted file mode 100644 index 0600fc63..00000000 --- a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config.ui +++ /dev/null @@ -1,61 +0,0 @@ -$C = "../../Common.ui"; -$S = "../shared/styles.ui"; -$Nav = "admin_nav_bar.ui"; - -$C.@PageOverlay { - $Nav.@HyperFactionsAdminNavBar #HyperFactionsAdminNavBar {} - - $C.@Container { - Anchor: (Width: 600, Height: 470); - - #Title { - $C.@Title #PageTitle { - @Text = "Configuration"; - } - } - - #Content { - LayoutMode: Top; - Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); - - Group #PlaceholderContent { - FlexWeight: 1; - LayoutMode: Top; - - Label { - Anchor: (Height: 100); - } - - Label #ComingSoon { - Text: "Configuration Editor"; - Style: (FontSize: 24, TextColor: #00FFFF, HorizontalAlignment: Center, VerticalAlignment: Center, RenderBold: true); - Anchor: (Height: 40); - } - - Label #ComingSoonSub { - Text: "Coming Soon"; - Style: (FontSize: 16, TextColor: #888888, HorizontalAlignment: Center, VerticalAlignment: Center); - Anchor: (Height: 30); - } - - Label { - Anchor: (Height: 20); - } - - Label #Description { - Text: "Configure HyperFactions settings directly from the GUI."; - Style: (FontSize: 12, TextColor: #666666, HorizontalAlignment: Center, VerticalAlignment: Center); - Anchor: (Height: 25); - } - - Label #Description2 { - Text: "For now, use /f reload to reload configuration changes."; - Style: (FontSize: 12, TextColor: #666666, HorizontalAlignment: Center, VerticalAlignment: Center); - Anchor: (Height: 25); - } - } - } - } -} - -$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_action_btn.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_action_btn.ui new file mode 100644 index 00000000..812caf5d --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_action_btn.ui @@ -0,0 +1,16 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 26, Bottom: 2, Top: 2); + Padding: (Left: 6, Right: 4); + + Group { FlexWeight: 1; } + + TextButton #ActionBtn { + Style: $S.@ButtonStyle; + Text: ""; + Anchor: (Width: 160, Height: 24); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_add_row.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_add_row.ui new file mode 100644 index 00000000..42fba7ff --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_add_row.ui @@ -0,0 +1,20 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 26, Top: 4); + Padding: (Left: 6, Right: 4); + + $C.@TextField #AddInput { + Anchor: (Width: 160, Height: 22); + } + + Group { Anchor: (Width: 6); } + + TextButton #AddBtn { + Style: $S.@ButtonStyle; + Text: "Add"; + Anchor: (Width: 80, Height: 22); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_blacklist_entry.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_blacklist_entry.ui new file mode 100644 index 00000000..43627a77 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_blacklist_entry.ui @@ -0,0 +1,21 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 24, Bottom: 1); + Background: (Color: #0d1520); + Padding: (Left: 8, Right: 8); + + Label #EntryLabel { + Text: ""; + Style: (FontSize: 12, TextColor: #CCCCCC, VerticalAlignment: Center); + FlexWeight: 1; + } + + TextButton #RemoveBtn { + Style: $S.@RedButtonStyle; + Text: "X"; + Anchor: (Width: 26, Height: 20); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_bool_row.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_bool_row.ui new file mode 100644 index 00000000..5738d3e2 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_bool_row.ui @@ -0,0 +1,19 @@ +$C = "../../Common.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 22, Bottom: 3); + Padding: (Left: 6, Right: 4); + + Label #SettingLabel { + Text: ""; + Style: (FontSize: 11, TextColor: #CCCCCC, VerticalAlignment: Center); + FlexWeight: 1; + } + + $C.@CheckBoxWithLabel #BoolToggle { + @Text = ""; + @Checked = false; + Anchor: (Height: 20, Width: 36); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_color_row.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_color_row.ui new file mode 100644 index 00000000..4e54ab0a --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_color_row.ui @@ -0,0 +1,33 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 24, Bottom: 3); + Padding: (Left: 6, Right: 4); + + Label #SettingLabel { + Text: ""; + Style: (FontSize: 11, TextColor: #CCCCCC, VerticalAlignment: Center); + FlexWeight: 1; + } + + ColorPickerDropdownBox #ColorPicker { + Style: $C.@DefaultColorPickerDropdownBoxStyle; + Anchor: (Width: 28, Height: 24); + } + + Group { Anchor: (Width: 4); } + + TextButton #ApplyColorBtn { + Style: $S.@ButtonStyle; + Text: "Set"; + Anchor: (Width: 32, Height: 22); + } + + Group { Anchor: (Width: 6); } + + $C.@TextField #ColorInput { + Anchor: (Width: 100, Height: 22); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_enum_row.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_enum_row.ui new file mode 100644 index 00000000..14077c05 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_enum_row.ui @@ -0,0 +1,19 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 24, Bottom: 3); + Padding: (Left: 6, Right: 4); + + Label #SettingLabel { + Text: ""; + Style: (FontSize: 11, TextColor: #CCCCCC, VerticalAlignment: Center); + FlexWeight: 1; + } + + DropdownBox #EnumSelect { + Style: $C.@DefaultDropdownBoxStyle; + Anchor: (Height: 24, Width: 140); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_facperm_child_row.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_facperm_child_row.ui new file mode 100644 index 00000000..cfade271 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_facperm_child_row.ui @@ -0,0 +1,27 @@ +$C = "../../Common.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 22, Bottom: 3); + Padding: (Left: 18, Right: 4); + + Label #SettingLabel { + Text: ""; + Style: (FontSize: 11, TextColor: #999999, VerticalAlignment: Center); + FlexWeight: 1; + } + + $C.@CheckBoxWithLabel #DefaultToggle { + @Text = ""; + @Checked = false; + Anchor: (Height: 20, Width: 36); + } + + Group { Anchor: (Width: 8); } + + $C.@CheckBoxWithLabel #LockToggle { + @Text = ""; + @Checked = false; + Anchor: (Height: 20, Width: 36); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_facperm_header.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_facperm_header.ui new file mode 100644 index 00000000..43ea6efb --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_facperm_header.ui @@ -0,0 +1,13 @@ +Group { + LayoutMode: Left; + Anchor: (Height: 14, Bottom: 1); + Padding: (Left: 6, Right: 4); + + Group { FlexWeight: 1; } + + Label { + Text: "Default / Lock"; + Style: (FontSize: 10, TextColor: #667788, RenderBold: true, HorizontalAlignment: Center, VerticalAlignment: Center); + Anchor: (Width: 80, Right: 10); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_facperm_row.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_facperm_row.ui new file mode 100644 index 00000000..546dc3f5 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_facperm_row.ui @@ -0,0 +1,27 @@ +$C = "../../Common.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 22, Bottom: 3); + Padding: (Left: 6, Right: 4); + + Label #SettingLabel { + Text: ""; + Style: (FontSize: 11, TextColor: #CCCCCC, VerticalAlignment: Center); + FlexWeight: 1; + } + + $C.@CheckBoxWithLabel #DefaultToggle { + @Text = ""; + @Checked = false; + Anchor: (Height: 20, Width: 36); + } + + Group { Anchor: (Width: 8); } + + $C.@CheckBoxWithLabel #LockToggle { + @Text = ""; + @Checked = false; + Anchor: (Height: 20, Width: 36); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_narrow.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_narrow.ui new file mode 100644 index 00000000..766f883a --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_narrow.ui @@ -0,0 +1,116 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "admin_nav_bar.ui"; + +@ConfigTab = MenuItem { + Padding: (Left: 10, Right: 10); + Style: ( + Default: ( + LabelStyle: (FontSize: 11, TextColor: #7c8b99, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ), + Hovered: ( + Background: #121a26, + LabelStyle: (FontSize: 11, TextColor: #00FFFF, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ) + ); + SelectedStyle: ( + Default: ( + LabelStyle: (FontSize: 11, TextColor: #00FFFF, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ), + Hovered: ( + Background: #121a26, + LabelStyle: (FontSize: 11, TextColor: #00FFFF, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ) + ); +}; + +$C.@PageOverlay { + $Nav.@HyperFactionsAdminNavBar #HyperFactionsAdminNavBar {} + + Group #TabBar { + LayoutMode: Left; + Background: #0d1520; + Anchor: (Height: 34, Top: 45); + Padding: (Left: 6); + + @ConfigTab #TabServer { Text: "Server"; } + @ConfigTab #TabChat { Text: "Chat"; } + @ConfigTab #TabAnnouncements { Text: "Announce"; } + @ConfigTab #TabEconomy { Text: "Economy"; } + @ConfigTab #TabFactions { Text: "Factions"; } + @ConfigTab #TabFactionPerms { Text: "Fac Perms"; } + @ConfigTab #TabWorldmap { Text: "Worldmap"; } + @ConfigTab #TabWorlds { Text: "Worlds"; } + @ConfigTab #TabBackup { Text: "Backup"; } + @ConfigTab #TabDebug { Text: "Debug"; } + @ConfigTab #TabGravestones { Text: "Graves"; } + } + + $C.@Container { + Anchor: (Width: 520, Height: 640); + + #Title { + Group { + LayoutMode: Left; + Padding: (Left: 15, Right: 15); + + $C.@Title #PageTitle { + @Text = "Config: Backup"; + } + + Group { FlexWeight: 1; } + + Label #StatusLabel { + Text: "No changes"; + Style: (FontSize: 11, TextColor: #888888, VerticalAlignment: Center); + Anchor: (Width: 160, Height: 30); + } + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 10, Right: 10, Top: 4, Bottom: 2); + + Group #LeftColumn { + FlexWeight: 1; + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + Padding: (Left: 2, Right: 2, Top: 2, Bottom: 2); + + Group #LeftContainer { + LayoutMode: Top; + } + } + + Group #ActionBar { + LayoutMode: Left; + Anchor: (Height: 40); + + TextButton #ResetBtn { + Style: $S.@RedButtonStyle; + Text: "Reset Defaults"; + Anchor: (Width: 150, Height: 32); + } + + Label { FlexWeight: 1; } + + TextButton #RevertBtn { + Style: $S.@ButtonStyle; + Text: "Revert"; + Anchor: (Width: 100, Height: 32); + } + + Label { FlexWeight: 1; } + + TextButton #SaveBtn { + Style: $S.@CyanButtonStyle; + Text: "Save"; + Anchor: (Width: 100, Height: 32); + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_num_row.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_num_row.ui new file mode 100644 index 00000000..b9fd5400 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_num_row.ui @@ -0,0 +1,34 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 22, Bottom: 3); + Padding: (Left: 6, Right: 4); + + Label #SettingLabel { + Text: ""; + Style: (FontSize: 11, TextColor: #CCCCCC, VerticalAlignment: Center); + FlexWeight: 1; + } + + TextButton #DecBtn { + Style: $S.@ButtonStyle; + Text: "-"; + Anchor: (Width: 22, Height: 20); + } + + Group { Anchor: (Width: 2); } + + $C.@TextField #NumInput { + Anchor: (Width: 64, Height: 20); + } + + Group { Anchor: (Width: 2); } + + TextButton #IncBtn { + Style: $S.@ButtonStyle; + Text: "+"; + Anchor: (Width: 22, Height: 20); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_scaling_entry.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_scaling_entry.ui new file mode 100644 index 00000000..0a3f41fd --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_scaling_entry.ui @@ -0,0 +1,43 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 26, Bottom: 2); + Background: (Color: #0d1520); + Padding: (Left: 8, Right: 8); + + $C.@TextField #ChunkInput { + Anchor: (Width: 90, Height: 22); + } + + Group { Anchor: (Width: 8); } + + $C.@TextField #CostInput { + Anchor: (Width: 90, Height: 22); + } + + Group { Anchor: (Width: 8); } + + TextButton #UpBtn { + Style: $S.@ButtonStyle; + Text: "^"; + Anchor: (Width: 22, Height: 22); + } + + Group { Anchor: (Width: 2); } + + TextButton #DownBtn { + Style: $S.@ButtonStyle; + Text: "v"; + Anchor: (Width: 22, Height: 22); + } + + Group { Anchor: (Width: 6); } + + TextButton #RemoveBtn { + Style: $S.@RedButtonStyle; + Text: "X"; + Anchor: (Width: 22, Height: 22); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_scaling_modal.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_scaling_modal.ui new file mode 100644 index 00000000..83300b40 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_scaling_modal.ui @@ -0,0 +1,86 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +$C.@PageOverlay { + $C.@Container { + Anchor: (Width: 500, Height: 420); + + #Content { + LayoutMode: Top; + Padding: (Left: 10, Right: 10, Top: 8, Bottom: 5); + + Label #ModalTitle { + Text: "Edit Scaling Tiers"; + Style: (FontSize: 14, TextColor: #FFFFFF, RenderBold: true); + Anchor: (Height: 22, Bottom: 2); + } + + Label #ExampleLabel { + Text: ""; + Style: (FontSize: 12, TextColor: #55AAFF); + Anchor: (Height: 20, Bottom: 4); + } + + Group #HeaderRow { + LayoutMode: Left; + Anchor: (Height: 20, Bottom: 2); + Padding: (Left: 8, Right: 8); + + Label { + Text: "Chunk Count"; + Style: (FontSize: 10, TextColor: #888888, RenderBold: true); + Anchor: (Width: 120); + } + + Label { + Text: "Cost Per Chunk"; + Style: (FontSize: 10, TextColor: #888888, RenderBold: true); + Anchor: (Width: 120); + } + } + + Group #TierList { + FlexWeight: 1; + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + Padding: (Left: 2, Right: 2, Top: 2, Bottom: 2); + + Group #TierContainer { + LayoutMode: Top; + } + } + + Group #AddBar { + LayoutMode: Left; + Anchor: (Height: 30, Top: 4); + Padding: (Left: 4, Right: 4); + + TextButton #AddTierBtn { + Style: $S.@ButtonStyle; + Text: "Add Tier"; + Anchor: (Width: 100, Height: 26); + } + } + + Group #ActionBar { + LayoutMode: Left; + Anchor: (Height: 32, Top: 4); + Padding: (Left: 4, Right: 4); + + TextButton #SaveBtn { + Style: $S.@CyanButtonStyle; + Text: "Save"; + Anchor: (Width: 100, Height: 26); + } + + Group { Anchor: (Width: 8); } + + TextButton #CancelBtn { + Style: $S.@ButtonStyle; + Text: "Cancel"; + Anchor: (Width: 100, Height: 26); + } + } + } + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_section.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_section.ui new file mode 100644 index 00000000..02ef8e92 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_section.ui @@ -0,0 +1,15 @@ +Group { + LayoutMode: Top; + Anchor: (Top: 4, Bottom: 2); + + Label #SectionTitle { + Text: ""; + Style: (FontSize: 11, TextColor: #888888, RenderBold: true); + Anchor: (Height: 18, Bottom: 4); + } + + Group { + Anchor: (Height: 1); + Background: (Color: #334455); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_standard.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_standard.ui new file mode 100644 index 00000000..ad39bbc2 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_standard.ui @@ -0,0 +1,133 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "admin_nav_bar.ui"; + +@ConfigTab = MenuItem { + Padding: (Left: 10, Right: 10); + Style: ( + Default: ( + LabelStyle: (FontSize: 11, TextColor: #7c8b99, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ), + Hovered: ( + Background: #121a26, + LabelStyle: (FontSize: 11, TextColor: #00FFFF, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ) + ); + SelectedStyle: ( + Default: ( + LabelStyle: (FontSize: 11, TextColor: #00FFFF, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ), + Hovered: ( + Background: #121a26, + LabelStyle: (FontSize: 11, TextColor: #00FFFF, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ) + ); +}; + +$C.@PageOverlay { + $Nav.@HyperFactionsAdminNavBar #HyperFactionsAdminNavBar {} + + Group #TabBar { + LayoutMode: Left; + Background: #0d1520; + Anchor: (Height: 34, Top: 45); + Padding: (Left: 6); + + @ConfigTab #TabServer { Text: "Server"; } + @ConfigTab #TabChat { Text: "Chat"; } + @ConfigTab #TabAnnouncements { Text: "Announce"; } + @ConfigTab #TabEconomy { Text: "Economy"; } + @ConfigTab #TabFactions { Text: "Factions"; } + @ConfigTab #TabFactionPerms { Text: "Fac Perms"; } + @ConfigTab #TabWorldmap { Text: "Worldmap"; } + @ConfigTab #TabWorlds { Text: "Worlds"; } + @ConfigTab #TabBackup { Text: "Backup"; } + @ConfigTab #TabDebug { Text: "Debug"; } + @ConfigTab #TabGravestones { Text: "Graves"; } + } + + $C.@Container { + Anchor: (Width: 780, Height: 640); + + #Title { + Group { + LayoutMode: Left; + Padding: (Left: 15, Right: 15); + + $C.@Title #PageTitle { + @Text = "Config: Server"; + } + + Group { FlexWeight: 1; } + + Label #StatusLabel { + Text: "No changes"; + Style: (FontSize: 11, TextColor: #888888, VerticalAlignment: Center); + Anchor: (Width: 160, Height: 30); + } + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 10, Right: 10, Top: 4, Bottom: 2); + + Group { + FlexWeight: 1; + LayoutMode: Left; + Padding: (Top: 2, Bottom: 2); + + Group #LeftColumn { + FlexWeight: 1; + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + Padding: (Left: 2, Right: 6, Top: 2, Bottom: 2); + + Group #LeftContainer { + LayoutMode: Top; + } + } + + Group #RightColumn { + FlexWeight: 1; + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@DefaultScrollbarStyle; + Padding: (Left: 6, Right: 2, Top: 2, Bottom: 2); + + Group #RightContainer { + LayoutMode: Top; + } + } + } + + Group #ActionBar { + LayoutMode: Left; + Anchor: (Height: 40); + + TextButton #ResetBtn { + Style: $S.@RedButtonStyle; + Text: "Reset Defaults"; + Anchor: (Width: 150, Height: 32); + } + + Label { FlexWeight: 1; } + + TextButton #RevertBtn { + Style: $S.@ButtonStyle; + Text: "Revert"; + Anchor: (Width: 100, Height: 32); + } + + Label { FlexWeight: 1; } + + TextButton #SaveBtn { + Style: $S.@CyanButtonStyle; + Text: "Save"; + Anchor: (Width: 100, Height: 32); + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_str_row.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_str_row.ui new file mode 100644 index 00000000..00b9763d --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_str_row.ui @@ -0,0 +1,18 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 22, Bottom: 3); + Padding: (Left: 6, Right: 4); + + Label #SettingLabel { + Text: ""; + Style: (FontSize: 11, TextColor: #CCCCCC, VerticalAlignment: Center); + FlexWeight: 1; + } + + $C.@TextField #StrInput { + Anchor: (Width: 140, Height: 20); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_str_wide_row.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_str_wide_row.ui new file mode 100644 index 00000000..7d2d41fd --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_str_wide_row.ui @@ -0,0 +1,18 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 22, Bottom: 3); + Padding: (Left: 6, Right: 4); + + Label #SettingLabel { + Text: ""; + Style: (FontSize: 11, TextColor: #CCCCCC, VerticalAlignment: Center); + FlexWeight: 1; + } + + $C.@TextField #StrInput { + Anchor: (Width: 200, Height: 20); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_tristate_row.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_tristate_row.ui new file mode 100644 index 00000000..a1e7f5fb --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_tristate_row.ui @@ -0,0 +1,18 @@ +$C = "../../Common.ui"; + +Group { + LayoutMode: Left; + Anchor: (Height: 24, Bottom: 1); + Padding: (Left: 18, Right: 4); + + Label #SettingLabel { + Text: ""; + Style: (FontSize: 11, TextColor: #AAAAAA, VerticalAlignment: Center); + FlexWeight: 1; + } + + DropdownBox #TristateSelect { + Style: $C.@DefaultDropdownBoxStyle; + Anchor: (Height: 22, Width: 110); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_wide.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_wide.ui new file mode 100644 index 00000000..6a76d1e7 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_wide.ui @@ -0,0 +1,155 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; +$Nav = "admin_nav_bar.ui"; + +@ConfigTab = MenuItem { + Padding: (Left: 10, Right: 10); + Style: ( + Default: ( + LabelStyle: (FontSize: 11, TextColor: #7c8b99, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ), + Hovered: ( + Background: #121a26, + LabelStyle: (FontSize: 11, TextColor: #00FFFF, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ) + ); + SelectedStyle: ( + Default: ( + LabelStyle: (FontSize: 11, TextColor: #00FFFF, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ), + Hovered: ( + Background: #121a26, + LabelStyle: (FontSize: 11, TextColor: #00FFFF, VerticalAlignment: Center, RenderUppercase: true, RenderBold: true) + ) + ); +}; + +$C.@PageOverlay { + $Nav.@HyperFactionsAdminNavBar #HyperFactionsAdminNavBar {} + + Group #TabBar { + LayoutMode: Left; + Background: #0d1520; + Anchor: (Height: 34, Top: 45); + Padding: (Left: 6); + + @ConfigTab #TabServer { Text: "Server"; } + @ConfigTab #TabChat { Text: "Chat"; } + @ConfigTab #TabAnnouncements { Text: "Announce"; } + @ConfigTab #TabEconomy { Text: "Economy"; } + @ConfigTab #TabFactions { Text: "Factions"; } + @ConfigTab #TabFactionPerms { Text: "Fac Perms"; } + @ConfigTab #TabWorldmap { Text: "Worldmap"; } + @ConfigTab #TabWorlds { Text: "Worlds"; } + @ConfigTab #TabBackup { Text: "Backup"; } + @ConfigTab #TabDebug { Text: "Debug"; } + @ConfigTab #TabGravestones { Text: "Graves"; } + } + + $C.@Container { + Anchor: (Width: 1020, Height: 640); + + #Title { + Group { + LayoutMode: Left; + Padding: (Left: 15, Right: 15); + + $C.@Title #PageTitle { + @Text = "Config: Factions"; + } + + Group { FlexWeight: 1; } + + Label #StatusLabel { + Text: "No changes"; + Style: (FontSize: 11, TextColor: #888888, VerticalAlignment: Center); + Anchor: (Width: 160, Height: 30); + } + } + } + + #Content { + LayoutMode: Top; + Padding: (Left: 10, Right: 10, Top: 4, Bottom: 2); + + Group { + FlexWeight: 1; + LayoutMode: Left; + Padding: (Top: 2, Bottom: 2); + + Group #Col1 { + FlexWeight: 1; + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@TranslucentScrollbarStyle; + Padding: (Left: 2, Right: 3, Top: 2, Bottom: 2); + + Group #Col1Container { + LayoutMode: Top; + } + } + + Group #Col2 { + FlexWeight: 1; + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@TranslucentScrollbarStyle; + Padding: (Left: 3, Right: 3, Top: 2, Bottom: 2); + + Group #Col2Container { + LayoutMode: Top; + } + } + + Group #Col3 { + FlexWeight: 1; + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@TranslucentScrollbarStyle; + Padding: (Left: 3, Right: 3, Top: 2, Bottom: 2); + + Group #Col3Container { + LayoutMode: Top; + } + } + + Group #Col4 { + FlexWeight: 1; + LayoutMode: TopScrolling; + ScrollbarStyle: $C.@TranslucentScrollbarStyle; + Padding: (Left: 3, Right: 2, Top: 2, Bottom: 2); + + Group #Col4Container { + LayoutMode: Top; + } + } + } + + Group #ActionBar { + LayoutMode: Left; + Anchor: (Height: 40); + + TextButton #ResetBtn { + Style: $S.@RedButtonStyle; + Text: "Reset Defaults"; + Anchor: (Width: 150, Height: 32); + } + + Label { FlexWeight: 1; } + + TextButton #RevertBtn { + Style: $S.@ButtonStyle; + Text: "Revert"; + Anchor: (Width: 100, Height: 32); + } + + Label { FlexWeight: 1; } + + TextButton #SaveBtn { + Style: $S.@CyanButtonStyle; + Text: "Save"; + Anchor: (Width: 100, Height: 32); + } + } + } + } +} + +$C.@BackButton {} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_world_entry.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_world_entry.ui new file mode 100644 index 00000000..442e1be1 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_config_world_entry.ui @@ -0,0 +1,39 @@ +$C = "../../Common.ui"; +$S = "../shared/styles.ui"; + +Group { + LayoutMode: Top; + Anchor: (Bottom: 2); + Background: (Color: #0d1520); + Padding: (Left: 8, Right: 8, Top: 4, Bottom: 4); + + Group #WorldHeader { + LayoutMode: Left; + Anchor: (Height: 24); + + Label #WorldName { + Text: ""; + Style: (FontSize: 12, TextColor: #00FFFF, RenderBold: true, VerticalAlignment: Center); + FlexWeight: 1; + } + + TextButton #ExpandBtn { + Style: $S.@ButtonStyle; + Text: "Edit"; + Anchor: (Width: 50, Height: 20); + } + + Group { Anchor: (Width: 4); } + + TextButton #RemoveWorldBtn { + Style: $S.@RedButtonStyle; + Text: "X"; + Anchor: (Width: 26, Height: 20); + } + } + + Group #WorldSettings { + LayoutMode: Top; + Anchor: (Top: 2); + } +} diff --git a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_updates.ui b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_updates.ui index 70c75c90..f2c3e279 100644 --- a/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_updates.ui +++ b/src/main/resources/Common/UI/Custom/HyperFactions/admin/admin_updates.ui @@ -6,7 +6,7 @@ $C.@PageOverlay { $Nav.@HyperFactionsAdminNavBar #HyperFactionsAdminNavBar {} $C.@Container { - Anchor: (Width: 600, Height: 470); + Anchor: (Width: 700, Height: 470); #Title { $C.@Title #PageTitle { @@ -18,40 +18,165 @@ $C.@PageOverlay { LayoutMode: Top; Padding: (Left: 15, Right: 15, Top: 10, Bottom: 10); - Group #PlaceholderContent { + Group #ColumnsWrapper { FlexWeight: 1; - LayoutMode: Top; + LayoutMode: Left; - Label { - Anchor: (Height: 100); + Group #HFColumn { + FlexWeight: 1; + LayoutMode: Top; + Padding: (Right: 8); + + Label #HFTitle { + Text: "HyperFactions"; + Style: (FontSize: 13, TextColor: #00FFFF, RenderBold: true); + Anchor: (Height: 20, Bottom: 4); + } + + Group { Background: (Color: #334455); Anchor: (Height: 1, Bottom: 6); } + + Group { + LayoutMode: Left; + Anchor: (Height: 16, Bottom: 3); + Label { Text: "Current:"; Style: (FontSize: 11, TextColor: #888888); Anchor: (Width: 60, Height: 16); } + Label #HFCurrentVersion { Text: "-"; Style: (FontSize: 11, TextColor: #CCCCCC); FlexWeight: 1; } + } + + Group { + LayoutMode: Left; + Anchor: (Height: 16, Bottom: 3); + Label { Text: "Latest:"; Style: (FontSize: 11, TextColor: #888888); Anchor: (Width: 60, Height: 16); } + Label #HFLatestVersion { Text: "-"; Style: (FontSize: 11, TextColor: #CCCCCC); FlexWeight: 1; } + } + + Group { + LayoutMode: Left; + Anchor: (Height: 16, Bottom: 3); + Label { Text: "Channel:"; Style: (FontSize: 11, TextColor: #888888); Anchor: (Width: 60, Height: 16); } + Label #HFChannel { Text: "-"; Style: (FontSize: 11, TextColor: #CCCCCC); FlexWeight: 1; } + } + + Group { + LayoutMode: Left; + Anchor: (Height: 16, Bottom: 3); + Label { Text: "Built:"; Style: (FontSize: 11, TextColor: #888888); Anchor: (Width: 60, Height: 16); } + Label #HFBuildDate { Text: "-"; Style: (FontSize: 11, TextColor: #CCCCCC); FlexWeight: 1; } + } + + Group { Anchor: (Height: 4); } + + Label #HFStatus { + Text: ""; + Style: (FontSize: 11, TextColor: #888888); + Anchor: (Height: 16, Bottom: 4); + } + + TextButton #DownloadBtn { + Style: $S.@CyanButtonStyle; + Text: "Download"; + Anchor: (Width: 140, Height: 26); + Visible: false; + } + + Group #ChangelogSection { + LayoutMode: Top; + Visible: false; + Anchor: (Top: 6); + + Label { + Text: "Changelog"; + Style: (FontSize: 11, TextColor: #00FFFF, RenderBold: true); + Anchor: (Height: 16, Bottom: 3); + } + + Group { + Background: (Color: #0d1520); + Padding: (Full: 6); + LayoutMode: Top; + + Label #ChangelogText { + Text: ""; + Style: (FontSize: 9, TextColor: #AAAAAA); + Anchor: (Height: 70); + } + } + } } - Label #ComingSoon { - Text: "Update Center"; - Style: (FontSize: 24, TextColor: #00FFFF, HorizontalAlignment: Center, VerticalAlignment: Center, RenderBold: true); - Anchor: (Height: 40); + Group { + Background: (Color: #334455); + Anchor: (Width: 1); } - Label #ComingSoonSub { - Text: "Coming Soon"; - Style: (FontSize: 16, TextColor: #888888, HorizontalAlignment: Center, VerticalAlignment: Center); - Anchor: (Height: 30); + Group #HPColumn { + FlexWeight: 1; + LayoutMode: Top; + Padding: (Left: 8); + + Label #HPTitle { + Text: "HyperProtect Mixin"; + Style: (FontSize: 13, TextColor: #00FFFF, RenderBold: true); + Anchor: (Height: 20, Bottom: 4); + } + + Group { Background: (Color: #334455); Anchor: (Height: 1, Bottom: 6); } + + Group { + LayoutMode: Left; + Anchor: (Height: 16, Bottom: 3); + Label { Text: "Current:"; Style: (FontSize: 11, TextColor: #888888); Anchor: (Width: 60, Height: 16); } + Label #HPCurrentVersion { Text: "-"; Style: (FontSize: 11, TextColor: #CCCCCC); FlexWeight: 1; } + } + + Group { + LayoutMode: Left; + Anchor: (Height: 16, Bottom: 3); + Label { Text: "Latest:"; Style: (FontSize: 11, TextColor: #888888); Anchor: (Width: 60, Height: 16); } + Label #HPLatestVersion { Text: "-"; Style: (FontSize: 11, TextColor: #CCCCCC); FlexWeight: 1; } + } + + Group { + LayoutMode: Left; + Anchor: (Height: 16, Bottom: 3); + Label { Text: "Status:"; Style: (FontSize: 11, TextColor: #888888); Anchor: (Width: 60, Height: 16); } + Label #HPStatus { Text: "-"; Style: (FontSize: 11, TextColor: #CCCCCC); FlexWeight: 1; } + } + + Group { Anchor: (Height: 4); } + + TextButton #DownloadMixinBtn { + Style: $S.@CyanButtonStyle; + Text: "Download Mixin"; + Anchor: (Width: 140, Height: 26); + Visible: false; + } } + } - Label { - Anchor: (Height: 20); + Group #ActionBar { + LayoutMode: Left; + Anchor: (Height: 40); + + TextButton #CheckUpdateBtn { + Style: $S.@ButtonStyle; + Text: "Check for Updates"; + Anchor: (Width: 160, Height: 32); } - Label #Description { - Text: "Check for updates and view changelog."; - Style: (FontSize: 12, TextColor: #666666, HorizontalAlignment: Center, VerticalAlignment: Center); - Anchor: (Height: 25); + Label { FlexWeight: 1; } + + Label #RestartNote { + Text: ""; + Style: (FontSize: 10, TextColor: #FFAA00, VerticalAlignment: Center); + Anchor: (Width: 200, Height: 32); + Visible: false; } - Label #Description2 { - Text: "Visit hypersystems.dev for the latest releases."; - Style: (FontSize: 12, TextColor: #666666, HorizontalAlignment: Center, VerticalAlignment: Center); - Anchor: (Height: 25); + TextButton #RollbackBtn { + Style: $S.@RedButtonStyle; + Text: "Rollback"; + Anchor: (Width: 120, Height: 32); + Visible: false; } } } diff --git a/src/main/resources/Server/Languages/de-DE/hyperfactions_admin.lang b/src/main/resources/Server/Languages/de-DE/hyperfactions_admin.lang index 23a7d943..4444f1c9 100644 --- a/src/main/resources/Server/Languages/de-DE/hyperfactions_admin.lang +++ b/src/main/resources/Server/Languages/de-DE/hyperfactions_admin.lang @@ -799,3 +799,136 @@ gui.zone_entry_edit_map = Karte bearbeiten gui.zone_entry_flags = Flags gui.zone_entry_settings = Einstellungen gui.zone_entry_delete = Löschen + +# ========== Updates Page ========== +updates.version_info = Versionsinformationen +updates.current_version = Aktuell: +updates.build_date = Erstellt: +updates.latest_version = Neueste: +updates.channel = Kanal: +updates.update_status = Update-Status +updates.status_checking = Suche nach Updates... +updates.status_up_to_date = Aktuell +updates.status_available = Neue Version verfuegbar: v{0} +updates.status_downloading = Update wird heruntergeladen... +updates.status_downloaded = Update erfolgreich heruntergeladen! +updates.status_failed = Fehler bei der Update-Pruefung. +updates.btn_check = Nach Updates suchen +updates.btn_download = Herunterladen & Installieren +updates.btn_rollback = Zuruecksetzen auf +updates.changelog_title = Aenderungsprotokoll +updates.no_changelog = Kein Aenderungsprotokoll verfuegbar. +updates.rollback_section = Zuruecksetzen +updates.rollback_confirm = Zuruecksetzen bestaetigen +updates.rollback_unsafe = Zuruecksetzen nicht sicher (Server wurde seit dem Update neu gestartet). +updates.rollback_success = Auf v{0} zurueckgesetzt. Neustart erforderlich. +updates.rollback_failed = Zuruecksetzen fehlgeschlagen: {0} +updates.mixin_title = HyperProtect Mixin +updates.mixin_version = Version: +updates.mixin_check = Mixin-Update pruefen +updates.mixin_up_to_date = Mixin ist aktuell. +updates.mixin_available = Mixin-Update verfuegbar: v{0} +updates.mixin_downloading = Mixin-Update wird heruntergeladen... +updates.mixin_downloaded = Mixin-Update heruntergeladen! +updates.mixin_failed = Mixin-Update fehlgeschlagen. +updates.mixin_not_installed = Nicht installiert +updates.restart_required = Serverneustart erforderlich. +updates.pre_release = (Vorabversion) +# ========== Backups Page ========== +backups.title = Backup Management +backups.total_count = Backups ({0} total) +backups.empty = No backups found. +backups.btn_create = Create Backup +backups.btn_restore = Restore +backups.btn_delete = Delete +backups.name_placeholder = Name: +backups.creating = Creating backup... +backups.created = Backup created: {0} +backups.create_failed = Failed to create backup +backups.type_hourly = Hourly +backups.type_daily = Daily +backups.type_weekly = Weekly +backups.type_manual = Manual +backups.type_migration = Migration +backups.detail_type = Type: +backups.detail_created = Created: +backups.detail_size = Size: +backups.restore_warning = Warning: This will overwrite current data! +backups.restore_confirm = Confirm Restore +backups.restoring = Restoring backup... +backups.restored = Restored {0} ({1} files). +backups.restore_failed = Restore failed +backups.delete_confirm = Confirm Delete +backups.deleting = Deleting backup... +backups.deleted = Deleted backup: {0} +backups.delete_failed = Failed to delete backup. +backups.reload_required = Run /f reload to apply restored data. + +# ========== Config Editor ========== +config.tab_server = Server +config.tab_chat = Chat +config.tab_announcements = Announce +config.tab_economy = Economy +config.tab_factions = Factions +config.tab_faction_perms = Fac Perms +config.tab_worldmap = Worldmap +config.tab_worlds = Worlds +config.tab_backup = Backup +config.tab_debug = Debug +config.tab_gravestones = Graves +config.changes_pending = changes pending +config.no_changes = No changes +config.btn_save = Save +config.btn_revert = Revert +config.btn_reset = Reset Defaults +config.saved = Configuration saved. +config.reverted = Changes reverted. +config.reset_confirm = Confirm Reset +config.reset_done = Configuration reset to defaults. +config.sec_teleport = Teleport +config.sec_autosave = Auto-Save +config.sec_messages = Messages +config.sec_gui = GUI +config.sec_permissions = Permissions +config.sec_language = Language +config.sec_mob_clear = Mob Clearing +config.sec_updates = Updates +config.sec_faction_limits = Faction Limits +config.sec_power = Power +config.sec_power_loss = Power Loss +config.sec_regen = Regeneration +config.sec_claims = Claims +config.sec_decay = Decay +config.sec_protection = Claim Protection +config.sec_combat_tag = Combat Tag +config.sec_friendly_fire = Friendly Fire +config.sec_spawn_prot = Spawn Protection +config.sec_relations = Relations +config.sec_invites = Invites +config.sec_stuck = Stuck Command +config.sec_format = Chat Format +config.sec_colors = Colors +config.sec_rel_colors = Relation Colors +config.sec_faction_chat = Faction Chat +config.sec_history = Chat History +config.sec_backup = Backup +config.sec_economy = Economy +config.sec_announcements = Announcements +config.sec_map_display = Map Display +config.sec_visibility = Player Visibility +config.sec_mixin = Mixin +config.sec_territory_notify = Territory Notifications +config.sec_wilderness = Wilderness Messages +config.sec_currency = Currency +config.sec_treasury_limits = Treasury Limits +config.sec_fees = Fees +config.sec_upkeep = Upkeep +config.sec_proximity = Proximity Mode +config.sec_incremental = Incremental Mode +config.sec_debounced = Debounced Mode +config.sec_debug_global = Global +config.sec_debug_categories = Categories +config.sec_sentry = Sentry +config.sec_gravestone_protection = Protection +config.sec_gravestone_access = Access +config.sec_gravestone_loot = Loot Rules diff --git a/src/main/resources/Server/Languages/en-US/help/admin/admin_config/configuration.md b/src/main/resources/Server/Languages/en-US/help/admin/admin_config/configuration.md index 95b6c952..3d60cef9 100644 --- a/src/main/resources/Server/Languages/en-US/help/admin/admin_config/configuration.md +++ b/src/main/resources/Server/Languages/en-US/help/admin/admin_config/configuration.md @@ -10,9 +10,12 @@ HyperFactions uses a modular JSON config system with 11 configuration files. | Command | Description | |---------|-------------| | `/f admin config` | Open the visual config editor GUI | +| `/f admin config ` | Open config editor to a specific tab | | `/f admin reload` | Reload all config files from disk | | `/f admin sync` | Synchronize faction data to storage | +**Tab names:** `server` (srv), `chat`, `announcements` (announce, ann), `economy` (eco), `factions` (fac), `factionperms` (facperms, perms), `worldmap` (map), `worlds` (world), `backup`, `debug` (dbg), `gravestones` (graves) + ## Configuration Files | File | Contents | @@ -29,7 +32,7 @@ HyperFactions uses a modular JSON config system with 11 configuration files. | `worldmap.json` | World map refresh modes | | `worlds.json` | Per-world behavior overrides | ->[!TIP] The config GUI provides a visual editor with descriptions for every setting. Changes are saved immediately but some require `/f admin reload` to take full effect. +>[!TIP] The config GUI provides a visual editor for every setting. Changes take effect immediately on save — periodic tasks (auto-save, mob clear, upkeep) and the worldmap scheduler are automatically restarted. ## Config Location diff --git a/src/main/resources/Server/Languages/en-US/hyperfactions_admin.lang b/src/main/resources/Server/Languages/en-US/hyperfactions_admin.lang index bb35ea86..4e068f83 100644 --- a/src/main/resources/Server/Languages/en-US/hyperfactions_admin.lang +++ b/src/main/resources/Server/Languages/en-US/hyperfactions_admin.lang @@ -799,3 +799,137 @@ gui.zone_entry_edit_map = Edit Map gui.zone_entry_flags = Flags gui.zone_entry_settings = Settings gui.zone_entry_delete = Delete + +# ========== Updates Page ========== +updates.version_info = Version Information +updates.current_version = Current: +updates.build_date = Built: +updates.latest_version = Latest: +updates.channel = Channel: +updates.update_status = Update Status +updates.status_checking = Checking for updates... +updates.status_up_to_date = Up to date +updates.status_available = New version available: v{0} +updates.status_downloading = Downloading update... +updates.status_downloaded = Update downloaded successfully! +updates.status_failed = Failed to check for updates. +updates.btn_check = Check for Updates +updates.btn_download = Download & Install +updates.btn_rollback = Rollback to +updates.changelog_title = Changelog +updates.no_changelog = No changelog available. +updates.rollback_section = Rollback +updates.rollback_confirm = Confirm Rollback +updates.rollback_unsafe = Rollback is not safe (server has restarted since update). +updates.rollback_success = Rolled back to v{0}. Restart to apply. +updates.rollback_failed = Rollback failed: {0} +updates.mixin_title = HyperProtect Mixin +updates.mixin_version = Version: +updates.mixin_check = Check Mixin Update +updates.mixin_up_to_date = Mixin is up to date. +updates.mixin_available = Mixin update available: v{0} +updates.mixin_downloading = Downloading mixin update... +updates.mixin_downloaded = Mixin update downloaded! +updates.mixin_failed = Failed to download mixin update. +updates.mixin_not_installed = Not installed +updates.restart_required = Server restart required to apply changes. +updates.pre_release = (pre-release) +# ========== Backups Page ========== +backups.title = Backup Management +backups.total_count = Backups ({0} total) +backups.empty = No backups found. +backups.btn_create = Create Backup +backups.btn_restore = Restore +backups.btn_delete = Delete +backups.name_placeholder = Name: +backups.creating = Creating backup... +backups.created = Backup created: {0} +backups.create_failed = Failed to create backup +backups.type_hourly = Hourly +backups.type_daily = Daily +backups.type_weekly = Weekly +backups.type_manual = Manual +backups.type_migration = Migration +backups.detail_type = Type: +backups.detail_created = Created: +backups.detail_size = Size: +backups.restore_warning = Warning: This will overwrite current data! +backups.restore_confirm = Confirm Restore +backups.restoring = Restoring backup... +backups.restored = Restored {0} ({1} files). +backups.restore_failed = Restore failed +backups.delete_confirm = Confirm Delete +backups.deleting = Deleting backup... +backups.deleted = Deleted backup: {0} +backups.delete_failed = Failed to delete backup. +backups.reload_required = Run /f reload to apply restored data. +# ========== Config Editor ========== +config.tab_server = Server +config.tab_chat = Chat +config.tab_announcements = Announce +config.tab_economy = Economy +config.tab_factions = Factions +config.tab_faction_perms = Fac Perms +config.tab_worldmap = Worldmap +config.tab_worlds = Worlds +config.tab_backup = Backup +config.tab_debug = Debug +config.tab_gravestones = Graves +config.changes_pending = changes pending +config.no_changes = No changes +config.btn_save = Save +config.btn_revert = Revert +config.btn_reset = Reset Defaults +config.saved = Configuration saved. +config.reverted = Changes reverted. +config.reset_confirm = Confirm Reset +config.reset_done = Configuration reset to defaults. + +# Config section headers +config.sec_teleport = Teleport +config.sec_autosave = Auto-Save +config.sec_messages = Messages +config.sec_gui = GUI +config.sec_permissions = Permissions +config.sec_language = Language +config.sec_mob_clear = Mob Clearing +config.sec_updates = Updates +config.sec_faction_limits = Faction Limits +config.sec_power = Power +config.sec_power_loss = Power Loss +config.sec_regen = Regeneration +config.sec_claims = Claims +config.sec_decay = Decay +config.sec_protection = Claim Protection +config.sec_combat_tag = Combat Tag +config.sec_friendly_fire = Friendly Fire +config.sec_spawn_prot = Spawn Protection +config.sec_relations = Relations +config.sec_invites = Invites +config.sec_stuck = Stuck Command +config.sec_format = Chat Format +config.sec_colors = Colors +config.sec_rel_colors = Relation Colors +config.sec_faction_chat = Faction Chat +config.sec_history = Chat History +config.sec_backup = Backup +config.sec_economy = Economy +config.sec_announcements = Announcements +config.sec_map_display = Map Display +config.sec_visibility = Player Visibility +config.sec_mixin = Mixin +config.sec_territory_notify = Territory Notifications +config.sec_wilderness = Wilderness Messages +config.sec_currency = Currency +config.sec_treasury_limits = Treasury Limits +config.sec_fees = Fees +config.sec_upkeep = Upkeep +config.sec_proximity = Proximity Mode +config.sec_incremental = Incremental Mode +config.sec_debounced = Debounced Mode +config.sec_debug_global = Global +config.sec_debug_categories = Categories +config.sec_sentry = Sentry +config.sec_gravestone_protection = Protection +config.sec_gravestone_access = Access +config.sec_gravestone_loot = Loot Rules diff --git a/src/main/resources/Server/Languages/es-ES/hyperfactions_admin.lang b/src/main/resources/Server/Languages/es-ES/hyperfactions_admin.lang index 605b811f..d9ff1680 100644 --- a/src/main/resources/Server/Languages/es-ES/hyperfactions_admin.lang +++ b/src/main/resources/Server/Languages/es-ES/hyperfactions_admin.lang @@ -799,3 +799,136 @@ gui.zone_entry_edit_map = Editar Mapa gui.zone_entry_flags = Flags gui.zone_entry_settings = Ajustes gui.zone_entry_delete = Eliminar + +# ========== Updates Page ========== +updates.version_info = Informacion de Version +updates.current_version = Actual: +updates.build_date = Compilado: +updates.latest_version = Ultima: +updates.channel = Canal: +updates.update_status = Estado de Actualizacion +updates.status_checking = Buscando actualizaciones... +updates.status_up_to_date = Actualizado +updates.status_available = Nueva version disponible: v{0} +updates.status_downloading = Descargando actualizacion... +updates.status_downloaded = Actualizacion descargada exitosamente! +updates.status_failed = Error al buscar actualizaciones. +updates.btn_check = Buscar Actualizaciones +updates.btn_download = Descargar e Instalar +updates.btn_rollback = Revertir a +updates.changelog_title = Registro de Cambios +updates.no_changelog = Sin registro de cambios disponible. +updates.rollback_section = Reversion +updates.rollback_confirm = Confirmar Reversion +updates.rollback_unsafe = La reversion no es segura (el servidor se reinicio desde la actualizacion). +updates.rollback_success = Revertido a v{0}. Reinicie para aplicar. +updates.rollback_failed = Reversion fallida: {0} +updates.mixin_title = HyperProtect Mixin +updates.mixin_version = Version: +updates.mixin_check = Verificar Actualizacion Mixin +updates.mixin_up_to_date = Mixin esta actualizado. +updates.mixin_available = Actualizacion de mixin disponible: v{0} +updates.mixin_downloading = Descargando actualizacion de mixin... +updates.mixin_downloaded = Actualizacion de mixin descargada! +updates.mixin_failed = Error al descargar actualizacion de mixin. +updates.mixin_not_installed = No instalado +updates.restart_required = Se requiere reinicio del servidor. +updates.pre_release = (pre-lanzamiento) +# ========== Backups Page ========== +backups.title = Backup Management +backups.total_count = Backups ({0} total) +backups.empty = No backups found. +backups.btn_create = Create Backup +backups.btn_restore = Restore +backups.btn_delete = Delete +backups.name_placeholder = Name: +backups.creating = Creating backup... +backups.created = Backup created: {0} +backups.create_failed = Failed to create backup +backups.type_hourly = Hourly +backups.type_daily = Daily +backups.type_weekly = Weekly +backups.type_manual = Manual +backups.type_migration = Migration +backups.detail_type = Type: +backups.detail_created = Created: +backups.detail_size = Size: +backups.restore_warning = Warning: This will overwrite current data! +backups.restore_confirm = Confirm Restore +backups.restoring = Restoring backup... +backups.restored = Restored {0} ({1} files). +backups.restore_failed = Restore failed +backups.delete_confirm = Confirm Delete +backups.deleting = Deleting backup... +backups.deleted = Deleted backup: {0} +backups.delete_failed = Failed to delete backup. +backups.reload_required = Run /f reload to apply restored data. + +# ========== Config Editor ========== +config.tab_server = Server +config.tab_chat = Chat +config.tab_announcements = Announce +config.tab_economy = Economy +config.tab_factions = Factions +config.tab_faction_perms = Fac Perms +config.tab_worldmap = Worldmap +config.tab_worlds = Worlds +config.tab_backup = Backup +config.tab_debug = Debug +config.tab_gravestones = Graves +config.changes_pending = changes pending +config.no_changes = No changes +config.btn_save = Save +config.btn_revert = Revert +config.btn_reset = Reset Defaults +config.saved = Configuration saved. +config.reverted = Changes reverted. +config.reset_confirm = Confirm Reset +config.reset_done = Configuration reset to defaults. +config.sec_teleport = Teleport +config.sec_autosave = Auto-Save +config.sec_messages = Messages +config.sec_gui = GUI +config.sec_permissions = Permissions +config.sec_language = Language +config.sec_mob_clear = Mob Clearing +config.sec_updates = Updates +config.sec_faction_limits = Faction Limits +config.sec_power = Power +config.sec_power_loss = Power Loss +config.sec_regen = Regeneration +config.sec_claims = Claims +config.sec_decay = Decay +config.sec_protection = Claim Protection +config.sec_combat_tag = Combat Tag +config.sec_friendly_fire = Friendly Fire +config.sec_spawn_prot = Spawn Protection +config.sec_relations = Relations +config.sec_invites = Invites +config.sec_stuck = Stuck Command +config.sec_format = Chat Format +config.sec_colors = Colors +config.sec_rel_colors = Relation Colors +config.sec_faction_chat = Faction Chat +config.sec_history = Chat History +config.sec_backup = Backup +config.sec_economy = Economy +config.sec_announcements = Announcements +config.sec_map_display = Map Display +config.sec_visibility = Player Visibility +config.sec_mixin = Mixin +config.sec_territory_notify = Territory Notifications +config.sec_wilderness = Wilderness Messages +config.sec_currency = Currency +config.sec_treasury_limits = Treasury Limits +config.sec_fees = Fees +config.sec_upkeep = Upkeep +config.sec_proximity = Proximity Mode +config.sec_incremental = Incremental Mode +config.sec_debounced = Debounced Mode +config.sec_debug_global = Global +config.sec_debug_categories = Categories +config.sec_sentry = Sentry +config.sec_gravestone_protection = Protection +config.sec_gravestone_access = Access +config.sec_gravestone_loot = Loot Rules diff --git a/src/main/resources/Server/Languages/fr-FR/hyperfactions_admin.lang b/src/main/resources/Server/Languages/fr-FR/hyperfactions_admin.lang index ddab89ea..fc368325 100644 --- a/src/main/resources/Server/Languages/fr-FR/hyperfactions_admin.lang +++ b/src/main/resources/Server/Languages/fr-FR/hyperfactions_admin.lang @@ -799,3 +799,136 @@ gui.zone_entry_edit_map = Modifier la Carte gui.zone_entry_flags = Drapeaux gui.zone_entry_settings = Paramètres gui.zone_entry_delete = Supprimer + +# ========== Updates Page ========== +updates.version_info = Informations de Version +updates.current_version = Actuelle: +updates.build_date = Compilee: +updates.latest_version = Derniere: +updates.channel = Canal: +updates.update_status = Statut de Mise a Jour +updates.status_checking = Recherche de mises a jour... +updates.status_up_to_date = A jour +updates.status_available = Nouvelle version disponible: v{0} +updates.status_downloading = Telechargement en cours... +updates.status_downloaded = Mise a jour telechargee avec succes! +updates.status_failed = Echec de la verification des mises a jour. +updates.btn_check = Verifier les Mises a Jour +updates.btn_download = Telecharger & Installer +updates.btn_rollback = Revenir a +updates.changelog_title = Journal des Modifications +updates.no_changelog = Aucun journal des modifications disponible. +updates.rollback_section = Retour en Arriere +updates.rollback_confirm = Confirmer le Retour +updates.rollback_unsafe = Le retour n'est pas sur (le serveur a redemarré depuis la mise a jour). +updates.rollback_success = Revenu a v{0}. Redemarrez pour appliquer. +updates.rollback_failed = Echec du retour: {0} +updates.mixin_title = HyperProtect Mixin +updates.mixin_version = Version: +updates.mixin_check = Verifier Mise a Jour Mixin +updates.mixin_up_to_date = Le mixin est a jour. +updates.mixin_available = Mise a jour mixin disponible: v{0} +updates.mixin_downloading = Telechargement de la mise a jour mixin... +updates.mixin_downloaded = Mise a jour mixin telechargee! +updates.mixin_failed = Echec du telechargement de la mise a jour mixin. +updates.mixin_not_installed = Non installe +updates.restart_required = Redemarrage du serveur requis. +updates.pre_release = (pre-version) +# ========== Backups Page ========== +backups.title = Backup Management +backups.total_count = Backups ({0} total) +backups.empty = No backups found. +backups.btn_create = Create Backup +backups.btn_restore = Restore +backups.btn_delete = Delete +backups.name_placeholder = Name: +backups.creating = Creating backup... +backups.created = Backup created: {0} +backups.create_failed = Failed to create backup +backups.type_hourly = Hourly +backups.type_daily = Daily +backups.type_weekly = Weekly +backups.type_manual = Manual +backups.type_migration = Migration +backups.detail_type = Type: +backups.detail_created = Created: +backups.detail_size = Size: +backups.restore_warning = Warning: This will overwrite current data! +backups.restore_confirm = Confirm Restore +backups.restoring = Restoring backup... +backups.restored = Restored {0} ({1} files). +backups.restore_failed = Restore failed +backups.delete_confirm = Confirm Delete +backups.deleting = Deleting backup... +backups.deleted = Deleted backup: {0} +backups.delete_failed = Failed to delete backup. +backups.reload_required = Run /f reload to apply restored data. + +# ========== Config Editor ========== +config.tab_server = Server +config.tab_chat = Chat +config.tab_announcements = Announce +config.tab_economy = Economy +config.tab_factions = Factions +config.tab_faction_perms = Fac Perms +config.tab_worldmap = Worldmap +config.tab_worlds = Worlds +config.tab_backup = Backup +config.tab_debug = Debug +config.tab_gravestones = Graves +config.changes_pending = changes pending +config.no_changes = No changes +config.btn_save = Save +config.btn_revert = Revert +config.btn_reset = Reset Defaults +config.saved = Configuration saved. +config.reverted = Changes reverted. +config.reset_confirm = Confirm Reset +config.reset_done = Configuration reset to defaults. +config.sec_teleport = Teleport +config.sec_autosave = Auto-Save +config.sec_messages = Messages +config.sec_gui = GUI +config.sec_permissions = Permissions +config.sec_language = Language +config.sec_mob_clear = Mob Clearing +config.sec_updates = Updates +config.sec_faction_limits = Faction Limits +config.sec_power = Power +config.sec_power_loss = Power Loss +config.sec_regen = Regeneration +config.sec_claims = Claims +config.sec_decay = Decay +config.sec_protection = Claim Protection +config.sec_combat_tag = Combat Tag +config.sec_friendly_fire = Friendly Fire +config.sec_spawn_prot = Spawn Protection +config.sec_relations = Relations +config.sec_invites = Invites +config.sec_stuck = Stuck Command +config.sec_format = Chat Format +config.sec_colors = Colors +config.sec_rel_colors = Relation Colors +config.sec_faction_chat = Faction Chat +config.sec_history = Chat History +config.sec_backup = Backup +config.sec_economy = Economy +config.sec_announcements = Announcements +config.sec_map_display = Map Display +config.sec_visibility = Player Visibility +config.sec_mixin = Mixin +config.sec_territory_notify = Territory Notifications +config.sec_wilderness = Wilderness Messages +config.sec_currency = Currency +config.sec_treasury_limits = Treasury Limits +config.sec_fees = Fees +config.sec_upkeep = Upkeep +config.sec_proximity = Proximity Mode +config.sec_incremental = Incremental Mode +config.sec_debounced = Debounced Mode +config.sec_debug_global = Global +config.sec_debug_categories = Categories +config.sec_sentry = Sentry +config.sec_gravestone_protection = Protection +config.sec_gravestone_access = Access +config.sec_gravestone_loot = Loot Rules diff --git a/src/main/resources/Server/Languages/it-IT/hyperfactions_admin.lang b/src/main/resources/Server/Languages/it-IT/hyperfactions_admin.lang index 87561a43..e75d59a1 100644 --- a/src/main/resources/Server/Languages/it-IT/hyperfactions_admin.lang +++ b/src/main/resources/Server/Languages/it-IT/hyperfactions_admin.lang @@ -799,3 +799,136 @@ gui.zone_entry_edit_map = Modifica Mappa gui.zone_entry_flags = Flag gui.zone_entry_settings = Impostazioni gui.zone_entry_delete = Elimina + +# ========== Updates Page ========== +updates.version_info = Informazioni Versione +updates.current_version = Attuale: +updates.build_date = Compilata: +updates.latest_version = Ultima: +updates.channel = Canale: +updates.update_status = Stato Aggiornamento +updates.status_checking = Ricerca aggiornamenti... +updates.status_up_to_date = Aggiornato +updates.status_available = Nuova versione disponibile: v{0} +updates.status_downloading = Download aggiornamento in corso... +updates.status_downloaded = Aggiornamento scaricato con successo! +updates.status_failed = Errore nella verifica aggiornamenti. +updates.btn_check = Cerca Aggiornamenti +updates.btn_download = Scarica & Installa +updates.btn_rollback = Ripristina a +updates.changelog_title = Registro Modifiche +updates.no_changelog = Nessun registro modifiche disponibile. +updates.rollback_section = Ripristino +updates.rollback_confirm = Conferma Ripristino +updates.rollback_unsafe = Il ripristino non e sicuro (il server e stato riavviato dopo l'aggiornamento). +updates.rollback_success = Ripristinato a v{0}. Riavvia per applicare. +updates.rollback_failed = Ripristino fallito: {0} +updates.mixin_title = HyperProtect Mixin +updates.mixin_version = Versione: +updates.mixin_check = Verifica Aggiornamento Mixin +updates.mixin_up_to_date = Il mixin e aggiornato. +updates.mixin_available = Aggiornamento mixin disponibile: v{0} +updates.mixin_downloading = Download aggiornamento mixin... +updates.mixin_downloaded = Aggiornamento mixin scaricato! +updates.mixin_failed = Errore nel download dell'aggiornamento mixin. +updates.mixin_not_installed = Non installato +updates.restart_required = Riavvio del server richiesto. +updates.pre_release = (pre-rilascio) +# ========== Backups Page ========== +backups.title = Backup Management +backups.total_count = Backups ({0} total) +backups.empty = No backups found. +backups.btn_create = Create Backup +backups.btn_restore = Restore +backups.btn_delete = Delete +backups.name_placeholder = Name: +backups.creating = Creating backup... +backups.created = Backup created: {0} +backups.create_failed = Failed to create backup +backups.type_hourly = Hourly +backups.type_daily = Daily +backups.type_weekly = Weekly +backups.type_manual = Manual +backups.type_migration = Migration +backups.detail_type = Type: +backups.detail_created = Created: +backups.detail_size = Size: +backups.restore_warning = Warning: This will overwrite current data! +backups.restore_confirm = Confirm Restore +backups.restoring = Restoring backup... +backups.restored = Restored {0} ({1} files). +backups.restore_failed = Restore failed +backups.delete_confirm = Confirm Delete +backups.deleting = Deleting backup... +backups.deleted = Deleted backup: {0} +backups.delete_failed = Failed to delete backup. +backups.reload_required = Run /f reload to apply restored data. + +# ========== Config Editor ========== +config.tab_server = Server +config.tab_chat = Chat +config.tab_announcements = Announce +config.tab_economy = Economy +config.tab_factions = Factions +config.tab_faction_perms = Fac Perms +config.tab_worldmap = Worldmap +config.tab_worlds = Worlds +config.tab_backup = Backup +config.tab_debug = Debug +config.tab_gravestones = Graves +config.changes_pending = changes pending +config.no_changes = No changes +config.btn_save = Save +config.btn_revert = Revert +config.btn_reset = Reset Defaults +config.saved = Configuration saved. +config.reverted = Changes reverted. +config.reset_confirm = Confirm Reset +config.reset_done = Configuration reset to defaults. +config.sec_teleport = Teleport +config.sec_autosave = Auto-Save +config.sec_messages = Messages +config.sec_gui = GUI +config.sec_permissions = Permissions +config.sec_language = Language +config.sec_mob_clear = Mob Clearing +config.sec_updates = Updates +config.sec_faction_limits = Faction Limits +config.sec_power = Power +config.sec_power_loss = Power Loss +config.sec_regen = Regeneration +config.sec_claims = Claims +config.sec_decay = Decay +config.sec_protection = Claim Protection +config.sec_combat_tag = Combat Tag +config.sec_friendly_fire = Friendly Fire +config.sec_spawn_prot = Spawn Protection +config.sec_relations = Relations +config.sec_invites = Invites +config.sec_stuck = Stuck Command +config.sec_format = Chat Format +config.sec_colors = Colors +config.sec_rel_colors = Relation Colors +config.sec_faction_chat = Faction Chat +config.sec_history = Chat History +config.sec_backup = Backup +config.sec_economy = Economy +config.sec_announcements = Announcements +config.sec_map_display = Map Display +config.sec_visibility = Player Visibility +config.sec_mixin = Mixin +config.sec_territory_notify = Territory Notifications +config.sec_wilderness = Wilderness Messages +config.sec_currency = Currency +config.sec_treasury_limits = Treasury Limits +config.sec_fees = Fees +config.sec_upkeep = Upkeep +config.sec_proximity = Proximity Mode +config.sec_incremental = Incremental Mode +config.sec_debounced = Debounced Mode +config.sec_debug_global = Global +config.sec_debug_categories = Categories +config.sec_sentry = Sentry +config.sec_gravestone_protection = Protection +config.sec_gravestone_access = Access +config.sec_gravestone_loot = Loot Rules diff --git a/src/main/resources/Server/Languages/nl-NL/hyperfactions_admin.lang b/src/main/resources/Server/Languages/nl-NL/hyperfactions_admin.lang index c06a292c..cef0bb43 100644 --- a/src/main/resources/Server/Languages/nl-NL/hyperfactions_admin.lang +++ b/src/main/resources/Server/Languages/nl-NL/hyperfactions_admin.lang @@ -799,3 +799,136 @@ gui.zone_entry_edit_map = Kaart Bewerken gui.zone_entry_flags = Vlaggen gui.zone_entry_settings = Instellingen gui.zone_entry_delete = Verwijderen + +# ========== Updates Page ========== +updates.version_info = Versie-informatie +updates.current_version = Huidig: +updates.build_date = Gebouwd: +updates.latest_version = Nieuwste: +updates.channel = Kanaal: +updates.update_status = Update Status +updates.status_checking = Zoeken naar updates... +updates.status_up_to_date = Up-to-date +updates.status_available = Nieuwe versie beschikbaar: v{0} +updates.status_downloading = Update downloaden... +updates.status_downloaded = Update succesvol gedownload! +updates.status_failed = Fout bij het zoeken naar updates. +updates.btn_check = Controleer op Updates +updates.btn_download = Downloaden & Installeren +updates.btn_rollback = Terugdraaien naar +updates.changelog_title = Wijzigingslogboek +updates.no_changelog = Geen wijzigingslogboek beschikbaar. +updates.rollback_section = Terugdraaien +updates.rollback_confirm = Bevestig Terugdraaien +updates.rollback_unsafe = Terugdraaien is niet veilig (server is herstart sinds de update). +updates.rollback_success = Teruggedraaid naar v{0}. Herstart om toe te passen. +updates.rollback_failed = Terugdraaien mislukt: {0} +updates.mixin_title = HyperProtect Mixin +updates.mixin_version = Versie: +updates.mixin_check = Controleer Mixin Update +updates.mixin_up_to_date = Mixin is up-to-date. +updates.mixin_available = Mixin update beschikbaar: v{0} +updates.mixin_downloading = Mixin update downloaden... +updates.mixin_downloaded = Mixin update gedownload! +updates.mixin_failed = Fout bij het downloaden van mixin update. +updates.mixin_not_installed = Niet geinstalleerd +updates.restart_required = Server herstart vereist. +updates.pre_release = (pre-release) +# ========== Backups Page ========== +backups.title = Backup Management +backups.total_count = Backups ({0} total) +backups.empty = No backups found. +backups.btn_create = Create Backup +backups.btn_restore = Restore +backups.btn_delete = Delete +backups.name_placeholder = Name: +backups.creating = Creating backup... +backups.created = Backup created: {0} +backups.create_failed = Failed to create backup +backups.type_hourly = Hourly +backups.type_daily = Daily +backups.type_weekly = Weekly +backups.type_manual = Manual +backups.type_migration = Migration +backups.detail_type = Type: +backups.detail_created = Created: +backups.detail_size = Size: +backups.restore_warning = Warning: This will overwrite current data! +backups.restore_confirm = Confirm Restore +backups.restoring = Restoring backup... +backups.restored = Restored {0} ({1} files). +backups.restore_failed = Restore failed +backups.delete_confirm = Confirm Delete +backups.deleting = Deleting backup... +backups.deleted = Deleted backup: {0} +backups.delete_failed = Failed to delete backup. +backups.reload_required = Run /f reload to apply restored data. + +# ========== Config Editor ========== +config.tab_server = Server +config.tab_chat = Chat +config.tab_announcements = Announce +config.tab_economy = Economy +config.tab_factions = Factions +config.tab_faction_perms = Fac Perms +config.tab_worldmap = Worldmap +config.tab_worlds = Worlds +config.tab_backup = Backup +config.tab_debug = Debug +config.tab_gravestones = Graves +config.changes_pending = changes pending +config.no_changes = No changes +config.btn_save = Save +config.btn_revert = Revert +config.btn_reset = Reset Defaults +config.saved = Configuration saved. +config.reverted = Changes reverted. +config.reset_confirm = Confirm Reset +config.reset_done = Configuration reset to defaults. +config.sec_teleport = Teleport +config.sec_autosave = Auto-Save +config.sec_messages = Messages +config.sec_gui = GUI +config.sec_permissions = Permissions +config.sec_language = Language +config.sec_mob_clear = Mob Clearing +config.sec_updates = Updates +config.sec_faction_limits = Faction Limits +config.sec_power = Power +config.sec_power_loss = Power Loss +config.sec_regen = Regeneration +config.sec_claims = Claims +config.sec_decay = Decay +config.sec_protection = Claim Protection +config.sec_combat_tag = Combat Tag +config.sec_friendly_fire = Friendly Fire +config.sec_spawn_prot = Spawn Protection +config.sec_relations = Relations +config.sec_invites = Invites +config.sec_stuck = Stuck Command +config.sec_format = Chat Format +config.sec_colors = Colors +config.sec_rel_colors = Relation Colors +config.sec_faction_chat = Faction Chat +config.sec_history = Chat History +config.sec_backup = Backup +config.sec_economy = Economy +config.sec_announcements = Announcements +config.sec_map_display = Map Display +config.sec_visibility = Player Visibility +config.sec_mixin = Mixin +config.sec_territory_notify = Territory Notifications +config.sec_wilderness = Wilderness Messages +config.sec_currency = Currency +config.sec_treasury_limits = Treasury Limits +config.sec_fees = Fees +config.sec_upkeep = Upkeep +config.sec_proximity = Proximity Mode +config.sec_incremental = Incremental Mode +config.sec_debounced = Debounced Mode +config.sec_debug_global = Global +config.sec_debug_categories = Categories +config.sec_sentry = Sentry +config.sec_gravestone_protection = Protection +config.sec_gravestone_access = Access +config.sec_gravestone_loot = Loot Rules diff --git a/src/main/resources/Server/Languages/pl-PL/hyperfactions_admin.lang b/src/main/resources/Server/Languages/pl-PL/hyperfactions_admin.lang index cc9395c8..c161a243 100644 --- a/src/main/resources/Server/Languages/pl-PL/hyperfactions_admin.lang +++ b/src/main/resources/Server/Languages/pl-PL/hyperfactions_admin.lang @@ -799,3 +799,136 @@ gui.zone_entry_edit_map = Edytuj mapę gui.zone_entry_flags = Flagi gui.zone_entry_settings = Ustawienia gui.zone_entry_delete = Usuń + +# ========== Updates Page ========== +updates.version_info = Informacje o Wersji +updates.current_version = Aktualna: +updates.build_date = Zbudowana: +updates.latest_version = Najnowsza: +updates.channel = Kanal: +updates.update_status = Status Aktualizacji +updates.status_checking = Sprawdzanie aktualizacji... +updates.status_up_to_date = Aktualna wersja +updates.status_available = Dostepna nowa wersja: v{0} +updates.status_downloading = Pobieranie aktualizacji... +updates.status_downloaded = Aktualizacja pobrana pomyslnie! +updates.status_failed = Blad sprawdzania aktualizacji. +updates.btn_check = Sprawdz Aktualizacje +updates.btn_download = Pobierz i Zainstaluj +updates.btn_rollback = Przywroc do +updates.changelog_title = Dziennik Zmian +updates.no_changelog = Brak dziennika zmian. +updates.rollback_section = Przywracanie +updates.rollback_confirm = Potwierdz Przywracanie +updates.rollback_unsafe = Przywracanie nie jest bezpieczne (serwer zostal uruchomiony ponownie od aktualizacji). +updates.rollback_success = Przywrocono do v{0}. Uruchom ponownie aby zastosowac. +updates.rollback_failed = Przywracanie nie powiodlo sie: {0} +updates.mixin_title = HyperProtect Mixin +updates.mixin_version = Wersja: +updates.mixin_check = Sprawdz Aktualizacje Mixin +updates.mixin_up_to_date = Mixin jest aktualny. +updates.mixin_available = Dostepna aktualizacja mixin: v{0} +updates.mixin_downloading = Pobieranie aktualizacji mixin... +updates.mixin_downloaded = Aktualizacja mixin pobrana! +updates.mixin_failed = Blad pobierania aktualizacji mixin. +updates.mixin_not_installed = Nie zainstalowany +updates.restart_required = Wymagane ponowne uruchomienie serwera. +updates.pre_release = (przedpremierowa) +# ========== Backups Page ========== +backups.title = Backup Management +backups.total_count = Backups ({0} total) +backups.empty = No backups found. +backups.btn_create = Create Backup +backups.btn_restore = Restore +backups.btn_delete = Delete +backups.name_placeholder = Name: +backups.creating = Creating backup... +backups.created = Backup created: {0} +backups.create_failed = Failed to create backup +backups.type_hourly = Hourly +backups.type_daily = Daily +backups.type_weekly = Weekly +backups.type_manual = Manual +backups.type_migration = Migration +backups.detail_type = Type: +backups.detail_created = Created: +backups.detail_size = Size: +backups.restore_warning = Warning: This will overwrite current data! +backups.restore_confirm = Confirm Restore +backups.restoring = Restoring backup... +backups.restored = Restored {0} ({1} files). +backups.restore_failed = Restore failed +backups.delete_confirm = Confirm Delete +backups.deleting = Deleting backup... +backups.deleted = Deleted backup: {0} +backups.delete_failed = Failed to delete backup. +backups.reload_required = Run /f reload to apply restored data. + +# ========== Config Editor ========== +config.tab_server = Server +config.tab_chat = Chat +config.tab_announcements = Announce +config.tab_economy = Economy +config.tab_factions = Factions +config.tab_faction_perms = Fac Perms +config.tab_worldmap = Worldmap +config.tab_worlds = Worlds +config.tab_backup = Backup +config.tab_debug = Debug +config.tab_gravestones = Graves +config.changes_pending = changes pending +config.no_changes = No changes +config.btn_save = Save +config.btn_revert = Revert +config.btn_reset = Reset Defaults +config.saved = Configuration saved. +config.reverted = Changes reverted. +config.reset_confirm = Confirm Reset +config.reset_done = Configuration reset to defaults. +config.sec_teleport = Teleport +config.sec_autosave = Auto-Save +config.sec_messages = Messages +config.sec_gui = GUI +config.sec_permissions = Permissions +config.sec_language = Language +config.sec_mob_clear = Mob Clearing +config.sec_updates = Updates +config.sec_faction_limits = Faction Limits +config.sec_power = Power +config.sec_power_loss = Power Loss +config.sec_regen = Regeneration +config.sec_claims = Claims +config.sec_decay = Decay +config.sec_protection = Claim Protection +config.sec_combat_tag = Combat Tag +config.sec_friendly_fire = Friendly Fire +config.sec_spawn_prot = Spawn Protection +config.sec_relations = Relations +config.sec_invites = Invites +config.sec_stuck = Stuck Command +config.sec_format = Chat Format +config.sec_colors = Colors +config.sec_rel_colors = Relation Colors +config.sec_faction_chat = Faction Chat +config.sec_history = Chat History +config.sec_backup = Backup +config.sec_economy = Economy +config.sec_announcements = Announcements +config.sec_map_display = Map Display +config.sec_visibility = Player Visibility +config.sec_mixin = Mixin +config.sec_territory_notify = Territory Notifications +config.sec_wilderness = Wilderness Messages +config.sec_currency = Currency +config.sec_treasury_limits = Treasury Limits +config.sec_fees = Fees +config.sec_upkeep = Upkeep +config.sec_proximity = Proximity Mode +config.sec_incremental = Incremental Mode +config.sec_debounced = Debounced Mode +config.sec_debug_global = Global +config.sec_debug_categories = Categories +config.sec_sentry = Sentry +config.sec_gravestone_protection = Protection +config.sec_gravestone_access = Access +config.sec_gravestone_loot = Loot Rules diff --git a/src/main/resources/Server/Languages/pt-BR/hyperfactions_admin.lang b/src/main/resources/Server/Languages/pt-BR/hyperfactions_admin.lang index 3188d15f..451bf25b 100644 --- a/src/main/resources/Server/Languages/pt-BR/hyperfactions_admin.lang +++ b/src/main/resources/Server/Languages/pt-BR/hyperfactions_admin.lang @@ -799,3 +799,136 @@ gui.zone_entry_edit_map = Editar Mapa gui.zone_entry_flags = Flags gui.zone_entry_settings = Configurações gui.zone_entry_delete = Excluir + +# ========== Updates Page ========== +updates.version_info = Informacoes de Versao +updates.current_version = Atual: +updates.build_date = Compilada: +updates.latest_version = Ultima: +updates.channel = Canal: +updates.update_status = Status da Atualizacao +updates.status_checking = Verificando atualizacoes... +updates.status_up_to_date = Atualizado +updates.status_available = Nova versao disponivel: v{0} +updates.status_downloading = Baixando atualizacao... +updates.status_downloaded = Atualizacao baixada com sucesso! +updates.status_failed = Erro ao verificar atualizacoes. +updates.btn_check = Verificar Atualizacoes +updates.btn_download = Baixar & Instalar +updates.btn_rollback = Reverter para +updates.changelog_title = Registro de Alteracoes +updates.no_changelog = Nenhum registro de alteracoes disponivel. +updates.rollback_section = Reversao +updates.rollback_confirm = Confirmar Reversao +updates.rollback_unsafe = A reversao nao e segura (o servidor foi reiniciado desde a atualizacao). +updates.rollback_success = Revertido para v{0}. Reinicie para aplicar. +updates.rollback_failed = Reversao falhou: {0} +updates.mixin_title = HyperProtect Mixin +updates.mixin_version = Versao: +updates.mixin_check = Verificar Atualizacao Mixin +updates.mixin_up_to_date = Mixin esta atualizado. +updates.mixin_available = Atualizacao de mixin disponivel: v{0} +updates.mixin_downloading = Baixando atualizacao de mixin... +updates.mixin_downloaded = Atualizacao de mixin baixada! +updates.mixin_failed = Erro ao baixar atualizacao de mixin. +updates.mixin_not_installed = Nao instalado +updates.restart_required = Reinicio do servidor necessario. +updates.pre_release = (pre-lancamento) +# ========== Backups Page ========== +backups.title = Backup Management +backups.total_count = Backups ({0} total) +backups.empty = No backups found. +backups.btn_create = Create Backup +backups.btn_restore = Restore +backups.btn_delete = Delete +backups.name_placeholder = Name: +backups.creating = Creating backup... +backups.created = Backup created: {0} +backups.create_failed = Failed to create backup +backups.type_hourly = Hourly +backups.type_daily = Daily +backups.type_weekly = Weekly +backups.type_manual = Manual +backups.type_migration = Migration +backups.detail_type = Type: +backups.detail_created = Created: +backups.detail_size = Size: +backups.restore_warning = Warning: This will overwrite current data! +backups.restore_confirm = Confirm Restore +backups.restoring = Restoring backup... +backups.restored = Restored {0} ({1} files). +backups.restore_failed = Restore failed +backups.delete_confirm = Confirm Delete +backups.deleting = Deleting backup... +backups.deleted = Deleted backup: {0} +backups.delete_failed = Failed to delete backup. +backups.reload_required = Run /f reload to apply restored data. + +# ========== Config Editor ========== +config.tab_server = Server +config.tab_chat = Chat +config.tab_announcements = Announce +config.tab_economy = Economy +config.tab_factions = Factions +config.tab_faction_perms = Fac Perms +config.tab_worldmap = Worldmap +config.tab_worlds = Worlds +config.tab_backup = Backup +config.tab_debug = Debug +config.tab_gravestones = Graves +config.changes_pending = changes pending +config.no_changes = No changes +config.btn_save = Save +config.btn_revert = Revert +config.btn_reset = Reset Defaults +config.saved = Configuration saved. +config.reverted = Changes reverted. +config.reset_confirm = Confirm Reset +config.reset_done = Configuration reset to defaults. +config.sec_teleport = Teleport +config.sec_autosave = Auto-Save +config.sec_messages = Messages +config.sec_gui = GUI +config.sec_permissions = Permissions +config.sec_language = Language +config.sec_mob_clear = Mob Clearing +config.sec_updates = Updates +config.sec_faction_limits = Faction Limits +config.sec_power = Power +config.sec_power_loss = Power Loss +config.sec_regen = Regeneration +config.sec_claims = Claims +config.sec_decay = Decay +config.sec_protection = Claim Protection +config.sec_combat_tag = Combat Tag +config.sec_friendly_fire = Friendly Fire +config.sec_spawn_prot = Spawn Protection +config.sec_relations = Relations +config.sec_invites = Invites +config.sec_stuck = Stuck Command +config.sec_format = Chat Format +config.sec_colors = Colors +config.sec_rel_colors = Relation Colors +config.sec_faction_chat = Faction Chat +config.sec_history = Chat History +config.sec_backup = Backup +config.sec_economy = Economy +config.sec_announcements = Announcements +config.sec_map_display = Map Display +config.sec_visibility = Player Visibility +config.sec_mixin = Mixin +config.sec_territory_notify = Territory Notifications +config.sec_wilderness = Wilderness Messages +config.sec_currency = Currency +config.sec_treasury_limits = Treasury Limits +config.sec_fees = Fees +config.sec_upkeep = Upkeep +config.sec_proximity = Proximity Mode +config.sec_incremental = Incremental Mode +config.sec_debounced = Debounced Mode +config.sec_debug_global = Global +config.sec_debug_categories = Categories +config.sec_sentry = Sentry +config.sec_gravestone_protection = Protection +config.sec_gravestone_access = Access +config.sec_gravestone_loot = Loot Rules diff --git a/src/main/resources/Server/Languages/ru-RU/hyperfactions_admin.lang b/src/main/resources/Server/Languages/ru-RU/hyperfactions_admin.lang index e766a7a1..d9ba018f 100644 --- a/src/main/resources/Server/Languages/ru-RU/hyperfactions_admin.lang +++ b/src/main/resources/Server/Languages/ru-RU/hyperfactions_admin.lang @@ -799,3 +799,136 @@ gui.zone_entry_edit_map = Редактировать карту gui.zone_entry_flags = Флаги gui.zone_entry_settings = Настройки gui.zone_entry_delete = Удалить + +# ========== Updates Page ========== +updates.version_info = Informatsiya o versii +updates.current_version = Tekushchaya: +updates.build_date = Sborka: +updates.latest_version = Poslednyaya: +updates.channel = Kanal: +updates.update_status = Status obnovleniya +updates.status_checking = Proverka obnovleniy... +updates.status_up_to_date = Obnovleno +updates.status_available = Dostupna novaya versiya: v{0} +updates.status_downloading = Zagruzka obnovleniya... +updates.status_downloaded = Obnovlenie uspeshno zagruzheno! +updates.status_failed = Oshibka proverki obnovleniy. +updates.btn_check = Proverit' obnovleniya +updates.btn_download = Skachat' i Ustanovit' +updates.btn_rollback = Otkatit' do +updates.changelog_title = Zhurnal izmeneniy +updates.no_changelog = Zhurnal izmeneniy nedostupen. +updates.rollback_section = Otkat +updates.rollback_confirm = Podtverdit' otkat +updates.rollback_unsafe = Otkat nebezopasen (server byl perezapushchen posle obnovleniya). +updates.rollback_success = Otkacheno do v{0}. Perezapustite dlya primeneniya. +updates.rollback_failed = Otkat ne udalsia: {0} +updates.mixin_title = HyperProtect Mixin +updates.mixin_version = Versiya: +updates.mixin_check = Proverit' obnovlenie Mixin +updates.mixin_up_to_date = Mixin obnovlen. +updates.mixin_available = Dostupno obnovlenie mixin: v{0} +updates.mixin_downloading = Zagruzka obnovleniya mixin... +updates.mixin_downloaded = Obnovlenie mixin zagruzheno! +updates.mixin_failed = Oshibka zagruzki obnovleniya mixin. +updates.mixin_not_installed = Ne ustanovlen +updates.restart_required = Trebuetsya perezapusk servera. +updates.pre_release = (predvarit. versiya) +# ========== Backups Page ========== +backups.title = Backup Management +backups.total_count = Backups ({0} total) +backups.empty = No backups found. +backups.btn_create = Create Backup +backups.btn_restore = Restore +backups.btn_delete = Delete +backups.name_placeholder = Name: +backups.creating = Creating backup... +backups.created = Backup created: {0} +backups.create_failed = Failed to create backup +backups.type_hourly = Hourly +backups.type_daily = Daily +backups.type_weekly = Weekly +backups.type_manual = Manual +backups.type_migration = Migration +backups.detail_type = Type: +backups.detail_created = Created: +backups.detail_size = Size: +backups.restore_warning = Warning: This will overwrite current data! +backups.restore_confirm = Confirm Restore +backups.restoring = Restoring backup... +backups.restored = Restored {0} ({1} files). +backups.restore_failed = Restore failed +backups.delete_confirm = Confirm Delete +backups.deleting = Deleting backup... +backups.deleted = Deleted backup: {0} +backups.delete_failed = Failed to delete backup. +backups.reload_required = Run /f reload to apply restored data. + +# ========== Config Editor ========== +config.tab_server = Server +config.tab_chat = Chat +config.tab_announcements = Announce +config.tab_economy = Economy +config.tab_factions = Factions +config.tab_faction_perms = Fac Perms +config.tab_worldmap = Worldmap +config.tab_worlds = Worlds +config.tab_backup = Backup +config.tab_debug = Debug +config.tab_gravestones = Graves +config.changes_pending = changes pending +config.no_changes = No changes +config.btn_save = Save +config.btn_revert = Revert +config.btn_reset = Reset Defaults +config.saved = Configuration saved. +config.reverted = Changes reverted. +config.reset_confirm = Confirm Reset +config.reset_done = Configuration reset to defaults. +config.sec_teleport = Teleport +config.sec_autosave = Auto-Save +config.sec_messages = Messages +config.sec_gui = GUI +config.sec_permissions = Permissions +config.sec_language = Language +config.sec_mob_clear = Mob Clearing +config.sec_updates = Updates +config.sec_faction_limits = Faction Limits +config.sec_power = Power +config.sec_power_loss = Power Loss +config.sec_regen = Regeneration +config.sec_claims = Claims +config.sec_decay = Decay +config.sec_protection = Claim Protection +config.sec_combat_tag = Combat Tag +config.sec_friendly_fire = Friendly Fire +config.sec_spawn_prot = Spawn Protection +config.sec_relations = Relations +config.sec_invites = Invites +config.sec_stuck = Stuck Command +config.sec_format = Chat Format +config.sec_colors = Colors +config.sec_rel_colors = Relation Colors +config.sec_faction_chat = Faction Chat +config.sec_history = Chat History +config.sec_backup = Backup +config.sec_economy = Economy +config.sec_announcements = Announcements +config.sec_map_display = Map Display +config.sec_visibility = Player Visibility +config.sec_mixin = Mixin +config.sec_territory_notify = Territory Notifications +config.sec_wilderness = Wilderness Messages +config.sec_currency = Currency +config.sec_treasury_limits = Treasury Limits +config.sec_fees = Fees +config.sec_upkeep = Upkeep +config.sec_proximity = Proximity Mode +config.sec_incremental = Incremental Mode +config.sec_debounced = Debounced Mode +config.sec_debug_global = Global +config.sec_debug_categories = Categories +config.sec_sentry = Sentry +config.sec_gravestone_protection = Protection +config.sec_gravestone_access = Access +config.sec_gravestone_loot = Loot Rules diff --git a/src/main/resources/Server/Languages/tl-PH/hyperfactions_admin.lang b/src/main/resources/Server/Languages/tl-PH/hyperfactions_admin.lang index 0708fde8..744b6373 100644 --- a/src/main/resources/Server/Languages/tl-PH/hyperfactions_admin.lang +++ b/src/main/resources/Server/Languages/tl-PH/hyperfactions_admin.lang @@ -799,3 +799,136 @@ gui.zone_entry_edit_map = I-edit ang Mapa gui.zone_entry_flags = Mga Flag gui.zone_entry_settings = Mga Setting gui.zone_entry_delete = Tanggalin + +# ========== Updates Page ========== +updates.version_info = Impormasyon ng Bersyon +updates.current_version = Kasalukuyan: +updates.build_date = Ginawa: +updates.latest_version = Pinakabago: +updates.channel = Channel: +updates.update_status = Status ng Update +updates.status_checking = Naghahanap ng mga update... +updates.status_up_to_date = Na-update na +updates.status_available = Bagong bersyon available: v{0} +updates.status_downloading = Dina-download ang update... +updates.status_downloaded = Matagumpay na na-download ang update! +updates.status_failed = Hindi makapag-check ng mga update. +updates.btn_check = Mag-check ng Updates +updates.btn_download = I-download at I-install +updates.btn_rollback = I-rollback sa +updates.changelog_title = Mga Pagbabago +updates.no_changelog = Walang changelog na available. +updates.rollback_section = Rollback +updates.rollback_confirm = Kumpirmahin ang Rollback +updates.rollback_unsafe = Hindi ligtas ang rollback (na-restart na ang server pagkatapos ng update). +updates.rollback_success = Na-rollback sa v{0}. I-restart para mag-apply. +updates.rollback_failed = Nabigo ang rollback: {0} +updates.mixin_title = HyperProtect Mixin +updates.mixin_version = Bersyon: +updates.mixin_check = I-check ang Mixin Update +updates.mixin_up_to_date = Ang mixin ay na-update na. +updates.mixin_available = May available na mixin update: v{0} +updates.mixin_downloading = Dina-download ang mixin update... +updates.mixin_downloaded = Na-download na ang mixin update! +updates.mixin_failed = Hindi ma-download ang mixin update. +updates.mixin_not_installed = Hindi naka-install +updates.restart_required = Kailangan i-restart ang server. +updates.pre_release = (pre-release) +# ========== Backups Page ========== +backups.title = Backup Management +backups.total_count = Backups ({0} total) +backups.empty = No backups found. +backups.btn_create = Create Backup +backups.btn_restore = Restore +backups.btn_delete = Delete +backups.name_placeholder = Name: +backups.creating = Creating backup... +backups.created = Backup created: {0} +backups.create_failed = Failed to create backup +backups.type_hourly = Hourly +backups.type_daily = Daily +backups.type_weekly = Weekly +backups.type_manual = Manual +backups.type_migration = Migration +backups.detail_type = Type: +backups.detail_created = Created: +backups.detail_size = Size: +backups.restore_warning = Warning: This will overwrite current data! +backups.restore_confirm = Confirm Restore +backups.restoring = Restoring backup... +backups.restored = Restored {0} ({1} files). +backups.restore_failed = Restore failed +backups.delete_confirm = Confirm Delete +backups.deleting = Deleting backup... +backups.deleted = Deleted backup: {0} +backups.delete_failed = Failed to delete backup. +backups.reload_required = Run /f reload to apply restored data. + +# ========== Config Editor ========== +config.tab_server = Server +config.tab_chat = Chat +config.tab_announcements = Announce +config.tab_economy = Economy +config.tab_factions = Factions +config.tab_faction_perms = Fac Perms +config.tab_worldmap = Worldmap +config.tab_worlds = Worlds +config.tab_backup = Backup +config.tab_debug = Debug +config.tab_gravestones = Graves +config.changes_pending = changes pending +config.no_changes = No changes +config.btn_save = Save +config.btn_revert = Revert +config.btn_reset = Reset Defaults +config.saved = Configuration saved. +config.reverted = Changes reverted. +config.reset_confirm = Confirm Reset +config.reset_done = Configuration reset to defaults. +config.sec_teleport = Teleport +config.sec_autosave = Auto-Save +config.sec_messages = Messages +config.sec_gui = GUI +config.sec_permissions = Permissions +config.sec_language = Language +config.sec_mob_clear = Mob Clearing +config.sec_updates = Updates +config.sec_faction_limits = Faction Limits +config.sec_power = Power +config.sec_power_loss = Power Loss +config.sec_regen = Regeneration +config.sec_claims = Claims +config.sec_decay = Decay +config.sec_protection = Claim Protection +config.sec_combat_tag = Combat Tag +config.sec_friendly_fire = Friendly Fire +config.sec_spawn_prot = Spawn Protection +config.sec_relations = Relations +config.sec_invites = Invites +config.sec_stuck = Stuck Command +config.sec_format = Chat Format +config.sec_colors = Colors +config.sec_rel_colors = Relation Colors +config.sec_faction_chat = Faction Chat +config.sec_history = Chat History +config.sec_backup = Backup +config.sec_economy = Economy +config.sec_announcements = Announcements +config.sec_map_display = Map Display +config.sec_visibility = Player Visibility +config.sec_mixin = Mixin +config.sec_territory_notify = Territory Notifications +config.sec_wilderness = Wilderness Messages +config.sec_currency = Currency +config.sec_treasury_limits = Treasury Limits +config.sec_fees = Fees +config.sec_upkeep = Upkeep +config.sec_proximity = Proximity Mode +config.sec_incremental = Incremental Mode +config.sec_debounced = Debounced Mode +config.sec_debug_global = Global +config.sec_debug_categories = Categories +config.sec_sentry = Sentry +config.sec_gravestone_protection = Protection +config.sec_gravestone_access = Access +config.sec_gravestone_loot = Loot Rules