Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/hyperfactions/HyperFactions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -264,7 +264,12 @@ private void dispatchCommand(@NotNull CommandContext ctx, @Nullable Store<Entity
}
Player playerEntity = store.getComponent(ref, Player.getComponentType());
if (playerEntity != null) {
hyperFactions.getGuiManager().openAdminConfig(playerEntity, ref, store, player);
String tab = subArgs.length > 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" -> {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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));
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigFile.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*/
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<String> 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) {}
Expand All@@ -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<String, WorldSettings> entry : config.getWorlds().entrySet()) {
String key = entry.getKey();
Expand All@@ -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);
}

/**
Expand DownExpand Up@@ -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();
Expand DownExpand Up@@ -180,13 +173,4 @@ public boolean isDefaultAllow() {
return defaultAllow;
}

/**
* Gets the claim blacklist.
*
* @return unmodifiable set of blacklisted world names
*/
@NotNull
public Set<String> getClaimBlacklist() {
return Collections.unmodifiableSet(claimBlacklist);
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -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() {
Expand DownExpand Up@@ -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; }
}
20 changes: 20 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/BackupConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/ChatConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/DebugConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) ===

/**
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/hyperfactions/HyperFactions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -264,7 +264,12 @@ private void dispatchCommand(@NotNull CommandContext ctx, @Nullable Store<Entity
}
Player playerEntity = store.getComponent(ref, Player.getComponentType());
if (playerEntity != null) {
hyperFactions.getGuiManager().openAdminConfig(playerEntity, ref, store, player);
String tab = subArgs.length > 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" -> {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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));
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigFile.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*/
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<String> 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) {}
Expand All@@ -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<String, WorldSettings> entry : config.getWorlds().entrySet()) {
String key = entry.getKey();
Expand All@@ -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);
}

/**
Expand DownExpand Up@@ -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();
Expand DownExpand Up@@ -180,13 +173,4 @@ public boolean isDefaultAllow() {
return defaultAllow;
}

/**
* Gets the claim blacklist.
*
* @return unmodifiable set of blacklisted world names
*/
@NotNull
public Set<String> getClaimBlacklist() {
return Collections.unmodifiableSet(claimBlacklist);
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -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() {
Expand DownExpand Up@@ -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; }
}
20 changes: 20 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/BackupConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/ChatConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/DebugConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) ===

/**
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/hyperfactions/HyperFactions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -264,7 +264,12 @@ private void dispatchCommand(@NotNull CommandContext ctx, @Nullable Store<Entity
}
Player playerEntity = store.getComponent(ref, Player.getComponentType());
if (playerEntity != null) {
hyperFactions.getGuiManager().openAdminConfig(playerEntity, ref, store, player);
String tab = subArgs.length > 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" -> {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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));
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigFile.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*/
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<String> 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) {}
Expand All@@ -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<String, WorldSettings> entry : config.getWorlds().entrySet()) {
String key = entry.getKey();
Expand All@@ -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);
}

/**
Expand DownExpand Up@@ -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();
Expand DownExpand Up@@ -180,13 +173,4 @@ public boolean isDefaultAllow() {
return defaultAllow;
}

/**
* Gets the claim blacklist.
*
* @return unmodifiable set of blacklisted world names
*/
@NotNull
public Set<String> getClaimBlacklist() {
return Collections.unmodifiableSet(claimBlacklist);
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -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() {
Expand DownExpand Up@@ -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; }
}
20 changes: 20 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/BackupConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/ChatConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/DebugConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) ===

/**
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/hyperfactions/HyperFactions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -264,7 +264,12 @@ private void dispatchCommand(@NotNull CommandContext ctx, @Nullable Store<Entity
}
Player playerEntity = store.getComponent(ref, Player.getComponentType());
if (playerEntity != null) {
hyperFactions.getGuiManager().openAdminConfig(playerEntity, ref, store, player);
String tab = subArgs.length > 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" -> {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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));
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigFile.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*/
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<String> 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) {}
Expand All@@ -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<String, WorldSettings> entry : config.getWorlds().entrySet()) {
String key = entry.getKey();
Expand All@@ -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);
}

/**
Expand DownExpand Up@@ -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();
Expand DownExpand Up@@ -180,13 +173,4 @@ public boolean isDefaultAllow() {
return defaultAllow;
}

/**
* Gets the claim blacklist.
*
* @return unmodifiable set of blacklisted world names
*/
@NotNull
public Set<String> getClaimBlacklist() {
return Collections.unmodifiableSet(claimBlacklist);
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -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() {
Expand DownExpand Up@@ -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; }
}
20 changes: 20 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/BackupConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/ChatConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/DebugConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) ===

/**
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/hyperfactions/HyperFactions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -264,7 +264,12 @@ private void dispatchCommand(@NotNull CommandContext ctx, @Nullable Store<Entity
}
Player playerEntity = store.getComponent(ref, Player.getComponentType());
if (playerEntity != null) {
hyperFactions.getGuiManager().openAdminConfig(playerEntity, ref, store, player);
String tab = subArgs.length > 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" -> {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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));
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigFile.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*/
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<String> 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) {}
Expand All@@ -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<String, WorldSettings> entry : config.getWorlds().entrySet()) {
String key = entry.getKey();
Expand All@@ -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);
}

/**
Expand DownExpand Up@@ -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();
Expand DownExpand Up@@ -180,13 +173,4 @@ public boolean isDefaultAllow() {
return defaultAllow;
}

/**
* Gets the claim blacklist.
*
* @return unmodifiable set of blacklisted world names
*/
@NotNull
public Set<String> getClaimBlacklist() {
return Collections.unmodifiableSet(claimBlacklist);
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -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() {
Expand DownExpand Up@@ -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; }
}
20 changes: 20 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/BackupConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/ChatConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/DebugConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) ===

/**
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/hyperfactions/HyperFactions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -264,7 +264,12 @@ private void dispatchCommand(@NotNull CommandContext ctx, @Nullable Store<Entity
}
Player playerEntity = store.getComponent(ref, Player.getComponentType());
if (playerEntity != null) {
hyperFactions.getGuiManager().openAdminConfig(playerEntity, ref, store, player);
String tab = subArgs.length > 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" -> {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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));
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigFile.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*/
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<String> 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) {}
Expand All@@ -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<String, WorldSettings> entry : config.getWorlds().entrySet()) {
String key = entry.getKey();
Expand All@@ -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);
}

/**
Expand DownExpand Up@@ -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();
Expand DownExpand Up@@ -180,13 +173,4 @@ public boolean isDefaultAllow() {
return defaultAllow;
}

/**
* Gets the claim blacklist.
*
* @return unmodifiable set of blacklisted world names
*/
@NotNull
public Set<String> getClaimBlacklist() {
return Collections.unmodifiableSet(claimBlacklist);
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -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() {
Expand DownExpand Up@@ -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; }
}
20 changes: 20 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/BackupConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/ChatConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/DebugConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) ===

/**
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/hyperfactions/HyperFactions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -264,7 +264,12 @@ private void dispatchCommand(@NotNull CommandContext ctx, @Nullable Store<Entity
}
Player playerEntity = store.getComponent(ref, Player.getComponentType());
if (playerEntity != null) {
hyperFactions.getGuiManager().openAdminConfig(playerEntity, ref, store, player);
String tab = subArgs.length > 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" -> {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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));
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigFile.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*/
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<String> 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) {}
Expand All@@ -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<String, WorldSettings> entry : config.getWorlds().entrySet()) {
String key = entry.getKey();
Expand All@@ -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);
}

/**
Expand DownExpand Up@@ -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();
Expand DownExpand Up@@ -180,13 +173,4 @@ public boolean isDefaultAllow() {
return defaultAllow;
}

/**
* Gets the claim blacklist.
*
* @return unmodifiable set of blacklisted world names
*/
@NotNull
public Set<String> getClaimBlacklist() {
return Collections.unmodifiableSet(claimBlacklist);
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -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() {
Expand DownExpand Up@@ -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; }
}
20 changes: 20 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/BackupConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/ChatConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/DebugConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) ===

/**
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/hyperfactions/HyperFactions.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -264,7 +264,12 @@ private void dispatchCommand(@NotNull CommandContext ctx, @Nullable Store<Entity
}
Player playerEntity = store.getComponent(ref, Player.getComponentType());
if (playerEntity != null) {
hyperFactions.getGuiManager().openAdminConfig(playerEntity, ref, store, player);
String tab = subArgs.length > 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" -> {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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));
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigFile.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/hyperfactions/config/ConfigManager.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
*/
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<String> 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) {}
Expand All@@ -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<String, WorldSettings> entry : config.getWorlds().entrySet()) {
String key = entry.getKey();
Expand All@@ -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);
}

/**
Expand DownExpand Up@@ -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();
Expand DownExpand Up@@ -180,13 +173,4 @@ public boolean isDefaultAllow() {
return defaultAllow;
}

/**
* Gets the claim blacklist.
*
* @return unmodifiable set of blacklisted world names
*/
@NotNull
public Set<String> getClaimBlacklist() {
return Collections.unmodifiableSet(claimBlacklist);
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -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() {
Expand DownExpand Up@@ -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; }
}
20 changes: 20 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/BackupConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/ChatConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 . */
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/hyperfactions/config/modules/DebugConfig.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) ===

/**
Expand Down
Loading
Loading