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 incore/src/com/unciv/logic/files/PlatformSaverLoader.ktthat specifiessaveGame()andloadGame()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.MapSaverandGameSaverlogic – Platform-agnostic utilities incore/src/com/unciv/logic/files/MapSaver.ktthat serializeTileMapandGameInfoobjects 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.
- The
SaveGameScreen.ktgathers the currentGameInfoobject and serializes it to JSON. - The serialized string is passed to the injected loader with suggested filename and callbacks.
- The platform loader displays the native file picker, writes the UTF-8 string to the selected location, and invokes
onSavedoronError.
// 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.
LoadGameScreen.ktinvokes the loader, which opens the platform's file picker.- The loader reads the selected file as a UTF-8 string and returns it via the
onLoadedcallback. - The UI deserializes the JSON into a
GameInfoinstance 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 –
PlatformSaverLoaderdefines a single-string contract (saveGame/loadGame) whileDesktopSaverLoader,LinuxX11SaverLoader, andAndroidSaverLoaderhandle OS-native file dialogs. - Injection pattern –
UncivFiles.saverLoaderis set at launch byDesktopLauncher.ktorAndroidLauncher.ktbased on the detected platform. - Consistent serialization –
MapSaverandGameInfoJSON conversion run identically on all targets, with optional Gzip compression for map data. - Graceful cancellation – The
PlatformSaverLoader.Cancelledexception 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →