Ver Fonte

persistent storage through sqlite

djeada há 2 meses atrás
pai
commit
9dd91cd17d

+ 5 - 2
CMakeLists.txt

@@ -16,14 +16,14 @@ endif()
 
 # ---- Qt ----
 # Try Qt6 first, fall back to Qt5 if not available
-find_package(Qt6 QUIET COMPONENTS Core Widgets OpenGL Quick Qml QuickControls2)
+find_package(Qt6 QUIET COMPONENTS Core Widgets OpenGL Quick Qml QuickControls2 Sql)
 if(Qt6_FOUND)
     message(STATUS "Using Qt6")
     set(QT_VERSION_MAJOR 6)
     find_package(OpenGL REQUIRED)
 else()
     message(STATUS "Qt6 not found, trying Qt5")
-    find_package(Qt5 REQUIRED COMPONENTS Core Widgets OpenGL Quick Qml QuickControls2)
+    find_package(Qt5 REQUIRED COMPONENTS Core Widgets OpenGL Quick Qml QuickControls2 Sql)
     message(STATUS "Using Qt5")
     set(QT_VERSION_MAJOR 5)
     find_package(OpenGL REQUIRED)
@@ -85,6 +85,8 @@ if(QT_VERSION_MAJOR EQUAL 6)
             ui/qml/HUDTop.qml
             ui/qml/HUDBottom.qml
             ui/qml/ProductionPanel.qml
+            ui/qml/SaveGamePanel.qml
+            ui/qml/LoadGamePanel.qml
             ui/qml/HUDVictory.qml
             ui/qml/BattleSummary.qml
             ui/qml/GameView.qml
@@ -110,6 +112,7 @@ target_link_libraries(standard_of_iron
         Qt${QT_VERSION_MAJOR}::OpenGL
         Qt${QT_VERSION_MAJOR}::Quick
         Qt${QT_VERSION_MAJOR}::Qml
+    Qt${QT_VERSION_MAJOR}::Sql
         ${OPENGL_LIBRARIES}
         engine_core
         render_gl

+ 411 - 16
app/core/game_engine.cpp

@@ -4,11 +4,14 @@
 #include "../controllers/command_controller.h"
 #include "../models/cursor_manager.h"
 #include "../models/hover_tracker.h"
+#include <QBuffer>
 #include <QCoreApplication>
 #include <QCursor>
 #include <QDebug>
+#include <QImage>
 #include <QOpenGLContext>
 #include <QQuickWindow>
+#include <QSize>
 #include <QVariant>
 #include <set>
 #include <unordered_map>
@@ -20,8 +23,10 @@
 #include "game/game_config.h"
 #include "game/map/level_loader.h"
 #include "game/map/map_catalog.h"
+#include "game/map/map_loader.h"
 #include "game/map/map_transformer.h"
 #include "game/map/skirmish_loader.h"
+#include "game/map/environment.h"
 #include "game/map/terrain_service.h"
 #include "game/map/visibility_service.h"
 #include "game/map/world_bootstrap.h"
@@ -68,6 +73,32 @@
 #include <cmath>
 #include <limits>
 
+namespace {
+
+QJsonArray vec3ToJsonArray(const QVector3D &vec) {
+  QJsonArray arr;
+  arr.append(vec.x());
+  arr.append(vec.y());
+  arr.append(vec.z());
+  return arr;
+}
+
+QVector3D jsonArrayToVec3(const QJsonValue &value,
+                          const QVector3D &fallback) {
+  if (!value.isArray()) {
+    return fallback;
+  }
+  const auto arr = value.toArray();
+  if (arr.size() < 3) {
+    return fallback;
+  }
+  return QVector3D(static_cast<float>(arr.at(0).toDouble(fallback.x())),
+                   static_cast<float>(arr.at(1).toDouble(fallback.y())),
+                   static_cast<float>(arr.at(2).toDouble(fallback.z())));
+}
+
+} // namespace
+
 GameEngine::GameEngine() {
 
   Game::Systems::NationRegistry::instance().initializeDefaults();
@@ -798,6 +829,7 @@ void GameEngine::startSkirmish(const QString &mapPath,
 
   clearError();
 
+  m_level.mapPath = mapPath;
   m_level.mapName = mapPath;
 
   m_runtime.victoryState = "";
@@ -909,17 +941,21 @@ void GameEngine::loadSave() {
     return;
   }
 
-  // Use a default save file path for now
-  QString saveFilePath = "savegame.json";
-  
-  bool success = m_saveLoadService->loadGame(*m_world, saveFilePath);
-  
+  const QString slotName = QStringLiteral("savegame");
+
+  bool success = m_saveLoadService->loadGameFromSlot(*m_world, slotName);
+
   if (success) {
-    qInfo() << "Game loaded successfully";
-    // Rebuild caches and update UI after loading
+    qInfo() << "Game loaded successfully from slot:" << slotName;
+    const QJsonObject metadata = m_saveLoadService->getLastMetadata();
+    applyEnvironmentFromMetadata(metadata);
+    rebuildRegistriesAfterLoad();
     rebuildEntityCache();
+    m_runtime.lastTroopCount = m_entityCache.playerTroopCount;
+    emit troopCountChanged();
     if (m_victoryService) {
-      m_victoryService->configure(Game::Map::VictoryConfig(), m_runtime.localOwnerId);
+      m_victoryService->configure(Game::Map::VictoryConfig(),
+                                  m_runtime.localOwnerId);
     }
     emit selectedUnitsChanged();
     emit ownerInfoChanged();
@@ -936,10 +972,20 @@ void GameEngine::saveGame(const QString &filename) {
     return;
   }
 
-  bool success = m_saveLoadService->saveGame(*m_world, filename);
-  
+  const QString slotName = filename;
+  const QString title = slotName;
+
+  QJsonObject metadata = buildSaveMetadata();
+  metadata["title"] = title;
+
+  const QByteArray screenshot = captureSaveScreenshot();
+
+  bool success = m_saveLoadService->saveGameToSlot(
+      *m_world, slotName, title, m_level.mapName, metadata, screenshot);
+
   if (success) {
-    qInfo() << "Game saved successfully to:" << filename;
+    qInfo() << "Game saved successfully to slot:" << slotName;
+    emit saveSlotsChanged();
   } else {
     QString error = m_saveLoadService->getLastError();
     qWarning() << "Failed to save game:" << error;
@@ -953,10 +999,17 @@ void GameEngine::saveGameToSlot(const QString &slotName) {
     return;
   }
 
-  bool success = m_saveLoadService->saveGameToSlot(*m_world, slotName, m_level.mapName);
-  
+  QJsonObject metadata = buildSaveMetadata();
+  metadata["title"] = slotName;
+
+  const QByteArray screenshot = captureSaveScreenshot();
+
+  bool success = m_saveLoadService->saveGameToSlot(
+      *m_world, slotName, slotName, m_level.mapName, metadata, screenshot);
+
   if (success) {
     qInfo() << "Game saved successfully to slot:" << slotName;
+    emit saveSlotsChanged();
   } else {
     QString error = m_saveLoadService->getLastError();
     qWarning() << "Failed to save game:" << error;
@@ -971,13 +1024,18 @@ void GameEngine::loadGameFromSlot(const QString &slotName) {
   }
 
   bool success = m_saveLoadService->loadGameFromSlot(*m_world, slotName);
-  
+
   if (success) {
     qInfo() << "Game loaded successfully from slot:" << slotName;
-    // Rebuild caches and update UI after loading
+    const QJsonObject metadata = m_saveLoadService->getLastMetadata();
+    applyEnvironmentFromMetadata(metadata);
+    rebuildRegistriesAfterLoad();
     rebuildEntityCache();
+    m_runtime.lastTroopCount = m_entityCache.playerTroopCount;
+    emit troopCountChanged();
     if (m_victoryService) {
-      m_victoryService->configure(Game::Map::VictoryConfig(), m_runtime.localOwnerId);
+      m_victoryService->configure(Game::Map::VictoryConfig(),
+                                  m_runtime.localOwnerId);
     }
     emit selectedUnitsChanged();
     emit ownerInfoChanged();
@@ -997,6 +1055,10 @@ QVariantList GameEngine::getSaveSlots() const {
   return m_saveLoadService->getSaveSlots();
 }
 
+void GameEngine::refreshSaveSlots() {
+  emit saveSlotsChanged();
+}
+
 bool GameEngine::deleteSaveSlot(const QString &slotName) {
   if (!m_saveLoadService) {
     qWarning() << "Cannot delete save slot: service not initialized";
@@ -1009,6 +1071,8 @@ bool GameEngine::deleteSaveSlot(const QString &slotName) {
     QString error = m_saveLoadService->getLastError();
     qWarning() << "Failed to delete save slot:" << error;
     setError(error);
+  } else {
+    emit saveSlotsChanged();
   }
   
   return success;
@@ -1154,6 +1218,337 @@ void GameEngine::rebuildEntityCache() {
   }
 }
 
+void GameEngine::rebuildRegistriesAfterLoad() {
+  if (!m_world)
+    return;
+
+  auto &ownerRegistry = Game::Systems::OwnerRegistry::instance();
+  m_runtime.localOwnerId = ownerRegistry.getLocalPlayerId();
+
+  Game::Systems::TroopCountRegistry::instance().rebuildFromWorld(*m_world);
+
+  auto &statsRegistry = Game::Systems::GlobalStatsRegistry::instance();
+  statsRegistry.rebuildFromWorld(*m_world);
+
+  const auto &allOwners = ownerRegistry.getAllOwners();
+  for (const auto &owner : allOwners) {
+    if (owner.type == Game::Systems::OwnerType::Player ||
+        owner.type == Game::Systems::OwnerType::AI) {
+      statsRegistry.markGameStart(owner.ownerId);
+    }
+  }
+
+  rebuildBuildingCollisions();
+
+  m_level.playerUnitId = 0;
+  auto units = m_world->getEntitiesWith<Engine::Core::UnitComponent>();
+  for (auto *entity : units) {
+    auto *unit = entity->getComponent<Engine::Core::UnitComponent>();
+    if (!unit)
+      continue;
+    if (unit->ownerId == m_runtime.localOwnerId) {
+      m_level.playerUnitId = entity->getId();
+      break;
+    }
+  }
+
+  if (m_selectedPlayerId != m_runtime.localOwnerId) {
+    m_selectedPlayerId = m_runtime.localOwnerId;
+    emit selectedPlayerIdChanged();
+  }
+}
+
+void GameEngine::rebuildBuildingCollisions() {
+  auto &registry = Game::Systems::BuildingCollisionRegistry::instance();
+  registry.clear();
+  if (!m_world)
+    return;
+
+  auto buildings = m_world->getEntitiesWith<Engine::Core::BuildingComponent>();
+  for (auto *entity : buildings) {
+    auto *transform = entity->getComponent<Engine::Core::TransformComponent>();
+    auto *unit = entity->getComponent<Engine::Core::UnitComponent>();
+    if (!transform || !unit)
+      continue;
+
+    registry.registerBuilding(entity->getId(), unit->unitType,
+                              transform->position.x, transform->position.z,
+                              unit->ownerId);
+  }
+}
+
+QJsonObject GameEngine::buildSaveMetadata() const {
+  QJsonObject metadata;
+  metadata["mapPath"] = m_level.mapPath;
+  metadata["mapName"] = m_level.mapName;
+  metadata["maxTroopsPerPlayer"] = m_level.maxTroopsPerPlayer;
+  metadata["localOwnerId"] = m_runtime.localOwnerId;
+  metadata["playerUnitId"] = static_cast<qint64>(m_level.playerUnitId);
+
+  metadata["gameMaxTroopsPerPlayer"] =
+      Game::GameConfig::instance().getMaxTroopsPerPlayer();
+
+  const auto &terrainService = Game::Map::TerrainService::instance();
+  if (const auto *heightMap = terrainService.getHeightMap()) {
+    metadata["gridWidth"] = heightMap->getWidth();
+    metadata["gridHeight"] = heightMap->getHeight();
+    metadata["tileSize"] = heightMap->getTileSize();
+  }
+
+  if (m_camera) {
+    QJsonObject cameraObj;
+    cameraObj["position"] = vec3ToJsonArray(m_camera->getPosition());
+    cameraObj["target"] = vec3ToJsonArray(m_camera->getTarget());
+    cameraObj["distance"] = m_camera->getDistance();
+    cameraObj["pitchDeg"] = m_camera->getPitchDeg();
+    cameraObj["fov"] = m_camera->getFOV();
+    cameraObj["near"] = m_camera->getNear();
+    cameraObj["far"] = m_camera->getFar();
+    metadata["camera"] = cameraObj;
+  }
+
+  QJsonObject runtimeObj;
+  runtimeObj["paused"] = m_runtime.paused;
+  runtimeObj["timeScale"] = m_runtime.timeScale;
+  runtimeObj["victoryState"] = m_runtime.victoryState;
+  runtimeObj["cursorMode"] = m_runtime.cursorMode;
+  runtimeObj["selectedPlayerId"] = m_selectedPlayerId;
+  runtimeObj["followSelection"] = m_followSelectionEnabled;
+  metadata["runtime"] = runtimeObj;
+
+  return metadata;
+}
+
+void GameEngine::applyEnvironmentFromMetadata(const QJsonObject &metadata) {
+  if (!m_world)
+    return;
+
+  if (metadata.contains("localOwnerId")) {
+    m_runtime.localOwnerId = metadata.value("localOwnerId")
+                                 .toInt(m_runtime.localOwnerId);
+  }
+
+  const QString mapPath = metadata.value("mapPath").toString();
+  if (!mapPath.isEmpty()) {
+    m_level.mapPath = mapPath;
+  }
+
+  if (metadata.contains("mapName")) {
+    m_level.mapName = metadata.value("mapName").toString(m_level.mapName);
+  }
+
+  if (metadata.contains("playerUnitId")) {
+    m_level.playerUnitId = static_cast<Engine::Core::EntityID>(
+        metadata.value("playerUnitId").toVariant().toULongLong());
+  }
+
+  int maxTroops = metadata.value("maxTroopsPerPlayer")
+                      .toInt(m_level.maxTroopsPerPlayer);
+  if (maxTroops <= 0) {
+    maxTroops = Game::GameConfig::instance().getMaxTroopsPerPlayer();
+  }
+  m_level.maxTroopsPerPlayer = maxTroops;
+  Game::GameConfig::instance().setMaxTroopsPerPlayer(maxTroops);
+
+  const auto fallbackGridWidth = metadata.value("gridWidth").toInt(50);
+  const auto fallbackGridHeight = metadata.value("gridHeight").toInt(50);
+  const float fallbackTileSize =
+      static_cast<float>(metadata.value("tileSize").toDouble(1.0));
+
+  Game::Map::MapDefinition def;
+  QString mapError;
+  bool loadedDefinition = false;
+  if (!mapPath.isEmpty()) {
+    loadedDefinition =
+        Game::Map::MapLoader::loadFromJsonFile(mapPath, def, &mapError);
+    if (!loadedDefinition) {
+      qWarning() << "GameEngine: Failed to load map definition from" << mapPath
+                 << "during save load:" << mapError;
+    }
+  }
+
+  auto &terrainService = Game::Map::TerrainService::instance();
+
+  if (loadedDefinition) {
+    terrainService.initialize(def);
+
+    if (!def.name.isEmpty()) {
+      m_level.mapName = def.name;
+    }
+
+    m_level.camFov = def.camera.fovY;
+    m_level.camNear = def.camera.nearPlane;
+    m_level.camFar = def.camera.farPlane;
+
+    if (m_renderer && m_camera) {
+      Game::Map::Environment::apply(def, *m_renderer, *m_camera);
+    }
+
+    if (m_ground) {
+      m_ground->configure(def.grid.tileSize, def.grid.width, def.grid.height);
+      if (terrainService.isInitialized()) {
+        m_ground->setBiome(terrainService.biomeSettings());
+      }
+    }
+
+    if (auto *heightMap = terrainService.getHeightMap()) {
+      if (m_terrain) {
+        m_terrain->configure(*heightMap, terrainService.biomeSettings());
+      }
+      if (m_biome) {
+        m_biome->configure(*heightMap, terrainService.biomeSettings());
+        m_biome->refreshGrass();
+      }
+      if (m_stone) {
+        m_stone->configure(*heightMap, terrainService.biomeSettings());
+      }
+    }
+
+    Game::Systems::CommandService::initialize(def.grid.width,
+                                              def.grid.height);
+
+    auto &visibilityService = Game::Map::VisibilityService::instance();
+    visibilityService.initialize(def.grid.width, def.grid.height,
+                                 def.grid.tileSize);
+    visibilityService.computeImmediate(*m_world, m_runtime.localOwnerId);
+
+    if (m_fog && visibilityService.isInitialized()) {
+      m_fog->updateMask(visibilityService.getWidth(),
+                        visibilityService.getHeight(),
+                        visibilityService.getTileSize(),
+                        visibilityService.snapshotCells());
+    }
+
+    m_runtime.visibilityVersion = visibilityService.version();
+    m_runtime.visibilityUpdateAccumulator = 0.0f;
+  } else {
+    if (m_renderer && m_camera) {
+      Game::Map::Environment::applyDefault(*m_renderer, *m_camera);
+    }
+
+    Game::Map::MapDefinition fallbackDef;
+    fallbackDef.grid.width = fallbackGridWidth;
+    fallbackDef.grid.height = fallbackGridHeight;
+    fallbackDef.grid.tileSize = fallbackTileSize;
+    fallbackDef.maxTroopsPerPlayer = maxTroops;
+    terrainService.initialize(fallbackDef);
+
+    if (m_ground) {
+      m_ground->configure(fallbackTileSize, fallbackGridWidth,
+                          fallbackGridHeight);
+    }
+
+    Game::Systems::CommandService::initialize(fallbackGridWidth,
+                                              fallbackGridHeight);
+
+    auto &visibilityService = Game::Map::VisibilityService::instance();
+    visibilityService.initialize(fallbackGridWidth, fallbackGridHeight,
+                                 fallbackTileSize);
+    visibilityService.computeImmediate(*m_world, m_runtime.localOwnerId);
+    if (m_fog && visibilityService.isInitialized()) {
+      m_fog->updateMask(visibilityService.getWidth(),
+                        visibilityService.getHeight(),
+                        visibilityService.getTileSize(),
+                        visibilityService.snapshotCells());
+    }
+    m_runtime.visibilityVersion = visibilityService.version();
+    m_runtime.visibilityUpdateAccumulator = 0.0f;
+  }
+
+  if (metadata.contains("camera") && m_camera) {
+    const auto cameraObj = metadata.value("camera").toObject();
+    const QVector3D position =
+        jsonArrayToVec3(cameraObj.value("position"), m_camera->getPosition());
+    const QVector3D target =
+        jsonArrayToVec3(cameraObj.value("target"), m_camera->getTarget());
+    m_camera->lookAt(position, target, QVector3D(0.0f, 1.0f, 0.0f));
+
+    const float nearPlane = static_cast<float>(
+        cameraObj.value("near").toDouble(m_camera->getNear()));
+    const float farPlane = static_cast<float>(
+        cameraObj.value("far").toDouble(m_camera->getFar()));
+    const float fov = static_cast<float>(
+        cameraObj.value("fov").toDouble(m_camera->getFOV()));
+
+    float aspect = m_camera->getAspect();
+    if (m_viewport.height > 0) {
+      aspect = float(m_viewport.width) /
+               float(std::max(1, m_viewport.height));
+    }
+    m_camera->setPerspective(fov, aspect, nearPlane, farPlane);
+  }
+
+  if (metadata.contains("runtime")) {
+    const auto runtimeObj = metadata.value("runtime").toObject();
+
+    if (runtimeObj.contains("paused")) {
+      setPaused(runtimeObj.value("paused").toBool(m_runtime.paused));
+    }
+
+    if (runtimeObj.contains("timeScale")) {
+      setGameSpeed(static_cast<float>(
+          runtimeObj.value("timeScale").toDouble(m_runtime.timeScale)));
+    }
+
+    const QString victory =
+        runtimeObj.value("victoryState").toString(m_runtime.victoryState);
+    if (victory != m_runtime.victoryState) {
+      m_runtime.victoryState = victory;
+      emit victoryStateChanged();
+    }
+
+    const QString cursor =
+        runtimeObj.value("cursorMode").toString(m_runtime.cursorMode);
+    if (!cursor.isEmpty()) {
+      setCursorMode(cursor);
+    }
+
+    const int selectedId =
+        runtimeObj.value("selectedPlayerId").toInt(m_selectedPlayerId);
+    if (selectedId != m_selectedPlayerId) {
+      m_selectedPlayerId = selectedId;
+      emit selectedPlayerIdChanged();
+    }
+
+    const bool follow =
+        runtimeObj.value("followSelection").toBool(m_followSelectionEnabled);
+    if (follow != m_followSelectionEnabled) {
+      m_followSelectionEnabled = follow;
+      if (m_camera && m_cameraService && m_world) {
+        m_cameraService->followSelection(*m_camera, *m_world,
+                                         m_followSelectionEnabled);
+      }
+    }
+  }
+}
+
+QByteArray GameEngine::captureSaveScreenshot() const {
+  if (!m_window) {
+    return {};
+  }
+
+  QImage image = m_window->grabWindow();
+  if (image.isNull()) {
+    return {};
+  }
+
+  const QSize targetSize(320, 180);
+  QImage scaled = image.scaled(targetSize, Qt::KeepAspectRatio,
+                               Qt::SmoothTransformation);
+
+  QByteArray buffer;
+  QBuffer qBuffer(&buffer);
+  if (!qBuffer.open(QIODevice::WriteOnly)) {
+    return {};
+  }
+
+  if (!scaled.save(&qBuffer, "PNG")) {
+    return {};
+  }
+
+  return buffer;
+}
+
 bool GameEngine::hasPatrolPreviewWaypoint() const {
   return m_commandController && m_commandController->hasPatrolFirstWaypoint();
 }

+ 10 - 0
app/core/game_engine.h

@@ -6,6 +6,8 @@
 #include "../utils/movement_utils.h"
 #include "../utils/selection_utils.h"
 #include "game/core/event_manager.h"
+#include <QJsonObject>
+#include <QList>
 #include <QMatrix4x4>
 #include <QObject>
 #include <QPointF>
@@ -166,6 +168,7 @@ public:
   Q_INVOKABLE void saveGameToSlot(const QString &slotName);
   Q_INVOKABLE void loadGameFromSlot(const QString &slotName);
   Q_INVOKABLE QVariantList getSaveSlots() const;
+  Q_INVOKABLE void refreshSaveSlots();
   Q_INVOKABLE bool deleteSaveSlot(const QString &slotName);
   Q_INVOKABLE void exitGame();
   Q_INVOKABLE QVariantList getOwnerInfo() const;
@@ -219,6 +222,7 @@ private:
     int height = 0;
   };
   struct LevelState {
+    QString mapPath;
     QString mapName;
     Engine::Core::EntityID playerUnitId = 0;
     float camFov = 45.0f;
@@ -235,6 +239,11 @@ private:
   void onUnitSpawned(const Engine::Core::UnitSpawnedEvent &event);
   void onUnitDied(const Engine::Core::UnitDiedEvent &event);
   void rebuildEntityCache();
+  void rebuildRegistriesAfterLoad();
+  void rebuildBuildingCollisions();
+  QJsonObject buildSaveMetadata() const;
+  void applyEnvironmentFromMetadata(const QJsonObject &metadata);
+  QByteArray captureSaveScreenshot() const;
   void updateCursor(Qt::CursorShape newCursor);
   void setError(const QString &errorMessage);
 
@@ -284,4 +293,5 @@ signals:
   void selectedPlayerIdChanged();
   void lastErrorChanged();
   void mapsLoadingChanged();
+  void saveSlotsChanged();
 };

+ 2 - 1
game/CMakeLists.txt

@@ -45,6 +45,7 @@ add_library(game_systems STATIC
     systems/global_stats_registry.cpp
     systems/victory_service.cpp
     systems/save_load_service.cpp
+    systems/save_storage.cpp
     systems/nation_registry.cpp
     systems/formation_system.cpp
     map/map_loader.cpp
@@ -65,4 +66,4 @@ add_library(game_systems STATIC
 )
 
 target_include_directories(game_systems PUBLIC .)
-target_link_libraries(game_systems PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui engine_core render_gl)
+target_link_libraries(game_systems PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Sql engine_core render_gl)

+ 331 - 22
game/core/serialization.cpp

@@ -6,15 +6,59 @@
 #include <QFile>
 #include <QJsonArray>
 #include <QJsonObject>
+#include <algorithm>
+
+#include "../systems/owner_registry.h"
 
 namespace Engine::Core {
 
+namespace {
+
+QString combatModeToString(AttackComponent::CombatMode mode) {
+  switch (mode) {
+  case AttackComponent::CombatMode::Melee:
+    return "melee";
+  case AttackComponent::CombatMode::Ranged:
+    return "ranged";
+  case AttackComponent::CombatMode::Auto:
+  default:
+    return "auto";
+  }
+}
+
+AttackComponent::CombatMode combatModeFromString(const QString &value) {
+  if (value == "melee") {
+    return AttackComponent::CombatMode::Melee;
+  }
+  if (value == "ranged") {
+    return AttackComponent::CombatMode::Ranged;
+  }
+  return AttackComponent::CombatMode::Auto;
+}
+
+QJsonArray serializeColor(const float color[3]) {
+  QJsonArray array;
+  array.append(color[0]);
+  array.append(color[1]);
+  array.append(color[2]);
+  return array;
+}
+
+void deserializeColor(const QJsonArray &array, float color[3]) {
+  if (array.size() >= 3) {
+    color[0] = static_cast<float>(array.at(0).toDouble());
+    color[1] = static_cast<float>(array.at(1).toDouble());
+    color[2] = static_cast<float>(array.at(2).toDouble());
+  }
+}
+
+} // namespace
+
 QJsonObject Serialization::serializeEntity(const Entity *entity) {
   QJsonObject entityObj;
   entityObj["id"] = static_cast<qint64>(entity->getId());
 
-  if (auto transform =
-          const_cast<Entity *>(entity)->getComponent<TransformComponent>()) {
+  if (const auto *transform = entity->getComponent<TransformComponent>()) {
     QJsonObject transformObj;
     transformObj["posX"] = transform->position.x;
     transformObj["posY"] = transform->position.y;
@@ -25,60 +69,309 @@ QJsonObject Serialization::serializeEntity(const Entity *entity) {
     transformObj["scaleX"] = transform->scale.x;
     transformObj["scaleY"] = transform->scale.y;
     transformObj["scaleZ"] = transform->scale.z;
+    transformObj["hasDesiredYaw"] = transform->hasDesiredYaw;
+    transformObj["desiredYaw"] = transform->desiredYaw;
     entityObj["transform"] = transformObj;
   }
 
-  if (auto unit = const_cast<Entity *>(entity)->getComponent<UnitComponent>()) {
+  if (const auto *renderable = entity->getComponent<RenderableComponent>()) {
+    QJsonObject renderableObj;
+    renderableObj["meshPath"] = QString::fromStdString(renderable->meshPath);
+    renderableObj["texturePath"] =
+        QString::fromStdString(renderable->texturePath);
+    renderableObj["visible"] = renderable->visible;
+    renderableObj["mesh"] = static_cast<int>(renderable->mesh);
+    renderableObj["color"] = serializeColor(renderable->color);
+    entityObj["renderable"] = renderableObj;
+  }
+
+  if (const auto *unit = entity->getComponent<UnitComponent>()) {
     QJsonObject unitObj;
     unitObj["health"] = unit->health;
     unitObj["maxHealth"] = unit->maxHealth;
     unitObj["speed"] = unit->speed;
+    unitObj["visionRange"] = unit->visionRange;
     unitObj["unitType"] = QString::fromStdString(unit->unitType);
     unitObj["ownerId"] = unit->ownerId;
     entityObj["unit"] = unitObj;
   }
 
+  if (const auto *movement = entity->getComponent<MovementComponent>()) {
+    QJsonObject movementObj;
+    movementObj["hasTarget"] = movement->hasTarget;
+    movementObj["targetX"] = movement->targetX;
+    movementObj["targetY"] = movement->targetY;
+    movementObj["goalX"] = movement->goalX;
+    movementObj["goalY"] = movement->goalY;
+    movementObj["vx"] = movement->vx;
+    movementObj["vz"] = movement->vz;
+    movementObj["pathPending"] = movement->pathPending;
+    movementObj["pendingRequestId"] =
+        static_cast<qint64>(movement->pendingRequestId);
+    movementObj["repathCooldown"] = movement->repathCooldown;
+    movementObj["lastGoalX"] = movement->lastGoalX;
+    movementObj["lastGoalY"] = movement->lastGoalY;
+    movementObj["timeSinceLastPathRequest"] =
+        movement->timeSinceLastPathRequest;
+
+    QJsonArray pathArray;
+    for (const auto &waypoint : movement->path) {
+      QJsonObject waypointObj;
+      waypointObj["x"] = waypoint.first;
+      waypointObj["y"] = waypoint.second;
+      pathArray.append(waypointObj);
+    }
+    movementObj["path"] = pathArray;
+    entityObj["movement"] = movementObj;
+  }
+
+  if (const auto *attack = entity->getComponent<AttackComponent>()) {
+    QJsonObject attackObj;
+    attackObj["range"] = attack->range;
+    attackObj["damage"] = attack->damage;
+    attackObj["cooldown"] = attack->cooldown;
+    attackObj["timeSinceLast"] = attack->timeSinceLast;
+    attackObj["meleeRange"] = attack->meleeRange;
+    attackObj["meleeDamage"] = attack->meleeDamage;
+    attackObj["meleeCooldown"] = attack->meleeCooldown;
+    attackObj["preferredMode"] = combatModeToString(attack->preferredMode);
+    attackObj["currentMode"] = combatModeToString(attack->currentMode);
+    attackObj["canMelee"] = attack->canMelee;
+    attackObj["canRanged"] = attack->canRanged;
+    attackObj["maxHeightDifference"] = attack->maxHeightDifference;
+    attackObj["inMeleeLock"] = attack->inMeleeLock;
+    attackObj["meleeLockTargetId"] =
+        static_cast<qint64>(attack->meleeLockTargetId);
+    entityObj["attack"] = attackObj;
+  }
+
+  if (const auto *attackTarget = entity->getComponent<AttackTargetComponent>()) {
+    QJsonObject attackTargetObj;
+    attackTargetObj["targetId"] =
+        static_cast<qint64>(attackTarget->targetId);
+    attackTargetObj["shouldChase"] = attackTarget->shouldChase;
+    entityObj["attackTarget"] = attackTargetObj;
+  }
+
+  if (const auto *patrol = entity->getComponent<PatrolComponent>()) {
+    QJsonObject patrolObj;
+    patrolObj["currentWaypoint"] = static_cast<int>(patrol->currentWaypoint);
+    patrolObj["patrolling"] = patrol->patrolling;
+
+    QJsonArray waypointsArray;
+    for (const auto &waypoint : patrol->waypoints) {
+      QJsonObject waypointObj;
+      waypointObj["x"] = waypoint.first;
+      waypointObj["y"] = waypoint.second;
+      waypointsArray.append(waypointObj);
+    }
+    patrolObj["waypoints"] = waypointsArray;
+    entityObj["patrol"] = patrolObj;
+  }
+
+  if (entity->getComponent<BuildingComponent>()) {
+    entityObj["building"] = true;
+  }
+
+  if (const auto *production = entity->getComponent<ProductionComponent>()) {
+    QJsonObject productionObj;
+    productionObj["inProgress"] = production->inProgress;
+    productionObj["buildTime"] = production->buildTime;
+    productionObj["timeRemaining"] = production->timeRemaining;
+    productionObj["producedCount"] = production->producedCount;
+    productionObj["maxUnits"] = production->maxUnits;
+    productionObj["productType"] =
+        QString::fromStdString(production->productType);
+    productionObj["rallyX"] = production->rallyX;
+    productionObj["rallyZ"] = production->rallyZ;
+    productionObj["rallySet"] = production->rallySet;
+    productionObj["villagerCost"] = production->villagerCost;
+
+    QJsonArray queueArray;
+    for (const auto &queued : production->productionQueue) {
+      queueArray.append(QString::fromStdString(queued));
+    }
+    productionObj["queue"] = queueArray;
+    entityObj["production"] = productionObj;
+  }
+
+  if (entity->getComponent<AIControlledComponent>()) {
+    entityObj["aiControlled"] = true;
+  }
+
   return entityObj;
 }
 
 void Serialization::deserializeEntity(Entity *entity, const QJsonObject &json) {
   if (json.contains("transform")) {
-    auto transformObj = json["transform"].toObject();
+    const auto transformObj = json["transform"].toObject();
     auto transform = entity->addComponent<TransformComponent>();
-    transform->position.x = transformObj["posX"].toDouble();
-    transform->position.y = transformObj["posY"].toDouble();
-    transform->position.z = transformObj["posZ"].toDouble();
-    transform->rotation.x = transformObj["rotX"].toDouble();
-    transform->rotation.y = transformObj["rotY"].toDouble();
-    transform->rotation.z = transformObj["rotZ"].toDouble();
-    transform->scale.x = transformObj["scaleX"].toDouble();
-    transform->scale.y = transformObj["scaleY"].toDouble();
-    transform->scale.z = transformObj["scaleZ"].toDouble();
+    transform->position.x = static_cast<float>(transformObj["posX"].toDouble());
+    transform->position.y = static_cast<float>(transformObj["posY"].toDouble());
+    transform->position.z = static_cast<float>(transformObj["posZ"].toDouble());
+    transform->rotation.x = static_cast<float>(transformObj["rotX"].toDouble());
+    transform->rotation.y = static_cast<float>(transformObj["rotY"].toDouble());
+    transform->rotation.z = static_cast<float>(transformObj["rotZ"].toDouble());
+    transform->scale.x = static_cast<float>(transformObj["scaleX"].toDouble());
+    transform->scale.y = static_cast<float>(transformObj["scaleY"].toDouble());
+    transform->scale.z = static_cast<float>(transformObj["scaleZ"].toDouble());
+    transform->hasDesiredYaw = transformObj["hasDesiredYaw"].toBool(false);
+    transform->desiredYaw = static_cast<float>(transformObj["desiredYaw"].toDouble());
+  }
+
+  if (json.contains("renderable")) {
+    const auto renderableObj = json["renderable"].toObject();
+    auto renderable = entity->addComponent<RenderableComponent>("", "");
+    renderable->meshPath = renderableObj["meshPath"].toString().toStdString();
+    renderable->texturePath =
+        renderableObj["texturePath"].toString().toStdString();
+    renderable->visible = renderableObj["visible"].toBool(true);
+    renderable->mesh = static_cast<RenderableComponent::MeshKind>(
+        renderableObj["mesh"].toInt(static_cast<int>(RenderableComponent::MeshKind::Cube)));
+    if (renderableObj.contains("color")) {
+      deserializeColor(renderableObj["color"].toArray(), renderable->color);
+    }
   }
 
   if (json.contains("unit")) {
-    auto unitObj = json["unit"].toObject();
+    const auto unitObj = json["unit"].toObject();
     auto unit = entity->addComponent<UnitComponent>();
-    unit->health = unitObj["health"].toInt();
-    unit->maxHealth = unitObj["maxHealth"].toInt();
-    unit->speed = unitObj["speed"].toDouble();
+    unit->health = unitObj["health"].toInt(100);
+    unit->maxHealth = unitObj["maxHealth"].toInt(100);
+    unit->speed = static_cast<float>(unitObj["speed"].toDouble());
+    unit->visionRange = static_cast<float>(unitObj["visionRange"].toDouble(12.0));
     unit->unitType = unitObj["unitType"].toString().toStdString();
     unit->ownerId = unitObj["ownerId"].toInt(0);
   }
+
+  if (json.contains("movement")) {
+    const auto movementObj = json["movement"].toObject();
+    auto movement = entity->addComponent<MovementComponent>();
+    movement->hasTarget = movementObj["hasTarget"].toBool(false);
+    movement->targetX = static_cast<float>(movementObj["targetX"].toDouble());
+    movement->targetY = static_cast<float>(movementObj["targetY"].toDouble());
+    movement->goalX = static_cast<float>(movementObj["goalX"].toDouble());
+    movement->goalY = static_cast<float>(movementObj["goalY"].toDouble());
+    movement->vx = static_cast<float>(movementObj["vx"].toDouble());
+    movement->vz = static_cast<float>(movementObj["vz"].toDouble());
+    movement->pathPending = movementObj["pathPending"].toBool(false);
+    movement->pendingRequestId = static_cast<std::uint64_t>(
+        movementObj["pendingRequestId"].toVariant().toULongLong());
+    movement->repathCooldown = static_cast<float>(movementObj["repathCooldown"].toDouble());
+    movement->lastGoalX = static_cast<float>(movementObj["lastGoalX"].toDouble());
+    movement->lastGoalY = static_cast<float>(movementObj["lastGoalY"].toDouble());
+    movement->timeSinceLastPathRequest = static_cast<float>(
+        movementObj["timeSinceLastPathRequest"].toDouble());
+
+    movement->path.clear();
+    const auto pathArray = movementObj["path"].toArray();
+    movement->path.reserve(pathArray.size());
+    for (const auto &value : pathArray) {
+      const auto waypointObj = value.toObject();
+      movement->path.emplace_back(
+          static_cast<float>(waypointObj["x"].toDouble()),
+          static_cast<float>(waypointObj["y"].toDouble()));
+    }
+  }
+
+  if (json.contains("attack")) {
+    const auto attackObj = json["attack"].toObject();
+    auto attack = entity->addComponent<AttackComponent>();
+    attack->range = static_cast<float>(attackObj["range"].toDouble());
+    attack->damage = attackObj["damage"].toInt(0);
+    attack->cooldown = static_cast<float>(attackObj["cooldown"].toDouble());
+    attack->timeSinceLast = static_cast<float>(attackObj["timeSinceLast"].toDouble());
+    attack->meleeRange = static_cast<float>(attackObj["meleeRange"].toDouble(1.5));
+    attack->meleeDamage = attackObj["meleeDamage"].toInt(0);
+    attack->meleeCooldown = static_cast<float>(attackObj["meleeCooldown"].toDouble());
+    attack->preferredMode =
+        combatModeFromString(attackObj["preferredMode"].toString());
+    attack->currentMode =
+        combatModeFromString(attackObj["currentMode"].toString());
+    attack->canMelee = attackObj["canMelee"].toBool(true);
+    attack->canRanged = attackObj["canRanged"].toBool(false);
+    attack->maxHeightDifference =
+        static_cast<float>(attackObj["maxHeightDifference"].toDouble(2.0));
+    attack->inMeleeLock = attackObj["inMeleeLock"].toBool(false);
+    attack->meleeLockTargetId = static_cast<EntityID>(
+        attackObj["meleeLockTargetId"].toVariant().toULongLong());
+  }
+
+  if (json.contains("attackTarget")) {
+    const auto attackTargetObj = json["attackTarget"].toObject();
+    auto attackTarget = entity->addComponent<AttackTargetComponent>();
+    attackTarget->targetId = static_cast<EntityID>(
+        attackTargetObj["targetId"].toVariant().toULongLong());
+    attackTarget->shouldChase = attackTargetObj["shouldChase"].toBool(false);
+  }
+
+  if (json.contains("patrol")) {
+    const auto patrolObj = json["patrol"].toObject();
+    auto patrol = entity->addComponent<PatrolComponent>();
+    patrol->currentWaypoint = static_cast<size_t>(
+        std::max(0, patrolObj["currentWaypoint"].toInt()));
+    patrol->patrolling = patrolObj["patrolling"].toBool(false);
+
+    patrol->waypoints.clear();
+    const auto waypointsArray = patrolObj["waypoints"].toArray();
+    patrol->waypoints.reserve(waypointsArray.size());
+    for (const auto &value : waypointsArray) {
+      const auto waypointObj = value.toObject();
+      patrol->waypoints.emplace_back(
+          static_cast<float>(waypointObj["x"].toDouble()),
+          static_cast<float>(waypointObj["y"].toDouble()));
+    }
+  }
+
+  if (json.contains("building") && json["building"].toBool()) {
+    entity->addComponent<BuildingComponent>();
+  }
+
+  if (json.contains("production")) {
+    const auto productionObj = json["production"].toObject();
+    auto production = entity->addComponent<ProductionComponent>();
+    production->inProgress = productionObj["inProgress"].toBool(false);
+    production->buildTime = static_cast<float>(productionObj["buildTime"].toDouble());
+    production->timeRemaining = static_cast<float>(productionObj["timeRemaining"].toDouble());
+    production->producedCount = productionObj["producedCount"].toInt(0);
+    production->maxUnits = productionObj["maxUnits"].toInt(0);
+    production->productType =
+        productionObj["productType"].toString().toStdString();
+    production->rallyX = static_cast<float>(productionObj["rallyX"].toDouble());
+    production->rallyZ = static_cast<float>(productionObj["rallyZ"].toDouble());
+    production->rallySet = productionObj["rallySet"].toBool(false);
+    production->villagerCost = productionObj["villagerCost"].toInt(1);
+
+    production->productionQueue.clear();
+    const auto queueArray = productionObj["queue"].toArray();
+    production->productionQueue.reserve(queueArray.size());
+    for (const auto &value : queueArray) {
+      production->productionQueue.push_back(value.toString().toStdString());
+    }
+  }
+
+  if (json.contains("aiControlled") && json["aiControlled"].toBool()) {
+    entity->addComponent<AIControlledComponent>();
+  }
 }
 
 QJsonDocument Serialization::serializeWorld(const World *world) {
   QJsonObject worldObj;
   QJsonArray entitiesArray;
-  
-  // Iterate over all entities and serialize them
+
   const auto &entities = world->getEntities();
   for (const auto &[id, entity] : entities) {
     QJsonObject entityObj = serializeEntity(entity.get());
     entitiesArray.append(entityObj);
   }
-  
+
   worldObj["entities"] = entitiesArray;
+  worldObj["nextEntityId"] =
+      static_cast<qint64>(world->getNextEntityId());
+  worldObj["schemaVersion"] = 1;
+  worldObj["ownerRegistry"] =
+      Game::Systems::OwnerRegistry::instance().toJson();
+
   return QJsonDocument(worldObj);
 }
 
@@ -87,8 +380,24 @@ void Serialization::deserializeWorld(World *world, const QJsonDocument &doc) {
   auto entitiesArray = worldObj["entities"].toArray();
   for (const auto &value : entitiesArray) {
     auto entityObj = value.toObject();
-    auto entity = world->createEntity();
-    deserializeEntity(entity, entityObj);
+    const auto entityId = static_cast<EntityID>(
+        entityObj["id"].toVariant().toULongLong());
+    auto entity = entityId == NULL_ENTITY ? world->createEntity()
+                                          : world->createEntityWithId(entityId);
+    if (entity) {
+      deserializeEntity(entity, entityObj);
+    }
+  }
+
+  if (worldObj.contains("nextEntityId")) {
+    const auto nextId = static_cast<EntityID>(
+        worldObj["nextEntityId"].toVariant().toULongLong());
+    world->setNextEntityId(nextId);
+  }
+
+  if (worldObj.contains("ownerRegistry")) {
+    Game::Systems::OwnerRegistry::instance().fromJson(
+        worldObj["ownerRegistry"].toObject());
   }
 }
 

+ 23 - 0
game/core/world.cpp

@@ -2,6 +2,7 @@
 #include "../systems/owner_registry.h"
 #include "../systems/troop_count_registry.h"
 #include "component.h"
+#include <algorithm>
 
 namespace Engine::Core {
 
@@ -16,6 +17,22 @@ Entity *World::createEntity() {
   return ptr;
 }
 
+Entity *World::createEntityWithId(EntityID id) {
+  if (id == NULL_ENTITY) {
+    return nullptr;
+  }
+
+  auto entity = std::make_unique<Entity>(id);
+  auto ptr = entity.get();
+  m_entities[id] = std::move(entity);
+
+  if (id >= m_nextEntityId) {
+    m_nextEntityId = id + 1;
+  }
+
+  return ptr;
+}
+
 void World::destroyEntity(EntityID id) { m_entities.erase(id); }
 
 void World::clear() {
@@ -106,4 +123,10 @@ int World::countTroopsForPlayer(int ownerId) const {
   return Game::Systems::TroopCountRegistry::instance().getTroopCount(ownerId);
 }
 
+EntityID World::getNextEntityId() const { return m_nextEntityId; }
+
+void World::setNextEntityId(EntityID nextId) {
+  m_nextEntityId = std::max(nextId, m_nextEntityId);
+}
+
 } // namespace Engine::Core

+ 4 - 0
game/core/world.h

@@ -14,6 +14,7 @@ public:
   ~World();
 
   Entity *createEntity();
+  Entity *createEntityWithId(EntityID id);
   void destroyEntity(EntityID id);
   Entity *getEntity(EntityID id);
   void clear();
@@ -53,6 +54,9 @@ public:
     return m_entities;
   }
 
+  EntityID getNextEntityId() const;
+  void setNextEntityId(EntityID nextId);
+
 private:
   EntityID m_nextEntityId = 1;
   std::unordered_map<EntityID, std::unique_ptr<Entity>> m_entities;

+ 98 - 0
game/systems/owner_registry.cpp

@@ -1,5 +1,52 @@
 #include "owner_registry.h"
 #include <QDebug>
+#include <algorithm>
+
+namespace {
+
+QString ownerTypeToString(Game::Systems::OwnerType type) {
+  using Game::Systems::OwnerType;
+  switch (type) {
+  case OwnerType::Player:
+    return QStringLiteral("player");
+  case OwnerType::AI:
+    return QStringLiteral("ai");
+  case OwnerType::Neutral:
+  default:
+    return QStringLiteral("neutral");
+  }
+}
+
+Game::Systems::OwnerType ownerTypeFromString(const QString &value) {
+  using Game::Systems::OwnerType;
+  if (value.compare(QStringLiteral("player"), Qt::CaseInsensitive) == 0) {
+    return OwnerType::Player;
+  }
+  if (value.compare(QStringLiteral("ai"), Qt::CaseInsensitive) == 0) {
+    return OwnerType::AI;
+  }
+  return OwnerType::Neutral;
+}
+
+QJsonArray colorToJson(const std::array<float, 3> &color) {
+  QJsonArray array;
+  array.append(color[0]);
+  array.append(color[1]);
+  array.append(color[2]);
+  return array;
+}
+
+std::array<float, 3> colorFromJson(const QJsonArray &array) {
+  std::array<float, 3> color{0.8f, 0.9f, 1.0f};
+  if (array.size() >= 3) {
+    color[0] = static_cast<float>(array.at(0).toDouble());
+    color[1] = static_cast<float>(array.at(1).toDouble());
+    color[2] = static_cast<float>(array.at(2).toDouble());
+  }
+  return color;
+}
+
+} // namespace
 
 namespace Game::Systems {
 
@@ -223,4 +270,55 @@ std::array<float, 3> OwnerRegistry::getOwnerColor(int ownerId) const {
   return {0.8f, 0.9f, 1.0f};
 }
 
+QJsonObject OwnerRegistry::toJson() const {
+  QJsonObject root;
+  root["nextOwnerId"] = m_nextOwnerId;
+  root["localPlayerId"] = m_localPlayerId;
+
+  QJsonArray ownersArray;
+  for (const auto &owner : m_owners) {
+    QJsonObject ownerObj;
+    ownerObj["ownerId"] = owner.ownerId;
+    ownerObj["type"] = ownerTypeToString(owner.type);
+    ownerObj["name"] = QString::fromStdString(owner.name);
+    ownerObj["teamId"] = owner.teamId;
+    ownerObj["color"] = colorToJson(owner.color);
+    ownersArray.append(ownerObj);
+  }
+
+  root["owners"] = ownersArray;
+  return root;
+}
+
+void OwnerRegistry::fromJson(const QJsonObject &json) {
+  clear();
+
+  m_nextOwnerId = json["nextOwnerId"].toInt(1);
+  m_localPlayerId = json["localPlayerId"].toInt(1);
+
+  const auto ownersArray = json["owners"].toArray();
+  m_owners.reserve(ownersArray.size());
+  for (const auto &value : ownersArray) {
+    const auto ownerObj = value.toObject();
+    OwnerInfo info;
+    info.ownerId = ownerObj["ownerId"].toInt();
+    info.type = ownerTypeFromString(ownerObj["type"].toString());
+    info.name = ownerObj["name"].toString().toStdString();
+    info.teamId = ownerObj["teamId"].toInt(0);
+    if (ownerObj.contains("color")) {
+      info.color = colorFromJson(ownerObj["color"].toArray());
+    }
+
+    const size_t index = m_owners.size();
+    m_owners.push_back(info);
+    m_ownerIdToIndex[info.ownerId] = index;
+  }
+
+  for (const auto &owner : m_owners) {
+    if (owner.ownerId >= m_nextOwnerId) {
+      m_nextOwnerId = owner.ownerId + 1;
+    }
+  }
+}
+
 } // namespace Game::Systems

+ 5 - 0
game/systems/owner_registry.h

@@ -1,5 +1,7 @@
 #pragma once
 
+#include <QJsonArray>
+#include <QJsonObject>
 #include <array>
 #include <string>
 #include <unordered_map>
@@ -56,6 +58,9 @@ public:
   void setOwnerColor(int ownerId, float r, float g, float b);
   std::array<float, 3> getOwnerColor(int ownerId) const;
 
+  QJsonObject toJson() const;
+  void fromJson(const QJsonObject &json);
+
 private:
   OwnerRegistry() = default;
   ~OwnerRegistry() = default;

+ 95 - 159
game/systems/save_load_service.cpp

@@ -1,24 +1,32 @@
 #include "save_load_service.h"
+
 #include "game/core/serialization.h"
 #include "game/core/world.h"
+#include "save_storage.h"
+
 #include <QCoreApplication>
 #include <QDateTime>
 #include <QDebug>
 #include <QDir>
-#include <QFile>
-#include <QFileInfo>
-#include <QJsonArray>
 #include <QJsonDocument>
 #include <QJsonObject>
-#include <QList>
-#include <QRegularExpression>
 #include <QStandardPaths>
-#include <QVariant>
+
+#include <utility>
 
 namespace Game {
 namespace Systems {
 
-SaveLoadService::SaveLoadService() = default;
+SaveLoadService::SaveLoadService() {
+  ensureSavesDirectoryExists();
+  m_storage = std::make_unique<SaveStorage>(getDatabasePath());
+  QString initError;
+  if (!m_storage->initialize(&initError)) {
+    m_lastError = initError;
+    qWarning() << "SaveLoadService: failed to initialize storage" << initError;
+  }
+}
+
 SaveLoadService::~SaveLoadService() = default;
 
 QString SaveLoadService::getSavesDirectory() const {
@@ -27,6 +35,10 @@ QString SaveLoadService::getSavesDirectory() const {
   return savesPath + "/saves";
 }
 
+QString SaveLoadService::getDatabasePath() const {
+  return getSavesDirectory() + QStringLiteral("/saves.sqlite");
+}
+
 void SaveLoadService::ensureSavesDirectoryExists() const {
   QString savesDir = getSavesDirectory();
   QDir dir;
@@ -35,111 +47,53 @@ void SaveLoadService::ensureSavesDirectoryExists() const {
   }
 }
 
-QString SaveLoadService::getSlotFilePath(const QString &slotName) const {
-  QString sanitized = slotName;
-  QRegularExpression regex("[^a-zA-Z0-9_-]");
-  sanitized.replace(regex, "_");
-  return getSavesDirectory() + "/" + sanitized + ".json";
-}
-
-bool SaveLoadService::saveGame(Engine::Core::World &world,
-                                const QString &filename) {
-  qInfo() << "Saving game to:" << filename;
+bool SaveLoadService::saveGameToSlot(Engine::Core::World &world,
+                                     const QString &slotName,
+                                     const QString &title,
+                                     const QString &mapName,
+                                     const QJsonObject &metadata,
+                                     const QByteArray &screenshot) {
+  qInfo() << "Saving game to slot:" << slotName;
 
   try {
-    // Serialize the world to a JSON document
-    QJsonDocument doc = Engine::Core::Serialization::serializeWorld(&world);
-
-    // Save to file
-    bool success = Engine::Core::Serialization::saveToFile(filename, doc);
-
-    if (success) {
-      qInfo() << "Game saved successfully to:" << filename;
-      m_lastError.clear();
-      return true;
-    } else {
-      m_lastError = "Failed to save game to file: " + filename;
+    if (!m_storage) {
+      m_lastError = QStringLiteral("Save storage unavailable");
       qWarning() << m_lastError;
       return false;
     }
-  } catch (const std::exception &e) {
-    m_lastError = QString("Exception while saving game: %1").arg(e.what());
-    qWarning() << m_lastError;
-    return false;
-  }
-}
-
-bool SaveLoadService::loadGame(Engine::Core::World &world,
-                                const QString &filename) {
-  qInfo() << "Loading game from:" << filename;
 
-  try {
-    // Load JSON document from file
-    QJsonDocument doc = Engine::Core::Serialization::loadFromFile(filename);
+    QJsonDocument worldDoc =
+        Engine::Core::Serialization::serializeWorld(&world);
+    const QByteArray worldBytes =
+        worldDoc.toJson(QJsonDocument::Compact);
+
+    QJsonObject combinedMetadata = metadata;
+    combinedMetadata["slotName"] = slotName;
+    combinedMetadata["title"] = title;
+    combinedMetadata["timestamp"] = QDateTime::currentDateTimeUtc().toString(
+        Qt::ISODateWithMs);
+    if (!combinedMetadata.contains("mapName")) {
+      combinedMetadata["mapName"] =
+          mapName.isEmpty() ? QStringLiteral("Unknown Map") : mapName;
+    }
+    combinedMetadata["version"] = QStringLiteral("1.0");
 
-    if (doc.isNull() || doc.isEmpty()) {
-      m_lastError =
-          "Failed to load game from file or file is empty: " + filename;
-      qWarning() << m_lastError;
+    QString storageError;
+    if (!m_storage->saveSlot(slotName, title, combinedMetadata, worldBytes,
+                             screenshot, &storageError)) {
+      m_lastError = storageError;
+      qWarning() << "SaveLoadService: failed to persist slot" << storageError;
       return false;
     }
 
-    // Clear current world state before loading
-    world.clear();
-
-    // Deserialize the world from the JSON document
-    Engine::Core::Serialization::deserializeWorld(&world, doc);
-
-    qInfo() << "Game loaded successfully from:" << filename;
+    m_lastMetadata = combinedMetadata;
+    m_lastTitle = title;
+    m_lastScreenshot = screenshot;
     m_lastError.clear();
     return true;
   } catch (const std::exception &e) {
-    m_lastError = QString("Exception while loading game: %1").arg(e.what());
-    qWarning() << m_lastError;
-    return false;
-  }
-}
-
-bool SaveLoadService::saveGameToSlot(Engine::Core::World &world,
-                                     const QString &slotName,
-                                     const QString &mapName) {
-  qInfo() << "Saving game to slot:" << slotName;
-
-  try {
-    ensureSavesDirectoryExists();
-
-    // Serialize the world
-    QJsonDocument worldDoc =
-        Engine::Core::Serialization::serializeWorld(&world);
-    QJsonObject worldObj = worldDoc.object();
-
-    // Add metadata
-    QJsonObject metadata;
-    metadata["slotName"] = slotName;
-    metadata["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate);
-    metadata["mapName"] = mapName.isEmpty() ? "Unknown Map" : mapName;
-    metadata["version"] = "1.0";
-
-    worldObj["metadata"] = metadata;
-
-    QJsonDocument finalDoc(worldObj);
-
-    // Save to slot file
-    QString filePath = getSlotFilePath(slotName);
-    bool success = Engine::Core::Serialization::saveToFile(filePath, finalDoc);
-
-    if (success) {
-      qInfo() << "Game saved successfully to slot:" << slotName << "at"
-              << filePath;
-      m_lastError.clear();
-      return true;
-    } else {
-      m_lastError = "Failed to save game to slot: " + slotName;
-      qWarning() << m_lastError;
-      return false;
-    }
-  } catch (const std::exception &e) {
-    m_lastError = QString("Exception while saving game to slot: %1").arg(e.what());
+    m_lastError =
+        QString("Exception while saving game to slot: %1").arg(e.what());
     qWarning() << m_lastError;
     return false;
   }
@@ -150,31 +104,41 @@ bool SaveLoadService::loadGameFromSlot(Engine::Core::World &world,
   qInfo() << "Loading game from slot:" << slotName;
 
   try {
-    QString filePath = getSlotFilePath(slotName);
-
-    // Check if file exists
-    if (!QFile::exists(filePath)) {
-      m_lastError = "Save slot not found: " + slotName;
+    if (!m_storage) {
+      m_lastError = QStringLiteral("Save storage unavailable");
       qWarning() << m_lastError;
       return false;
     }
 
-    // Load JSON document
-    QJsonDocument doc = Engine::Core::Serialization::loadFromFile(filePath);
+    QByteArray worldBytes;
+    QJsonObject metadata;
+    QByteArray screenshot;
+    QString title;
+
+    QString loadError;
+    if (!m_storage->loadSlot(slotName, worldBytes, metadata, screenshot, title,
+                             &loadError)) {
+      m_lastError = loadError;
+      qWarning() << "SaveLoadService: failed to load slot" << loadError;
+      return false;
+    }
 
-    if (doc.isNull() || doc.isEmpty()) {
-      m_lastError = "Failed to load game from slot or file is empty: " + slotName;
+    QJsonParseError parseError{};
+    QJsonDocument doc =
+        QJsonDocument::fromJson(worldBytes, &parseError);
+    if (parseError.error != QJsonParseError::NoError || doc.isNull()) {
+      m_lastError = QStringLiteral("Corrupted save data for slot '%1': %2")
+                        .arg(slotName, parseError.errorString());
       qWarning() << m_lastError;
       return false;
     }
 
-    // Clear current world state before loading
     world.clear();
-
-    // Deserialize the world (metadata will be ignored during deserialization)
     Engine::Core::Serialization::deserializeWorld(&world, doc);
 
-    qInfo() << "Game loaded successfully from slot:" << slotName;
+    m_lastMetadata = metadata;
+    m_lastTitle = title;
+    m_lastScreenshot = screenshot;
     m_lastError.clear();
     return true;
   } catch (const std::exception &e) {
@@ -186,67 +150,39 @@ bool SaveLoadService::loadGameFromSlot(Engine::Core::World &world,
 }
 
 QVariantList SaveLoadService::getSaveSlots() const {
-  QVariantList slots;
-
-  QString savesDir = getSavesDirectory();
-  QDir dir(savesDir);
-
-  if (!dir.exists()) {
-    return slots;
+  if (!m_storage) {
+    return {};
   }
 
-  QStringList filters;
-  filters << "*.json";
-  QFileInfoList files =
-      dir.entryInfoList(filters, QDir::Files, QDir::Time | QDir::Reversed);
-
-  for (const QFileInfo &fileInfo : files) {
-    try {
-      QJsonDocument doc =
-          Engine::Core::Serialization::loadFromFile(fileInfo.absoluteFilePath());
-
-      if (!doc.isNull() && doc.isObject()) {
-        QJsonObject obj = doc.object();
-        QJsonObject metadata = obj["metadata"].toObject();
-
-        QVariantMap slot;
-        slot["name"] = metadata["slotName"].toString(fileInfo.baseName());
-        slot["timestamp"] = metadata["timestamp"].toString(
-            fileInfo.lastModified().toString(Qt::ISODate));
-        slot["mapName"] = metadata["mapName"].toString("Unknown Map");
-        slot["filePath"] = fileInfo.absoluteFilePath();
-
-        slots.append(slot);
-      }
-    } catch (...) {
-      // Skip corrupted save files
-      continue;
-    }
+  QString listError;
+  QVariantList slotList = m_storage->listSlots(&listError);
+  if (!listError.isEmpty()) {
+    m_lastError = listError;
+    qWarning() << "SaveLoadService: failed to enumerate slots" << listError;
+  } else {
+    m_lastError.clear();
   }
-
-  return slots;
+  return slotList;
 }
 
 bool SaveLoadService::deleteSaveSlot(const QString &slotName) {
   qInfo() << "Deleting save slot:" << slotName;
 
-  QString filePath = getSlotFilePath(slotName);
-
-  if (!QFile::exists(filePath)) {
-    m_lastError = "Save slot not found: " + slotName;
+  if (!m_storage) {
+    m_lastError = QStringLiteral("Save storage unavailable");
     qWarning() << m_lastError;
     return false;
   }
 
-  if (QFile::remove(filePath)) {
-    qInfo() << "Save slot deleted successfully:" << slotName;
-    m_lastError.clear();
-    return true;
-  } else {
-    m_lastError = "Failed to delete save slot: " + slotName;
-    qWarning() << m_lastError;
+  QString deleteError;
+  if (!m_storage->deleteSlot(slotName, &deleteError)) {
+    m_lastError = deleteError;
+    qWarning() << "SaveLoadService: failed to delete slot" << deleteError;
     return false;
   }
+
+  m_lastError.clear();
+  return true;
 }
 
 void SaveLoadService::openSettings() {

+ 20 - 11
game/systems/save_load_service.h

@@ -1,8 +1,11 @@
 #pragma once
 
+#include <QByteArray>
+#include <QJsonObject>
+#include <QVariantList>
 #include <QString>
-#include <QVariant>
-#include <functional>
+
+#include <memory>
 
 namespace Engine {
 namespace Core {
@@ -13,20 +16,18 @@ class World;
 namespace Game {
 namespace Systems {
 
+class SaveStorage;
+
 class SaveLoadService {
 public:
   SaveLoadService();
   ~SaveLoadService();
 
-  // Save current game state to file
-  bool saveGame(Engine::Core::World &world, const QString &filename);
-
-  // Load game state from file
-  bool loadGame(Engine::Core::World &world, const QString &filename);
-
   // Save game to named slot with metadata
   bool saveGameToSlot(Engine::Core::World &world, const QString &slotName,
-                      const QString &mapName = QString());
+                      const QString &title, const QString &mapName,
+                      const QJsonObject &metadata = {},
+                      const QByteArray &screenshot = QByteArray());
 
   // Load game from named slot
   bool loadGameFromSlot(Engine::Core::World &world, const QString &slotName);
@@ -43,6 +44,10 @@ public:
   // Clear the last error
   void clearError() { m_lastError.clear(); }
 
+  QJsonObject getLastMetadata() const { return m_lastMetadata; }
+  QString getLastTitle() const { return m_lastTitle; }
+  QByteArray getLastScreenshot() const { return m_lastScreenshot; }
+
   // Settings-related functionality
   void openSettings();
 
@@ -50,11 +55,15 @@ public:
   void exitGame();
 
 private:
-  QString getSlotFilePath(const QString &slotName) const;
   QString getSavesDirectory() const;
+  QString getDatabasePath() const;
   void ensureSavesDirectoryExists() const;
 
-  QString m_lastError;
+  mutable QString m_lastError;
+  QJsonObject m_lastMetadata;
+  QString m_lastTitle;
+  QByteArray m_lastScreenshot;
+  std::unique_ptr<SaveStorage> m_storage;
 };
 
 } // namespace Systems

+ 472 - 0
game/systems/save_storage.cpp

@@ -0,0 +1,472 @@
+#include "save_storage.h"
+
+#include <QDateTime>
+#include <QJsonDocument>
+#include <QMetaType>
+#include <QSqlDatabase>
+#include <QSqlError>
+#include <QSqlQuery>
+#include <QVariant>
+
+#include <utility>
+
+namespace Game::Systems {
+
+namespace {
+constexpr const char *kDriverName = "QSQLITE";
+constexpr int kCurrentSchemaVersion = 1;
+
+QString buildConnectionName(const SaveStorage *instance) {
+  return QStringLiteral("SaveStorage_%1").arg(
+      reinterpret_cast<quintptr>(instance), /*fieldWidth=*/0, /*base=*/16);
+}
+
+QString lastErrorString(const QSqlError &error) {
+  if (error.type() == QSqlError::NoError) {
+    return {};
+  }
+  return error.text();
+}
+
+class TransactionGuard {
+public:
+  explicit TransactionGuard(QSqlDatabase &database) : m_database(database) {}
+
+  bool begin(QString *outError) {
+    if (!m_database.transaction()) {
+      if (outError) {
+        *outError = QStringLiteral("Failed to begin transaction: %1")
+                        .arg(lastErrorString(m_database.lastError()));
+      }
+      return false;
+    }
+    m_active = true;
+    return true;
+  }
+
+  bool commit(QString *outError) {
+    if (!m_active) {
+      return true;
+    }
+
+    if (!m_database.commit()) {
+      if (outError) {
+        *outError = QStringLiteral("Failed to commit transaction: %1")
+                        .arg(lastErrorString(m_database.lastError()));
+      }
+      rollback();
+      return false;
+    }
+
+    m_active = false;
+    return true;
+  }
+
+  void rollback() {
+    if (m_active) {
+      m_database.rollback();
+      m_active = false;
+    }
+  }
+
+  ~TransactionGuard() { rollback(); }
+
+private:
+  QSqlDatabase &m_database;
+  bool m_active = false;
+};
+} // namespace
+
+SaveStorage::SaveStorage(QString databasePath)
+    : m_databasePath(std::move(databasePath)),
+      m_connectionName(buildConnectionName(this)) {}
+
+SaveStorage::~SaveStorage() {
+  if (m_database.isValid()) {
+    if (m_database.isOpen()) {
+      m_database.close();
+    }
+    const QString connectionName = m_connectionName;
+    m_database = QSqlDatabase();
+    QSqlDatabase::removeDatabase(connectionName);
+  }
+}
+
+bool SaveStorage::initialize(QString *outError) {
+  if (m_initialized && m_database.isValid() && m_database.isOpen()) {
+    return true;
+  }
+  if (!open(outError)) {
+    return false;
+  }
+  if (!ensureSchema(outError)) {
+    return false;
+  }
+  m_initialized = true;
+  return true;
+}
+
+bool SaveStorage::saveSlot(const QString &slotName, const QString &title,
+                           const QJsonObject &metadata,
+                           const QByteArray &worldState,
+                           const QByteArray &screenshot,
+                           QString *outError) {
+  if (!initialize(outError)) {
+    return false;
+  }
+
+  TransactionGuard transaction(m_database);
+  if (!transaction.begin(outError)) {
+    return false;
+  }
+
+  QSqlQuery query(m_database);
+  const QString insertSql =
+      QStringLiteral(
+          "INSERT INTO saves (slot_name, title, map_name, timestamp, "
+          "metadata, world_state, screenshot, created_at, updated_at) "
+          "VALUES (:slot_name, :title, :map_name, :timestamp, :metadata, "
+          ":world_state, :screenshot, :created_at, :updated_at) "
+          "ON CONFLICT(slot_name) DO UPDATE SET "
+          "title = excluded.title, "
+          "map_name = excluded.map_name, "
+          "timestamp = excluded.timestamp, "
+          "metadata = excluded.metadata, "
+          "world_state = excluded.world_state, "
+          "screenshot = excluded.screenshot, "
+          "updated_at = excluded.updated_at");
+
+  if (!query.prepare(insertSql)) {
+    if (outError) {
+      *outError = QStringLiteral("Failed to prepare save query: %1")
+                       .arg(lastErrorString(query.lastError()));
+    }
+    return false;
+  }
+
+  const QString nowIso =
+      QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs);
+  QString mapName = metadata.value("mapName").toString();
+  if (mapName.isEmpty()) {
+    mapName = QStringLiteral("Unknown Map");
+  }
+  const QByteArray metadataBytes =
+      QJsonDocument(metadata).toJson(QJsonDocument::Compact);
+
+  query.bindValue(QStringLiteral(":slot_name"), slotName);
+  query.bindValue(QStringLiteral(":title"), title);
+  query.bindValue(QStringLiteral(":map_name"), mapName);
+  query.bindValue(QStringLiteral(":timestamp"), nowIso);
+  query.bindValue(QStringLiteral(":metadata"), metadataBytes);
+  query.bindValue(QStringLiteral(":world_state"), worldState);
+  if (screenshot.isEmpty()) {
+    query.bindValue(QStringLiteral(":screenshot"),
+                    QVariant(QMetaType::fromType<QByteArray>()));
+  } else {
+    query.bindValue(QStringLiteral(":screenshot"), screenshot);
+  }
+  query.bindValue(QStringLiteral(":created_at"), nowIso);
+  query.bindValue(QStringLiteral(":updated_at"), nowIso);
+
+  if (!query.exec()) {
+    if (outError) {
+      *outError = QStringLiteral("Failed to persist save slot: %1")
+                       .arg(lastErrorString(query.lastError()));
+    }
+    transaction.rollback();
+    return false;
+  }
+
+  if (!transaction.commit(outError)) {
+    return false;
+  }
+
+  return true;
+}
+
+bool SaveStorage::loadSlot(const QString &slotName, QByteArray &worldState,
+                           QJsonObject &metadata, QByteArray &screenshot,
+                           QString &title, QString *outError) {
+  if (!initialize(outError)) {
+    return false;
+  }
+
+  QSqlQuery query(m_database);
+  query.prepare(QStringLiteral(
+      "SELECT title, metadata, world_state, screenshot FROM saves "
+      "WHERE slot_name = :slot_name"));
+  query.bindValue(QStringLiteral(":slot_name"), slotName);
+
+  if (!query.exec()) {
+    if (outError) {
+      *outError = QStringLiteral("Failed to read save slot: %1")
+                       .arg(lastErrorString(query.lastError()));
+    }
+    return false;
+  }
+
+  if (!query.next()) {
+    if (outError) {
+      *outError = QStringLiteral("Save slot '%1' not found").arg(slotName);
+    }
+    return false;
+  }
+
+  title = query.value(0).toString();
+  const QByteArray metadataBytes = query.value(1).toByteArray();
+  metadata = QJsonDocument::fromJson(metadataBytes).object();
+  worldState = query.value(2).toByteArray();
+  screenshot = query.value(3).toByteArray();
+  return true;
+}
+
+QVariantList SaveStorage::listSlots(QString *outError) const {
+  QVariantList result;
+  if (!const_cast<SaveStorage *>(this)->initialize(outError)) {
+    return result;
+  }
+
+  QSqlQuery query(m_database);
+  if (!query.exec(QStringLiteral(
+          "SELECT slot_name, title, map_name, timestamp, metadata, screenshot "
+          "FROM saves ORDER BY datetime(timestamp) DESC"))) {
+    if (outError) {
+      *outError = QStringLiteral("Failed to enumerate save slots: %1")
+                       .arg(lastErrorString(query.lastError()));
+    }
+    return result;
+  }
+
+  while (query.next()) {
+    QVariantMap slot;
+    slot.insert(QStringLiteral("slotName"), query.value(0).toString());
+    slot.insert(QStringLiteral("title"), query.value(1).toString());
+    slot.insert(QStringLiteral("mapName"), query.value(2).toString());
+    slot.insert(QStringLiteral("timestamp"), query.value(3).toString());
+
+    const QByteArray metadataBytes = query.value(4).toByteArray();
+    const QJsonObject metadataObj =
+        QJsonDocument::fromJson(metadataBytes).object();
+    slot.insert(QStringLiteral("metadata"), metadataObj.toVariantMap());
+
+    const QByteArray screenshotBytes = query.value(5).toByteArray();
+    if (!screenshotBytes.isEmpty()) {
+      slot.insert(QStringLiteral("thumbnail"),
+                  QString::fromLatin1(screenshotBytes.toBase64()));
+    } else {
+      slot.insert(QStringLiteral("thumbnail"), QString());
+    }
+
+    // Optional helpers for QML convenience
+    if (metadataObj.contains("playTime")) {
+      slot.insert(QStringLiteral("playTime"),
+                  metadataObj.value("playTime").toString());
+    }
+
+    result.append(slot);
+  }
+
+  return result;
+}
+
+bool SaveStorage::deleteSlot(const QString &slotName, QString *outError) {
+  if (!initialize(outError)) {
+    return false;
+  }
+
+  TransactionGuard transaction(m_database);
+  if (!transaction.begin(outError)) {
+    return false;
+  }
+
+  QSqlQuery query(m_database);
+  query.prepare(
+      QStringLiteral("DELETE FROM saves WHERE slot_name = :slot_name"));
+  query.bindValue(QStringLiteral(":slot_name"), slotName);
+
+  if (!query.exec()) {
+    if (outError) {
+      *outError = QStringLiteral("Failed to delete save slot: %1")
+                       .arg(lastErrorString(query.lastError()));
+    }
+    transaction.rollback();
+    return false;
+  }
+
+  if (query.numRowsAffected() == 0) {
+    if (outError) {
+      *outError = QStringLiteral("Save slot '%1' not found").arg(slotName);
+    }
+    transaction.rollback();
+    return false;
+  }
+
+  if (!transaction.commit(outError)) {
+    return false;
+  }
+
+  return true;
+}
+
+bool SaveStorage::open(QString *outError) const {
+  if (m_database.isValid() && m_database.isOpen()) {
+    return true;
+  }
+
+  if (!m_database.isValid()) {
+    m_database = QSqlDatabase::addDatabase(kDriverName, m_connectionName);
+    m_database.setDatabaseName(m_databasePath);
+    m_database.setConnectOptions(QStringLiteral("QSQLITE_BUSY_TIMEOUT=5000"));
+  }
+
+  if (!m_database.open()) {
+    if (outError) {
+      *outError = QStringLiteral("Failed to open save database: %1")
+                       .arg(lastErrorString(m_database.lastError()));
+    }
+    return false;
+  }
+
+  QSqlQuery foreignKeysQuery(m_database);
+  foreignKeysQuery.exec(QStringLiteral("PRAGMA foreign_keys = ON"));
+
+  QSqlQuery journalModeQuery(m_database);
+  journalModeQuery.exec(QStringLiteral("PRAGMA journal_mode=WAL"));
+
+  return true;
+}
+
+bool SaveStorage::ensureSchema(QString *outError) const {
+  const int currentVersion = schemaVersion(outError);
+  if (currentVersion < 0) {
+    return false;
+  }
+
+  if (currentVersion > kCurrentSchemaVersion) {
+    if (outError) {
+      *outError = QStringLiteral(
+          "Save database schema version %1 is newer than supported %2")
+                       .arg(currentVersion)
+                       .arg(kCurrentSchemaVersion);
+    }
+    return false;
+  }
+
+  if (currentVersion == kCurrentSchemaVersion) {
+    return true;
+  }
+
+  TransactionGuard transaction(m_database);
+  if (!transaction.begin(outError)) {
+    return false;
+  }
+
+  if (!migrateSchema(currentVersion, outError)) {
+    transaction.rollback();
+    return false;
+  }
+
+  if (!setSchemaVersion(kCurrentSchemaVersion, outError)) {
+    transaction.rollback();
+    return false;
+  }
+
+  if (!transaction.commit(outError)) {
+    return false;
+  }
+
+  return true;
+}
+
+int SaveStorage::schemaVersion(QString *outError) const {
+  QSqlQuery pragmaQuery(m_database);
+  if (!pragmaQuery.exec(QStringLiteral("PRAGMA user_version"))) {
+    if (outError) {
+      *outError = QStringLiteral("Failed to read schema version: %1")
+                       .arg(lastErrorString(pragmaQuery.lastError()));
+    }
+    return -1;
+  }
+
+  if (pragmaQuery.next()) {
+    return pragmaQuery.value(0).toInt();
+  }
+
+  return 0;
+}
+
+bool SaveStorage::setSchemaVersion(int version, QString *outError) const {
+  QSqlQuery pragmaQuery(m_database);
+  if (!pragmaQuery.exec(
+          QStringLiteral("PRAGMA user_version = %1").arg(version))) {
+    if (outError) {
+      *outError = QStringLiteral("Failed to update schema version: %1")
+                       .arg(lastErrorString(pragmaQuery.lastError()));
+    }
+    return false;
+  }
+  return true;
+}
+
+bool SaveStorage::createBaseSchema(QString *outError) const {
+  QSqlQuery query(m_database);
+  const QString createSql = QStringLiteral(
+      "CREATE TABLE IF NOT EXISTS saves ("
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, "
+      "slot_name TEXT UNIQUE NOT NULL, "
+      "title TEXT NOT NULL, "
+      "map_name TEXT, "
+      "timestamp TEXT NOT NULL, "
+      "metadata BLOB NOT NULL, "
+      "world_state BLOB NOT NULL, "
+      "screenshot BLOB, "
+      "created_at TEXT NOT NULL, "
+      "updated_at TEXT NOT NULL"
+      ")");
+
+  if (!query.exec(createSql)) {
+    if (outError) {
+      *outError = QStringLiteral("Failed to create save schema: %1")
+                       .arg(lastErrorString(query.lastError()));
+    }
+    return false;
+  }
+
+  QSqlQuery indexQuery(m_database);
+  if (!indexQuery.exec(QStringLiteral(
+          "CREATE INDEX IF NOT EXISTS idx_saves_updated_at ON saves "
+          "(updated_at DESC)"))) {
+    if (outError) {
+      *outError = QStringLiteral("Failed to build save index: %1")
+                       .arg(lastErrorString(indexQuery.lastError()));
+    }
+    return false;
+  }
+
+  return true;
+}
+
+bool SaveStorage::migrateSchema(int fromVersion, QString *outError) const {
+  int version = fromVersion;
+
+  while (version < kCurrentSchemaVersion) {
+    switch (version) {
+    case 0:
+      if (!createBaseSchema(outError)) {
+        return false;
+      }
+      version = 1;
+      break;
+    default:
+      if (outError) {
+        *outError = QStringLiteral("Unsupported migration path from %1")
+                         .arg(version);
+      }
+      return false;
+    }
+  }
+
+  return true;
+}
+
+} // namespace Game::Systems

+ 46 - 0
game/systems/save_storage.h

@@ -0,0 +1,46 @@
+#pragma once
+
+#include <QByteArray>
+#include <QJsonObject>
+#include <QSqlDatabase>
+#include <QString>
+#include <QVariantList>
+
+#include <memory>
+
+namespace Game::Systems {
+
+class SaveStorage {
+public:
+  explicit SaveStorage(QString databasePath);
+  ~SaveStorage();
+
+  bool initialize(QString *outError = nullptr);
+
+  bool saveSlot(const QString &slotName, const QString &title,
+                const QJsonObject &metadata, const QByteArray &worldState,
+                const QByteArray &screenshot, QString *outError = nullptr);
+
+  bool loadSlot(const QString &slotName, QByteArray &worldState,
+                QJsonObject &metadata, QByteArray &screenshot, QString &title,
+                QString *outError = nullptr);
+
+  QVariantList listSlots(QString *outError = nullptr) const;
+
+  bool deleteSlot(const QString &slotName, QString *outError = nullptr);
+
+private:
+  bool open(QString *outError = nullptr) const;
+  bool ensureSchema(QString *outError = nullptr) const;
+  bool createBaseSchema(QString *outError = nullptr) const;
+  bool migrateSchema(int fromVersion, QString *outError = nullptr) const;
+  int schemaVersion(QString *outError = nullptr) const;
+  bool setSchemaVersion(int version, QString *outError = nullptr) const;
+
+  QString m_databasePath;
+  QString m_connectionName;
+  mutable bool m_initialized = false;
+  mutable QSqlDatabase m_database;
+};
+
+} // namespace Game::Systems

+ 151 - 29
ui/qml/LoadGamePanel.qml

@@ -1,6 +1,7 @@
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
+import QtQml 2.15
 import StandardOfIron.UI 1.0
 
 Item {
@@ -11,6 +12,61 @@ Item {
 
     anchors.fill: parent
     z: 25
+    onVisibleChanged: {
+        if (!visible) {
+            return
+        }
+
+        if (typeof loadListModel !== 'undefined') {
+            loadListModel.loadFromGame()
+        }
+
+        if (typeof loadListView !== 'undefined') {
+            loadListView.selectedIndex = loadListModel.count > 0 && !loadListModel.get(0).isEmpty ? 0 : -1
+        }
+    }
+
+    Connections {
+        target: typeof game !== 'undefined' ? game : null
+        onSaveSlotsChanged: {
+            if (typeof loadListModel === 'undefined') {
+                return
+            }
+
+            var previousSlot = ""
+            if (typeof loadListView !== 'undefined' && loadListView.selectedIndex >= 0 && loadListView.selectedIndex < loadListModel.count) {
+                var current = loadListModel.get(loadListView.selectedIndex)
+                if (current && !current.isEmpty) {
+                    previousSlot = current.slotName
+                }
+            }
+
+            loadListModel.loadFromGame()
+
+            if (typeof loadListView === 'undefined') {
+                return
+            }
+
+            var newIndex = -1
+            if (previousSlot !== "") {
+                for (var i = 0; i < loadListModel.count; ++i) {
+                    var slot = loadListModel.get(i)
+                    if (!slot.isEmpty && slot.slotName === previousSlot) {
+                        newIndex = i
+                        break
+                    }
+                }
+            }
+
+            if (newIndex === -1) {
+                if (loadListModel.count > 0 && !loadListModel.get(0).isEmpty) {
+                    newIndex = 0
+                }
+            }
+
+            loadListView.selectedIndex = newIndex
+        }
+    }
 
     Rectangle {
         anchors.fill: parent
@@ -81,36 +137,57 @@ Item {
                         model: ListModel {
                             id: loadListModel
 
-                            Component.onCompleted: {
-                                // Populate with existing saves
-                                if (typeof game !== 'undefined' && game.getSaveSlots) {
-                                    var slots = game.getSaveSlots()
-                                    for (var i = 0; i < slots.length; i++) {
-                                        append({
-                                            slotName: slots[i].name,
-                                            timestamp: slots[i].timestamp,
-                                            mapName: slots[i].mapName || "Unknown Map",
-                                            playTime: slots[i].playTime || "Unknown"
-                                        })
-                                    }
+                            function loadFromGame() {
+                                clear()
+
+                                if (typeof game === 'undefined' || !game.getSaveSlots) {
+                                    append({
+                                        slotName: "No saves found",
+                                        title: "",
+                                        timestamp: 0,
+                                        mapName: "",
+                                        playTime: "",
+                                        thumbnail: "",
+                                        isEmpty: true
+                                    })
+                                    return
+                                }
+
+                                var slots = game.getSaveSlots()
+                                for (var i = 0; i < slots.length; i++) {
+                                    append({
+                                        slotName: slots[i].slotName || slots[i].name,
+                                        title: slots[i].title || slots[i].name || slots[i].slotName || "Untitled Save",
+                                        timestamp: slots[i].timestamp,
+                                        mapName: slots[i].mapName || "Unknown Map",
+                                        playTime: slots[i].playTime || "",
+                                        thumbnail: slots[i].thumbnail || "",
+                                        isEmpty: false
+                                    })
                                 }
-                                
+
                                 if (count === 0) {
                                     append({
                                         slotName: "No saves found",
+                                        title: "",
                                         timestamp: 0,
                                         mapName: "",
                                         playTime: "",
+                                        thumbnail: "",
                                         isEmpty: true
                                     })
                                 }
                             }
+
+                            Component.onCompleted: {
+                                loadFromGame()
+                            }
                         }
 
                         spacing: Theme.spacingSmall
                         delegate: Rectangle {
                             width: loadListView.width
-                            height: model.isEmpty ? 100 : 120
+                            height: model.isEmpty ? 100 : 130
                             color: loadListView.selectedIndex === index ? Theme.selectedBg : 
                                    mouseArea.containsMouse ? Theme.hoverBg : Qt.rgba(0, 0, 0, 0)
                             radius: Theme.radiusMedium
@@ -123,13 +200,44 @@ Item {
                                 anchors.margins: Theme.spacingMedium
                                 spacing: Theme.spacingMedium
 
+                                Rectangle {
+                                    id: loadThumbnail
+                                    Layout.preferredWidth: 128
+                                    Layout.preferredHeight: 80
+                                    radius: Theme.radiusSmall
+                                    color: Theme.cardBase
+                                    border.color: Theme.cardBorder
+                                    border.width: 1
+                                    clip: true
+                                    visible: !model.isEmpty
+
+                                    Image {
+                                        id: loadThumbnailImage
+                                        anchors.fill: parent
+                                        anchors.margins: 2
+                                        fillMode: Image.PreserveAspectCrop
+                                        source: model.thumbnail && model.thumbnail.length > 0
+                                                    ? "data:image/png;base64," + model.thumbnail
+                                                    : ""
+                                        visible: source !== ""
+                                    }
+
+                                    Label {
+                                        anchors.centerIn: parent
+                                        visible: !loadThumbnailImage.visible
+                                        text: "No Preview"
+                                        color: Theme.textHint
+                                        font.pointSize: Theme.fontSizeTiny
+                                    }
+                                }
+
                                 ColumnLayout {
                                     Layout.fillWidth: true
                                     spacing: Theme.spacingTiny
                                     visible: !model.isEmpty
 
                                     Label {
-                                        text: model.slotName
+                                        text: model.title
                                         color: Theme.textMain
                                         font.pointSize: Theme.fontSizeLarge
                                         font.bold: true
@@ -137,6 +245,14 @@ Item {
                                         elide: Label.ElideRight
                                     }
 
+                                    Label {
+                                        text: "Slot: " + model.slotName
+                                        color: Theme.textSub
+                                        font.pointSize: Theme.fontSizeSmall
+                                        Layout.fillWidth: true
+                                        elide: Label.ElideRight
+                                    }
+
                                     Label {
                                         text: model.mapName
                                         color: Theme.textSub
@@ -225,7 +341,7 @@ Item {
 
                 Label {
                     text: loadListView.selectedIndex >= 0 && !loadListModel.get(loadListView.selectedIndex).isEmpty ? 
-                          "Selected: " + loadListModel.get(loadListView.selectedIndex).slotName : 
+                          "Selected: " + loadListModel.get(loadListView.selectedIndex).title : 
                           "Select a save to load"
                     color: Theme.textSub
                     font.pointSize: Theme.fontSizeMedium
@@ -236,7 +352,7 @@ Item {
                     enabled: loadListView.selectedIndex >= 0 && !loadListModel.get(loadListView.selectedIndex).isEmpty
                     highlighted: true
                     onClicked: {
-                        if (loadListView.selectedIndex >= 0) {
+                        if (loadListView.selectedIndex >= 0 && !loadListModel.get(loadListView.selectedIndex).isEmpty) {
                             root.loadRequested(loadListModel.get(loadListView.selectedIndex).slotName)
                         }
                     }
@@ -259,18 +375,24 @@ Item {
 
         onAccepted: {
             if (typeof game !== 'undefined' && game.deleteSaveSlot) {
-                game.deleteSaveSlot(slotName)
-                loadListModel.remove(slotIndex)
-                
-                // Add empty message if no saves left
-                if (loadListModel.count === 0) {
-                    loadListModel.append({
-                        slotName: "No saves found",
-                        timestamp: 0,
-                        mapName: "",
-                        playTime: "",
-                        isEmpty: true
-                    })
+                if (game.deleteSaveSlot(slotName)) {
+                    loadListModel.remove(slotIndex)
+
+                    if (loadListModel.count === 0) {
+                        loadListModel.append({
+                            slotName: "No saves found",
+                            title: "",
+                            timestamp: 0,
+                            mapName: "",
+                            playTime: "",
+                            thumbnail: "",
+                            isEmpty: true
+                        })
+                    }
+
+                    if (loadListView.selectedIndex >= loadListModel.count) {
+                        loadListView.selectedIndex = loadListModel.count > 0 && !loadListModel.get(0).isEmpty ? loadListModel.count - 1 : -1
+                    }
                 }
             }
         }

+ 82 - 12
ui/qml/SaveGamePanel.qml

@@ -1,6 +1,7 @@
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
+import QtQml 2.15
 import StandardOfIron.UI 1.0
 
 Item {
@@ -11,6 +12,28 @@ Item {
 
     anchors.fill: parent
     z: 25
+    onVisibleChanged: {
+        if (!visible) {
+            return
+        }
+
+        if (typeof saveListModel !== 'undefined') {
+            saveListModel.loadFromGame()
+        }
+
+        if (typeof saveNameField !== 'undefined' && saveNameField) {
+            saveNameField.text = "Save_" + Qt.formatDateTime(new Date(), "yyyy-MM-dd_HH-mm")
+        }
+    }
+
+    Connections {
+        target: typeof game !== 'undefined' ? game : null
+        onSaveSlotsChanged: {
+            if (typeof saveListModel !== 'undefined') {
+                saveListModel.loadFromGame()
+            }
+        }
+    }
 
     Rectangle {
         anchors.fill: parent
@@ -138,19 +161,28 @@ Item {
                                 return false
                             }
 
-                            Component.onCompleted: {
-                                // Populate with existing saves
-                                if (typeof game !== 'undefined' && game.getSaveSlots) {
-                                    var slots = game.getSaveSlots()
-                                    for (var i = 0; i < slots.length; i++) {
-                                        append({
-                                            slotName: slots[i].name,
-                                            timestamp: slots[i].timestamp,
-                                            mapName: slots[i].mapName || "Unknown Map"
-                                        })
-                                    }
+                            function loadFromGame() {
+                                clear()
+
+                                if (typeof game === 'undefined' || !game.getSaveSlots) {
+                                    return
+                                }
+
+                                var slots = game.getSaveSlots()
+                                for (var i = 0; i < slots.length; i++) {
+                                    append({
+                                        slotName: slots[i].slotName || slots[i].name,
+                                        title: slots[i].title || slots[i].name || slots[i].slotName || "Untitled Save",
+                                        timestamp: slots[i].timestamp,
+                                        mapName: slots[i].mapName || "Unknown Map",
+                                        thumbnail: slots[i].thumbnail || ""
+                                    })
                                 }
                             }
+
+                            Component.onCompleted: {
+                                loadFromGame()
+                            }
                         }
 
                         spacing: Theme.spacingSmall
@@ -167,12 +199,42 @@ Item {
                                 anchors.margins: Theme.spacingMedium
                                 spacing: Theme.spacingMedium
 
+                                Rectangle {
+                                    id: thumbnailContainer
+                                    Layout.preferredWidth: 96
+                                    Layout.preferredHeight: 64
+                                    radius: Theme.radiusSmall
+                                    color: Theme.cardBase
+                                    border.color: Theme.cardBorder
+                                    border.width: 1
+                                    clip: true
+
+                                    Image {
+                                        id: thumbnailImage
+                                        anchors.fill: parent
+                                        anchors.margins: 2
+                                        fillMode: Image.PreserveAspectCrop
+                                        source: model.thumbnail && model.thumbnail.length > 0
+                                                    ? "data:image/png;base64," + model.thumbnail
+                                                    : ""
+                                        visible: source !== ""
+                                    }
+
+                                    Label {
+                                        anchors.centerIn: parent
+                                        visible: !thumbnailImage.visible
+                                        text: "No Preview"
+                                        color: Theme.textHint
+                                        font.pointSize: Theme.fontSizeTiny
+                                    }
+                                }
+
                                 ColumnLayout {
                                     Layout.fillWidth: true
                                     spacing: Theme.spacingTiny
 
                                     Label {
-                                        text: model.slotName
+                                        text: model.title
                                         color: Theme.textMain
                                         font.pointSize: Theme.fontSizeLarge
                                         font.bold: true
@@ -180,6 +242,14 @@ Item {
                                         elide: Label.ElideRight
                                     }
 
+                                    Label {
+                                        text: "Slot: " + model.slotName
+                                        color: Theme.textSub
+                                        font.pointSize: Theme.fontSizeSmall
+                                        Layout.fillWidth: true
+                                        elide: Label.ElideRight
+                                    }
+
                                     Label {
                                         text: model.mapName
                                         color: Theme.textSub

+ 2 - 0
ui/qml/qmldir

@@ -2,3 +2,5 @@ module StandardOfIron.UI
 singleton StyleGuide 1.0 StyleGuide.qml
 StyledButton 1.0 StyledButton.qml
 ProductionPanel 1.0 ProductionPanel.qml
+SaveGamePanel 1.0 SaveGamePanel.qml
+LoadGamePanel 1.0 LoadGamePanel.qml