Skip to content

Latest commit

 

History

History
806 lines (647 loc) · 25.1 KB

File metadata and controls

806 lines (647 loc) · 25.1 KB

HyperFactions Storage Layer

Version: 0.10.0

Architecture documentation for the HyperFactions data persistence system.

Overview

HyperFactions uses an interface-based storage layer with:

  • Storage Interfaces - Abstract contracts for data operations
  • JSON Implementations - File-based storage with pretty-printed JSON
  • Async Operations - All I/O returns CompletableFuture for non-blocking
  • Data Models - Java records for immutable data structures
  • Auto-Save - Periodic saves with configurable interval
  • Safe-Save - Atomic writes with SHA-256 checksums, backup recovery, .bak auto-cleanup
  • Per-UUID Locking - JsonPlayerStorage uses per-UUID locks to prevent concurrent load-modify-save race conditions (e.g., simultaneous deaths losing kill/death increments)
  • Migration Support - Automatic config (v1→v6) and data (v0→v1) format upgrades
  • Backup System - GFS rotation with hourly/daily/weekly/manual/migration types
  • Import Directories - Data import from ElbaphFactions and HyFactions

Architecture

Storage Interface                  Implementation
      │                                  │
FactionStorage ────────────────► JsonFactionStorage
PlayerStorage  ────────────────► JsonPlayerStorage
ZoneStorage    ────────────────► JsonZoneStorage
      │                                  │
      └──────── Data Models ◄────────────┘
                    │
           Faction, PlayerPower,
           Zone, FactionClaim, etc.

Backup System
      │
BackupManager ─────────────────► ZIP archives in backups/
      │                          (GFS rotation: hourly, daily, weekly)
      │
      └── BackupMetadata ──────► Filename-encoded metadata

Data Directory Structure

<server>/mods/com.hyperfactions_HyperFactions/
├── config/                        # Configuration files
│   ├── factions.json              # Faction gameplay settings
│   ├── server.json                # Server behavior settings
│   └── ...                        # Other module configs
├── data/                          # All data files (migrated from root in v0→v1)
│   ├── factions/                  # Per-faction JSON files
│   │   └── {uuid}.json
│   ├── players/                   # Per-player power data
│   │   └── {uuid}.json
│   ├── chat/                      # Per-faction chat history
│   │   └── {factionId}.json
│   ├── economy/                   # Per-faction treasury data
│   │   └── {factionId}.json
│   ├── zones.json                 # All zones in one file
│   ├── invites.json               # Pending faction invites
│   ├── join_requests.json         # Pending join requests
│   └── .version                   # Data layout version marker (currently: 1)
└── backups/                       # Backup archives
    ├── hourly_2025-01-15_12-00-00.zip
    ├── daily_2025-01-15_00-00-00.zip
    ├── weekly_2025-01-13_00-00-00.zip
    ├── manual_my-backup.zip
    └── migration_v3-to-v4_2025-01-15_00-00-00.zip

Backup System

The BackupManager implements GFS (Grandfather-Father-Son) rotation for automatic backup management.

Backup Types

Type Auto-Rotated Default Retention
HOURLY Yes Last 24
DAILY Yes Last 7
WEEKLY Yes Last 4
MANUAL No Keep all (configurable)
MIGRATION No Keep all

Backup Contents

Each ZIP archive contains:

  • data/factions/ — All faction JSON files
  • data/players/ — All player power JSON files
  • data/chat/ — Per-faction chat history files
  • data/economy/ — Per-faction treasury data files
  • data/zones.json — Zone definitions
  • data/invites.json — Pending faction invites
  • data/join_requests.json — Pending join requests
  • config/ — Configuration files (factions.json, server.json, etc.)

Key Operations

Method Description
createBackup(type) Create async ZIP backup
restoreBackup(name) Async ZIP extraction + reload
listBackups() List sorted by timestamp (newest first)
performRotation() GFS cleanup of old backups
startScheduledBackups() Schedule hourly backups (72,000 ticks)

See Data Import & Migration for import directory details and config migration.

Key Classes

Class Path Purpose
FactionStorage storage/FactionStorage.java Faction storage interface
PlayerStorage storage/PlayerStorage.java Player power storage interface
ZoneStorage storage/ZoneStorage.java Zone storage interface
JsonFactionStorage storage/json/JsonFactionStorage.java JSON faction storage
JsonPlayerStorage storage/json/JsonPlayerStorage.java JSON player storage
JsonZoneStorage storage/json/JsonZoneStorage.java JSON zone storage
ChatHistoryStorage storage/ChatHistoryStorage.java Chat history storage interface
JsonChatHistoryStorage storage/json/JsonChatHistoryStorage.java JSON chat history storage
JsonEconomyStorage storage/JsonEconomyStorage.java JSON economy/treasury storage
StorageUtils storage/StorageUtils.java Atomic write, checksum, backup recovery
StorageHealth storage/StorageHealth.java Storage health monitoring

Data Directory Structure

<server>/mods/com.hyperfactions_HyperFactions/
├── config/                        # Configuration files (see config.md)
│   ├── factions.json              # Faction gameplay settings
│   ├── server.json                # Server behavior settings
│   └── ...                        # Other module configs
├── data/                          # All data files
│   ├── factions/                  # One file per faction
│   │   └── {uuid}.json
│   ├── players/                   # One file per player
│   │   └── {uuid}.json
│   ├── chat/                      # Per-faction chat history
│   │   └── {factionId}.json
│   ├── economy/                   # Per-faction treasury data
│   │   └── {factionId}.json
│   ├── zones.json                 # All zones in single file
│   ├── invites.json               # Pending faction invites
│   ├── join_requests.json         # Pending join requests
│   └── .version                   # Data layout version (1)
├── update_preferences.json        # Update notification preferences
└── backups/                       # Backup storage (ZIP archives)
    └── backup_*.zip

Storage Interfaces

FactionStorage

storage/FactionStorage.java

public interface FactionStorage {

    /**
     * Initialize storage (create directories, etc.).
     */
    CompletableFuture<Void> init();

    /**
     * Shutdown storage (flush pending writes).
     */
    CompletableFuture<Void> shutdown();

    /**
     * Load a single faction by ID.
     */
    CompletableFuture<Optional<Faction>> loadFaction(UUID factionId);

    /**
     * Save a faction (create or update).
     */
    CompletableFuture<Void> saveFaction(Faction faction);

    /**
     * Delete a faction.
     */
    CompletableFuture<Void> deleteFaction(UUID factionId);

    /**
     * Load all factions.
     */
    CompletableFuture<Collection<Faction>> loadAllFactions();
}

PlayerStorage

storage/PlayerStorage.java

public interface PlayerStorage {

    CompletableFuture<Void> init();
    CompletableFuture<Void> shutdown();

    CompletableFuture<Optional<PlayerPower>> loadPlayerPower(UUID playerUuid);
    CompletableFuture<Void> savePlayerPower(PlayerPower power);
    CompletableFuture<Void> deletePlayerPower(UUID playerUuid);
    CompletableFuture<Collection<PlayerPower>> loadAllPlayerPower();
}

ZoneStorage

storage/ZoneStorage.java

public interface ZoneStorage {

    CompletableFuture<Void> init();
    CompletableFuture<Void> shutdown();

    CompletableFuture<Collection<Zone>> loadAllZones();
    CompletableFuture<Void> saveAllZones(Collection<Zone> zones);
}

JSON Implementations

JsonFactionStorage

storage/json/JsonFactionStorage.java

Stores one JSON file per faction in factions/ directory:

public class JsonFactionStorage implements FactionStorage {

    private final Path factionsDir;
    private final Gson gson;

    public JsonFactionStorage(Path dataDir) {
        this.factionsDir = dataDir.resolve("factions");
        this.gson = new GsonBuilder()
            .setPrettyPrinting()
            .serializeNulls()
            .create();
    }

    @Override
    public CompletableFuture<Void> saveFaction(Faction faction) {
        return CompletableFuture.runAsync(() -> {
            Path file = factionsDir.resolve(faction.id() + ".json");
            try (Writer writer = Files.newBufferedWriter(file)) {
                gson.toJson(factionToJson(faction), writer);
            }
        });
    }

    private Path getFactionFile(UUID factionId) {
        return factionsDir.resolve(factionId.toString() + ".json");
    }
}

JsonPlayerStorage

storage/json/JsonPlayerStorage.java

Stores one JSON file per player in players/ directory. Uses per-UUID locking to prevent race conditions from concurrent kill/death tracking:

public class JsonPlayerStorage implements PlayerStorage {

    private final Path playersDir;
    private final ConcurrentHashMap<UUID, ReentrantLock> playerLocks = new ConcurrentHashMap<>();

    @Override
    public CompletableFuture<Void> savePlayerPower(PlayerPower power) {
        return CompletableFuture.runAsync(() -> {
            Path file = playersDir.resolve(power.uuid() + ".json");
            // Write JSON...
        });
    }

    /**
     * Atomically update player data under a per-UUID lock.
     * Prevents lost updates from concurrent deaths/kills.
     */
    public CompletableFuture<Void> updatePlayerData(UUID uuid, UnaryOperator<PlayerData> updater) {
        return CompletableFuture.runAsync(() -> {
            ReentrantLock lock = playerLocks.computeIfAbsent(uuid, k -> new ReentrantLock());
            lock.lock();
            try {
                PlayerData data = loadPlayerDataSync(uuid);
                PlayerData updated = updater.apply(data);
                savePlayerDataSync(updated);
            } finally {
                lock.unlock();
            }
        });
    }
}

The updatePlayerData method ensures that concurrent operations (e.g., two simultaneous deaths) do not lose increments through unsynchronized load-modify-save cycles.

JsonZoneStorage

storage/json/JsonZoneStorage.java

Stores all zones in a single zones.json file (zones are typically few in number):

public class JsonZoneStorage implements ZoneStorage {

    private final Path zonesFile;

    @Override
    public CompletableFuture<Void> saveAllZones(Collection<Zone> zones) {
        return CompletableFuture.runAsync(() -> {
            // Write all zones as JSON array
        });
    }
}

Data Models

Faction

data/Faction.java

Mutable entity with builder-style setters:

public class Faction {
    private final UUID id;
    private String name;
    private String description;
    private String tag;
    private String color;
    private long createdAt;
    private boolean open;
    private FactionHome home;
    private final List<FactionMember> members;
    private final List<FactionClaim> claims;
    private final List<FactionRelation> relations;
    private final List<FactionLog> logs;
    private FactionPermissions permissions;

    // Getters and builder-style setters
    public Faction setName(String name) {
        this.name = name;
        return this;
    }
}

JSON Structure (factions/{uuid}.json):

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Warriors",
  "description": "A mighty faction",
  "tag": "WAR",
  "color": "c",
  "createdAt": 1706745600000,
  "open": false,
  "home": {
    "world": "world",
    "x": 100.5,
    "y": 64.0,
    "z": 200.5,
    "yaw": 90.0,
    "pitch": 0.0,
    "setAt": 1706745600000,
    "setBy": "player-uuid"
  },
  "members": [
    {
      "uuid": "player-uuid",
      "username": "PlayerName",
      "role": "LEADER",
      "joinedAt": 1706745600000,
      "lastOnline": 1706832000000
    }
  ],
  "claims": [
    {
      "world": "world",
      "chunkX": 10,
      "chunkZ": 20,
      "claimedAt": 1706745600000,
      "claimedBy": "player-uuid"
    }
  ],
  "relations": [
    {
      "targetFactionId": "other-uuid",
      "type": "ALLY",
      "since": 1706745600000
    }
  ],
  "logs": [
    {
      "type": "MEMBER_JOIN",
      "message": "PlayerName joined",
      "timestamp": 1706745600000,
      "actorUuid": "player-uuid"
    }
  ],
  "permissions": {
    "outsiderBreak": false,
    "memberBreak": true,
    "pvpEnabled": true
  }
}

FactionMember

data/FactionMember.java

public record FactionMember(
    UUID uuid,
    String username,
    FactionRole role,
    long joinedAt,
    long lastOnline
) {}

FactionRole

data/FactionRole.java

public enum FactionRole {
    LEADER,   // Full control
    OFFICER,  // Can manage members, claims
    MEMBER    // Basic permissions
}

PlayerPower

data/PlayerPower.java

public record PlayerPower(
    UUID uuid,
    double power,
    double maxPower,
    long lastDeath,
    long lastRegen
) {}

JSON Structure (players/{uuid}.json):

{
  "uuid": "550e8400-e29b-41d4-a716-446655440000",
  "power": 15.5,
  "maxPower": 20.0,
  "lastDeath": 1706745600000,
  "lastRegen": 1706832000000
}

Zone

data/Zone.java

public class Zone {
    private final UUID id;
    private String name;
    private ZoneType type;
    private String world;
    private final Set<ChunkKey> chunks;
    private long createdAt;
    private UUID createdBy;
    private final Map<String, Boolean> flags;
}

JSON Structure (zones.json):

[
  {
    "id": "zone-uuid",
    "name": "Spawn",
    "type": "SAFE",
    "world": "world",
    "chunks": [
      { "x": 0, "z": 0 },
      { "x": 0, "z": 1 }
    ],
    "createdAt": 1706745600000,
    "createdBy": "admin-uuid",
    "flags": {
      "pvp_enabled": false,
      "build_allowed": false
    }
  }
]

ChunkKey

data/ChunkKey.java

Immutable identifier for a chunk:

public record ChunkKey(String world, int x, int z) {

    @Override
    public int hashCode() {
        return Objects.hash(world, x, z);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ChunkKey other)) return false;
        return x == other.x && z == other.z && world.equals(other.world);
    }
}

Async Pattern

All storage operations are async to prevent blocking the main thread:

// In manager
public void loadAll() {
    factionStorage.loadAllFactions()
        .thenAccept(factions -> {
            for (Faction faction : factions) {
                cache.put(faction.id(), faction);
            }
        })
        .join(); // Block only during startup
}

public void saveFaction(Faction faction) {
    // Fire and forget during normal operation
    factionStorage.saveFaction(faction);
}

Startup Loading

During startup, .join() is used to ensure data is loaded before the plugin is ready:

// In HyperFactions.enable()
factionStorage.init().join();
playerStorage.init().join();
zoneStorage.init().join();

factionManager.loadAll().join();
powerManager.loadAll().join();
zoneManager.loadAll().join();

Runtime Saves

During normal operation, saves are fire-and-forget:

// In FactionManager
public void updateFaction(Faction faction) {
    cache.put(faction.id(), faction);
    factionStorage.saveFaction(faction); // Async, doesn't block
}

Auto-Save System

Configured in config/server.json:

{
  "autoSave": {
    "enabled": true,
    "intervalMinutes": 5
  }
}

Implementation in HyperFactions.java:

private void startAutoSaveTask() {
    int intervalMinutes = ConfigManager.get().getAutoSaveIntervalMinutes();
    int periodTicks = intervalMinutes * 60 * 20;

    autoSaveTaskId = scheduleRepeatingTask(periodTicks, periodTicks, this::saveAllData);
}

public void saveAllData() {
    Logger.info("Auto-saving data...");
    factionManager.saveAll().join();
    powerManager.saveAll().join();
    zoneManager.saveAll().join();
    Logger.info("Auto-save complete");
}

Safe-Save Mechanism

storage/StorageUtils.java

All 7 storage types use StorageUtils.writeAtomic() for crash-safe writes:

  1. Write content to a temp file (file.{counter}.tmp)
  2. Compute SHA-256 checksum of content
  3. Read back temp file and verify checksum matches
  4. Copy existing file to .bak backup
  5. Atomic rename: temp → target
  6. Delete .bak file (cleanup after successful write)

If the process crashes during steps 1-4, the original file is untouched. If it crashes during step 5, the .bak file provides recovery. On startup, cleanupOrphanedFiles() removes any stray .tmp or orphaned .bak files.

Storage Types Using writeAtomic()

Storage File Pattern Notes
JsonFactionStorage data/factions/{uuid}.json One file per faction
JsonPlayerStorage data/players/{uuid}.json One file per player
JsonZoneStorage data/zones.json Single file for all zones
JsonChatHistoryStorage data/chat/{factionId}.json One file per faction
JsonEconomyStorage data/economy/{factionId}.json One file per faction
InviteManager data/invites.json Single file
JoinRequestManager data/join_requests.json Single file

Data Migration

Data Directory Migration (v0→v1)

migration/migrations/data/DataV0ToV1Migration.java

Moves data files from the plugin root into a data/ subdirectory. The migration:

  • Creates data/ directory
  • Moves: factions/, players/, chat/, economy/, zones.json, invites.json, join_requests.json
  • Also moves any .bak files alongside their data files
  • Writes data/.version with 1 (last step — if crash before this, migration re-runs)
  • MigrationRunner creates ZIP backup before execution for rollback support

Detection: Runs when data/.version doesn't exist AND at least one old-path item exists.

Zone Format Migration

migration/MigrationRunner.java

Handles automatic data format upgrades:

Zone Format Migration

Old single-chunk format:

{
  "id": "...",
  "world": "world",
  "chunkX": 10,
  "chunkZ": 20
}

Migrates to multi-chunk format:

{
  "id": "...",
  "world": "world",
  "chunks": [{ "x": 10, "z": 20 }]
}

Migration is detected and run automatically on load.

Storage Health

storage/StorageHealth.java

Monitors storage system health:

public class StorageHealth {

    private final AtomicLong lastSaveTime = new AtomicLong();
    private final AtomicInteger failedSaves = new AtomicInteger();

    public void recordSave() {
        lastSaveTime.set(System.currentTimeMillis());
    }

    public void recordFailure() {
        failedSaves.incrementAndGet();
    }

    public boolean isHealthy() {
        // Check if saves are succeeding
        return failedSaves.get() < MAX_CONSECUTIVE_FAILURES;
    }
}

Implementing Alternative Storage

To add database support, implement the storage interfaces:

public class MySqlFactionStorage implements FactionStorage {

    private final DataSource dataSource;

    @Override
    public CompletableFuture<Void> saveFaction(Faction faction) {
        return CompletableFuture.runAsync(() -> {
            try (Connection conn = dataSource.getConnection()) {
                // SQL INSERT/UPDATE
            }
        });
    }

    @Override
    public CompletableFuture<Optional<Faction>> loadFaction(UUID factionId) {
        return CompletableFuture.supplyAsync(() -> {
            try (Connection conn = dataSource.getConnection()) {
                // SQL SELECT
            }
        });
    }
}

Then configure in HyperFactions:

// In HyperFactions.enable()
if (ConfigManager.get().isUsingDatabase()) {
    factionStorage = new MySqlFactionStorage(dataSource);
} else {
    factionStorage = new JsonFactionStorage(dataDir);
}

Backup Integration

Storage integrates with the backup system:

// In BackupManager
public void createBackup(BackupType type) {
    // Save all data first
    hyperFactions.saveAllData();

    // Copy data directories to backup (from data/ subdirectory)
    Path dataPath = dataDir.resolve("data");
    copyDirectory(dataPath.resolve("factions"), backupDir);
    copyDirectory(dataPath.resolve("players"), backupDir);
    copyDirectory(dataPath.resolve("chat"), backupDir);
    copyDirectory(dataPath.resolve("economy"), backupDir);
    copyFile(dataPath.resolve("zones.json"), backupDir);
    copyFile(dataPath.resolve("invites.json"), backupDir);
    copyFile(dataPath.resolve("join_requests.json"), backupDir);
    copyDirectory(dataDir.resolve("config"), backupDir);
}

Manual Data Editing

JSON files can be manually edited while the server is stopped:

  1. Stop the server
  2. Edit JSON files
  3. Start the server (data loads fresh)

Warning: Editing while the server is running may cause data loss due to auto-save overwriting changes.

Code Links

Class Path
FactionStorage storage/FactionStorage.java
PlayerStorage storage/PlayerStorage.java
ZoneStorage storage/ZoneStorage.java
JsonFactionStorage storage/json/JsonFactionStorage.java
JsonPlayerStorage storage/json/JsonPlayerStorage.java
JsonZoneStorage storage/json/JsonZoneStorage.java
Faction data/Faction.java
PlayerPower data/PlayerPower.java
Zone data/Zone.java
ChunkKey data/ChunkKey.java
ChatHistoryStorage storage/ChatHistoryStorage.java
JsonChatHistoryStorage storage/json/JsonChatHistoryStorage.java
JsonEconomyStorage storage/JsonEconomyStorage.java
StorageUtils storage/StorageUtils.java
DataV0ToV1Migration migration/migrations/data/DataV0ToV1Migration.java
BackupManager backup/BackupManager.java