diff --git a/src/main/java/com/hyperfactions/command/admin/handler/AdminImportHandler.java b/src/main/java/com/hyperfactions/command/admin/handler/AdminImportHandler.java index e738ee5..80dd24b 100644 --- a/src/main/java/com/hyperfactions/command/admin/handler/AdminImportHandler.java +++ b/src/main/java/com/hyperfactions/command/admin/handler/AdminImportHandler.java @@ -3,6 +3,7 @@ import com.hyperfactions.HyperFactions; import com.hyperfactions.command.util.CommandUtil; import com.hyperfactions.importer.ElbaphFactionsImporter; +import com.hyperfactions.importer.FactionsXImporter; import com.hyperfactions.importer.HyFactionsImporter; import com.hyperfactions.importer.ImportResult; import com.hyperfactions.util.CommandHelp; @@ -59,6 +60,7 @@ public void handleAdminImport(CommandContext ctx, String[] args) { switch (subCmd) { case "hyfactions" -> handleImportHyFactions(ctx, subArgs); case "elbaphfactions" -> handleImportElbaphFactions(ctx, subArgs); + case "factionsx" -> handleImportFactionsX(ctx, subArgs); case "help", "?" -> showImportHelp(ctx); default -> { ctx.sendMessage(prefix().insert(msg("Unknown import source: " + subCmd, COLOR_RED))); @@ -73,6 +75,8 @@ private void showImportHelp(CommandContext ctx) { commands.add(new CommandHelp(" Default path: mods/Kaws_Hyfaction", "")); commands.add(new CommandHelp("/f admin import elbaphfactions [path] [flags]", "Import from ElbaphFactions mod")); commands.add(new CommandHelp(" Default path: mods/ElbaphFactions", "")); + commands.add(new CommandHelp("/f admin import factionsx [path] [flags]", "Import from FactionsX mod")); + commands.add(new CommandHelp(" Default path: mods/FactionsX", "")); commands.add(new CommandHelp(" Flags:", "")); commands.add(new CommandHelp(" --dry-run / -n", "Simulate without changes")); commands.add(new CommandHelp(" --overwrite", "Replace existing factions")); @@ -187,6 +191,59 @@ public void handleImportElbaphFactions(CommandContext ctx, String[] args) { .thenAccept(result -> reportImportResult(ctx, result, finalDryRun, "ElbaphFactions")); } + /** Handles import factions x. */ + public void handleImportFactionsX(CommandContext ctx, String[] args) { + // Parse path (optional - default to mods/FactionsX) + String pathStr = "mods/FactionsX"; + int flagStartIndex = 0; + + if (args.length > 0 && !args[0].startsWith("-")) { + pathStr = args[0]; + flagStartIndex = 1; + } + + Path dataPath = Paths.get(pathStr); + + boolean dryRun = false; + boolean overwrite = false; + boolean skipZones = false; + boolean skipPower = false; + + for (int i = flagStartIndex; i < args.length; i++) { + String flag = args[i].toLowerCase(); + switch (flag) { + case "--dry-run", "-n" -> dryRun = true; + case "--overwrite" -> overwrite = true; + case "--no-zones" -> skipZones = true; + case "--no-power" -> skipPower = true; + default -> throw new IllegalStateException("Unexpected value"); + } + } + + ctx.sendMessage(prefix().insert(msg("Importing from FactionsX...", COLOR_YELLOW))); + ctx.sendMessage(msg(" Path: " + dataPath, COLOR_GRAY)); + if (dryRun) { + ctx.sendMessage(msg(" (Dry run - no changes will be made)", COLOR_GRAY)); + } + + FactionsXImporter importer = new FactionsXImporter( + hyperFactions.getFactionManager(), + hyperFactions.getClaimManager(), + hyperFactions.getZoneManager(), + hyperFactions.getPowerManager(), + hyperFactions.getBackupManager() + ); + + importer.setDryRun(dryRun); + importer.setOverwrite(overwrite); + importer.setSkipZones(skipZones); + importer.setSkipPower(skipPower); + + final boolean finalDryRun = dryRun; + CompletableFuture.supplyAsync(() -> importer.importFrom(dataPath)) + .thenAccept(result -> reportImportResult(ctx, result, finalDryRun, "FactionsX")); + } + private void reportImportResult(CommandContext ctx, ImportResult result, boolean dryRun, String sourceName) { if (!result.hasErrors()) { ctx.sendMessage(prefix().insert(msg(sourceName + " import " + (dryRun ? "simulation " : "") + "complete!", COLOR_GREEN))); diff --git a/src/main/java/com/hyperfactions/importer/ElbaphFactionsImporter.java b/src/main/java/com/hyperfactions/importer/ElbaphFactionsImporter.java index 5adf3fc..f2c564a 100644 --- a/src/main/java/com/hyperfactions/importer/ElbaphFactionsImporter.java +++ b/src/main/java/com/hyperfactions/importer/ElbaphFactionsImporter.java @@ -419,11 +419,15 @@ private ElbaphZones loadZonesForValidation(File sourceDir, ImportValidationRepor public ImportResult importFrom(@NotNull Path sourcePath) { ImportResult.Builder result = ImportResult.builder().dryRun(dryRun); - // Check HyFactions importer isn't running + // Check other importers aren't running if (HyFactionsImporter.isImportInProgress()) { result.error("A HyFactions import is already in progress. Please wait for it to complete."); return result.build(); } + if (FactionsXImporter.isImportInProgress()) { + result.error("A FactionsX import is already in progress. Please wait for it to complete."); + return result.build(); + } // Thread safety: prevent concurrent imports if (!importLock.tryLock()) { diff --git a/src/main/java/com/hyperfactions/importer/FactionsXImporter.java b/src/main/java/com/hyperfactions/importer/FactionsXImporter.java new file mode 100644 index 0000000..f297fe2 --- /dev/null +++ b/src/main/java/com/hyperfactions/importer/FactionsXImporter.java @@ -0,0 +1,1350 @@ +package com.hyperfactions.importer; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.hyperfactions.backup.BackupManager; +import com.hyperfactions.backup.BackupType; +import com.hyperfactions.config.ConfigManager; +import com.hyperfactions.data.*; +import com.hyperfactions.importer.factionsx.*; +import com.hyperfactions.manager.ClaimManager; +import com.hyperfactions.manager.FactionManager; +import com.hyperfactions.manager.PowerManager; +import com.hyperfactions.manager.ZoneManager; +import com.hyperfactions.util.Logger; +import java.io.File; +import java.io.FileReader; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Imports faction data from FactionsX mod (by Humblegod666) into HyperFactions. + * Thread-safe: only one import can run at a time. + * + *
FactionsX data layout (under {@code mods/FactionsX/config/}): + *
Key differences from other importers: + *
FactionsX permissions control what each role can do within faction territory.
+ * We map "Build" to Break+Place, "Interact" to Interact, and leave other flags as defaults.
+ * The outsider/ally equivalent permissions are not stored in FactionsX, so we use defaults.
+ */
+ @Nullable
+ private FactionPermissions convertPermissions(
+ @Nullable Map Note: FactionsX stores the Z coordinate under the key "ChunkY" — this is a naming
+ * bug inherited from HyFactions. Use {@link #getChunkZ()} for the actual Z coordinate.
+ */
+public record FxChunkInfo(
+ @Nullable String UUID,
+ int ChunkX,
+ int ChunkY,
+ @Nullable FxTracker CreatedTracker
+) {
+
+ /** Returns the actual chunk Z coordinate (stored as ChunkY in FactionsX data). */
+ public int getChunkZ() {
+ return ChunkY;
+ }
+}
diff --git a/src/main/java/com/hyperfactions/importer/factionsx/FxClaims.java b/src/main/java/com/hyperfactions/importer/factionsx/FxClaims.java
new file mode 100644
index 0000000..fe4468a
--- /dev/null
+++ b/src/main/java/com/hyperfactions/importer/factionsx/FxClaims.java
@@ -0,0 +1,11 @@
+package com.hyperfactions.importer.factionsx;
+
+import java.util.List;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Gson-mapped record for FactionsX Claims.json root object.
+ */
+public record FxClaims(
+ @Nullable List Key quirk: the {@code Members} map does NOT include the faction owner.
+ * The owner is always LEADER and stored separately in the {@code Owner} field.
+ */
+public record FxFaction(
+ @Nullable String Id,
+ @Nullable String Owner,
+ @Nullable String Name,
+ int Color,
+ @Nullable String Description,
+ @Nullable FxTracker CreatedTracker,
+ @Nullable FxTracker ModifiedTracker,
+ @Nullable String HomeDimension,
+ double HomeX,
+ double HomeY,
+ double HomeZ,
+ float HomeYaw,
+ float HomePitch,
+ @Nullable Map Power is per-player (not per-faction). FactionId/FactionRole may be null if the player
+ * is not in a faction.
+ */
+public record FxPlayer(
+ @Nullable String Uuid,
+ @Nullable String LastKnownName,
+ @Nullable String FactionId,
+ @Nullable String FactionRole,
+ int Power,
+ int MaxPower
+) {}
diff --git a/src/main/java/com/hyperfactions/importer/factionsx/FxTracker.java b/src/main/java/com/hyperfactions/importer/factionsx/FxTracker.java
new file mode 100644
index 0000000..4b67071
--- /dev/null
+++ b/src/main/java/com/hyperfactions/importer/factionsx/FxTracker.java
@@ -0,0 +1,42 @@
+package com.hyperfactions.importer.factionsx;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Gson-mapped record for FactionsX tracker objects (CreatedTracker / ModifiedTracker).
+ * Date is a LocalDateTime ISO-8601 string (e.g. "2026-01-22T22:17:02.563322051").
+ */
+public record FxTracker(
+ @Nullable String UserUUID,
+ @Nullable String UserName,
+ @Nullable String Date
+) {
+
+ /**
+ * Parses the ISO-8601 date string to epoch milliseconds.
+ * Falls back to current time if parsing fails.
+ */
+ public long toEpochMillis() {
+ if (Date == null || Date.isEmpty()) {
+ return System.currentTimeMillis();
+ }
+
+ try {
+ // FactionsX uses LocalDateTime.now().toString() which produces ISO-8601 without zone
+ LocalDateTime ldt = LocalDateTime.parse(Date, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+ return ldt.toInstant(ZoneOffset.UTC).toEpochMilli();
+ } catch (DateTimeParseException e) {
+ // Try ISO instant format as fallback
+ try {
+ return Instant.parse(Date).toEpochMilli();
+ } catch (DateTimeParseException e2) {
+ return System.currentTimeMillis();
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/hyperfactions/importer/factionsx/FxZoneChunk.java b/src/main/java/com/hyperfactions/importer/factionsx/FxZoneChunk.java
new file mode 100644
index 0000000..2e92313
--- /dev/null
+++ b/src/main/java/com/hyperfactions/importer/factionsx/FxZoneChunk.java
@@ -0,0 +1,13 @@
+package com.hyperfactions.importer.factionsx;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Parsed zone chunk record (not a direct JSON mapping).
+ * Created by splitting the {@code "chunkX:chunkZ"} strings from FactionsX Zones.json.
+ */
+public record FxZoneChunk(
+ @NotNull String dimension,
+ int chunkX,
+ int chunkZ
+) {}
diff --git a/src/main/java/com/hyperfactions/importer/factionsx/FxZones.java b/src/main/java/com/hyperfactions/importer/factionsx/FxZones.java
new file mode 100644
index 0000000..902fea0
--- /dev/null
+++ b/src/main/java/com/hyperfactions/importer/factionsx/FxZones.java
@@ -0,0 +1,16 @@
+package com.hyperfactions.importer.factionsx;
+
+import java.util.List;
+import java.util.Map;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Gson-mapped record for FactionsX Zones.json root object.
+ *
+ * Zone chunks are stored as {@code "chunkX:chunkZ"} strings grouped by dimension name.
+ * Keys are "Safezone" and "Warzone" (singular, matching FactionsX source).
+ */
+public record FxZones(
+ @Nullable Map