How Unciv's Save/Load System Works Across Desktop, Linux X11, and Android

Unciv stores game states and maps as JSON strings (optionally Gzip-compressed) and delegates file I/O to platform-specific PlatformSaverLoader implementations, allowing the same serialization logic to run natively on Windows, macOS, Linux X11, and Android.

Unciv, the open-source Civilization V clone maintained by yairm210, persists game data using a clean separation between platform-agnostic serialization and operating system-specific file operations. The Unciv save/load system relies on a single-string contract where the core game logic handles JSON conversion while injected platform loaders manage native file choosers and storage access.

Core Architecture

The system is built around three components that isolate platform differences from game logic:

  • PlatformSaverLoader – An interface defined in core/src/com/unciv/logic/files/PlatformSaverLoader.kt that specifies saveGame() and loadGame() methods accepting serialized strings and callback handlers.
  • UncivFiles.saverLoader – A globally accessible singleton that UI screens call when players initiate save or load actions. The concrete implementation is injected at launch by platform-specific entry points.
  • MapSaver and GameSaver logic – Platform-agnostic utilities in core/src/com/unciv/logic/files/MapSaver.kt that serialize TileMap and GameInfo objects to JSON, optionally compress with Gzip, and pass the resulting string to the active loader.

Platform-Specific Loader Selection

When Unciv starts, the launcher detects the operating environment and injects the appropriate loader into UncivFiles.saverLoader.

Desktop (Windows/macOS) – DesktopLauncher.kt instantiates DesktopSaverLoader, which uses the standard Swing file chooser.

Linux X11 – LinuxX11SaverLoader.kt provides isRequired() to detect X11 environments. When true, DesktopLauncher.kt injects LinuxX11SaverLoader instead of the default to avoid AWT blocking bugs on Linux.

Android – AndroidLauncher.kt constructs AndroidSaverLoader(activity), which wraps the Android Storage Access Framework (SAF).

// From DesktopLauncher.kt
UncivFiles.saverLoader = if (LinuxX11SaverLoader.isRequired())
    LinuxX11SaverLoader() else DesktopSaverLoader()

Saving Games

The save workflow begins in the UI layer and delegates through the abstraction to native storage.

  1. The SaveGameScreen.kt gathers the current GameInfo object and serializes it to JSON.
  2. The serialized string is passed to the injected loader with suggested filename and callbacks.
  3. The platform loader displays the native file picker, writes the UTF-8 string to the selected location, and invokes onSaved or onError.
// Inside SaveGameScreen.kt
val serializedGame = json().toJson(UncivGame.Current.gameInfo)
UncivFiles.saverLoader.saveGame(
    data = serializedGame,
    suggestedLocation = "MyGame.save",
    onSaved = { location -> 
        Gdx.app.postRunnable { toast("Saved to $location") } 
    },
    onError = { ex -> 
        if (ex !is PlatformSaverLoader.Cancelled) 
            toast("Error: ${ex.message}") 
    }
)

Loading Games

Loading mirrors the save process but returns the file content as a string that the game deserializes.

  1. LoadGameScreen.kt invokes the loader, which opens the platform's file picker.
  2. The loader reads the selected file as a UTF-8 string and returns it via the onLoaded callback.
  3. The UI deserializes the JSON into a GameInfo instance and replaces the current game state.
// Inside LoadGameScreen.kt
UncivFiles.saverLoader.loadGame(
    onLoaded = { data, location ->
        val loadedGame = json().fromJson(GameInfo::class.java, data)
        UncivGame.Current.gameInfo = loadedGame
        toast("Loaded $location")
    },
    onError = { ex -> 
        if (ex !is PlatformSaverLoader.Cancelled) 
            toast("Load failed: ${ex.message}") 
    }
)

Map-Specific Operations

Map files use the same serialization pipeline but often bypass the external file picker for internal storage. MapSaver.kt handles TileMap objects used by the map editor and map-selection UI.

When saving to internal storage, MapSaver.saveMap() writes to UncivGame.Current.files.getLocalFile("maps"). When explicit user selection is required, it uses the same PlatformSaverLoader contract. The utility respects the MapSaver.saveZipped boolean to apply Gzip compression before writing.

// Save a map from the map editor
val mapName = "MyCustomMap"
MapSaver.saveMap(mapName, editorScreen.currentMap)

Summary

  • Platform abstraction – PlatformSaverLoader defines a single-string contract (saveGame/loadGame) while DesktopSaverLoader, LinuxX11SaverLoader, and AndroidSaverLoader handle OS-native file dialogs.
  • Injection pattern – UncivFiles.saverLoader is set at launch by DesktopLauncher.kt or AndroidLauncher.kt based on the detected platform.
  • Consistent serialization – MapSaver and GameInfo JSON conversion run identically on all targets, with optional Gzip compression for map data.
  • Graceful cancellation – The PlatformSaverLoader.Cancelled exception allows UI screens to silently handle user-canceled dialogs without error messages.

Frequently Asked Questions

Why does Unciv use a separate loader for Linux X11?

The standard Java AWT file chooser blocks indefinitely on some Linux X11 window managers. LinuxX11SaverLoader works around this by using an X11-specific native dialog, while DesktopSaverLoader handles standard Windows and macOS Swing dialogs.

What file format does Unciv use for saves?

Unciv uses plain-text JSON for game saves and maps, optionally wrapped in Gzip compression for map files when MapSaver.saveZipped is enabled. The save files are human-readable when uncompressed and store complete GameInfo object states.

How does Unciv handle user cancellation during save/load operations?

Each PlatformSaverLoader implementation throws a PlatformSaverLoader.Cancelled exception when the user dismisses the file picker without selecting a location. UI screens in SaveGameScreen.kt and LoadGameScreen.kt catch this exception specifically to suppress error toasts, treating cancellation as a silent no-op rather than a failure.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →