存档系统

Save System v1.2.0 - Infinite Slot Architecture

版本: v1.2.0
创建日期: 2026-01-14
更新日期: 2026-02-04
状态: 全部完成 ✅

需求概述

背景问题

原系统存在以下问题:

  1. 游戏启动时自动加载数据库,用户未点击任何按钮数据就已读入内存
  2. 退出游戏时直接调用 get_tree().quit(),无保存选项
  3. 所有修改直接写入原始数据库文件,开发调试时可能损坏数据

目标

  1. 延迟加载: 启动时不加载数据库,等待用户选择后再加载
  2. 保存功能: 退出时提供保存选项,创建新的存档文件
  3. 无限槽位: 每次保存都创建新文件(时间戳命名),永不覆盖
  4. 原始文件保护: 开发模式下保存到项目目录 res://sqlite_database/saves/

主菜单三按钮设计

按钮 功能
开始游戏 从原始 game_trade.db 加载,开始全新游戏
继续游戏 从最新存档文件加载(如无存档则等同开始游戏)
加载游戏 显示所有存档列表,供玩家选择加载

游戏内ESC菜单设计

按钮 功能
保存 保存当前进度,继续游戏
保存并退出 保存后退出程序
保存并返回主界面 保存后返回主菜单(方便调试)
返回主界面 不保存,直接返回主菜单
返回游戏 关闭对话框,继续游戏

系统架构

┌─────────────────────────────────────────────────────────────┐
│                        StartMenu                             │
│   [开始游戏]  [继续游戏]  [加载游戏]  [退出游戏]              │
└─────────────────────────────────────────────────────────────┘
         │            │            │            │
         ▼            ▼            ▼            ▼
┌─────────────────────────────────────────────────────────────┐
│                       GlobalData                             │
│  - start_new_game()                                          │
│  - load_database_from_file(path)                             │
│  - is_game_loaded()                                          │
│  - unload_game()                                             │
└─────────────────────────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────────────────────────┐
│                    SaveGameManager                           │
│  - quick_save_to_new_file() → 创建新存档文件                 │
│  - save_to_file(path) → 覆盖指定存档                         │
│  - get_all_saves() → 获取所有存档列表                        │
│  - get_latest_save() → 获取最新存档                          │
│  - delete_save_file(path) → 删除指定存档                     │
└─────────────────────────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────────────────────────┐
│                  MemoryDataManager                           │
│  - sync_all_dirty_to_database() → 同步脏数据到SQLite         │
│  - DirtyTracker → 追踪修改的数据                             │
└─────────────────────────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────────────────────────┐
│                      SQLite Database                         │
│  原始: res://sqlite_database/game_trade.db                   │
│  存档: res://sqlite_database/saves/game_save_YYYYMMDD_HHMMSS.db │
└─────────────────────────────────────────────────────────────┘

实施阶段

阶段0:延迟加载重构 ✅ 已完成

目标: 重构 GlobalData 为延迟加载模式,启动时不自动加载数据库

修改文件:

  • scripts/core/global_data.gd
  • scripts/ui/menus/start_menu.gd
  • scripts/ui/menus/main_menu.gd
  • scripts/core/managers/scene_manager.gd
  • scripts/ui/hud/welcome_ui.gd
  • scripts/database/button_sql.gd

新增API (global_data.gd):

# 状态变量
var _game_loaded: bool = false
var _current_db_path: String = ""

# 常量
const ORIGINAL_DATABASE_PATH = "res://sqlite_database/game_trade.db"
const SAVE_DIR = "user://saves/"

# 检查游戏是否已加载
func is_game_loaded() -> bool

# 获取当前数据库路径
func get_current_db_path() -> String

# 开始新游戏(从原始数据库加载)
func start_new_game() -> bool

# 从指定数据库文件加载游戏
func load_database_from_file(db_path: String) -> bool

# 卸载当前游戏(清理所有数据)
func unload_game()

阶段1:数据同步功能 ✅ 已完成

目标: 增强 MemoryDataManager,添加将脏数据同步到SQLite的功能

## 将所有脏数据同步到SQLite数据库
func sync_all_dirty_to_database() -> bool

## 获取待同步的脏数据统计
func get_pending_sync_stats() -> Dictionary

同步策略:

表名 同步操作 状态
traders UPDATE 指定字段
bulk_commodity_holdings INSERT/UPDATE/DELETE 根据quantity
mercenaries UPDATE 状态字段
mercenary_mood UPDATE 心情值
equipment_instances UPDATE 所有者/状态
adventure_projects UPDATE 进度/状态
adventure_teams UPDATE 队伍状态
team_members INSERT/UPDATE/DELETE

阶段2:无限槽位保存系统 ✅ 已完成

存档文件策略:

res://sqlite_database/saves/          # 开发模式保存在项目目录
├── game_save_20260114_103045.db      # 时间戳命名
├── game_save_20260114_112233.db      # 永不覆盖
├── game_save_20260114_143022.db      # 无限数量
└── ...

新增API:

const SAVE_DIR = "res://sqlite_database/saves/"
const SAVE_PREFIX = "game_save_"
const ORIGINAL_DB = "res://sqlite_database/game_trade.db"

## 快速保存游戏到新文件(时间戳命名,永不覆盖)
func quick_save_to_new_file() -> Dictionary

## 保存到指定文件(覆盖现有存档)
func save_to_file(file_path: String) -> Dictionary

## 获取所有存档列表(按时间倒序)
func get_all_saves() -> Array

## 获取最新存档
func get_latest_save() -> Dictionary

## 删除指定存档
func delete_save_file(file_path: String) -> bool

## 获取存档数量
func get_save_count() -> int

阶段3:存档列表UI ✅ 已完成

UI特性:

  • 支持加载模式保存模式两种显示
  • 保存模式下顶部显示"创建新存档"(绿色高亮)
  • 保存模式下现有存档显示"覆盖"按钮
  • 加载模式下显示"加载"按钮
  • 两种模式都支持删除存档(带确认对话框)
  • 独立扫描功能: 在主菜单时也能直接扫描存档目录

阶段4:退出确认对话框 ✅ 已完成

┌─────────────────────────────────┐
│          确认退出                │
├─────────────────────────────────┤
│                                 │
│   是否在退出前保存游戏?         │
│                                 │
│   ⚠️ 有 X 条未保存的数据        │
│                                 │
│   [      保存       ]           │
│   [   保存并退出    ]           │
│   [ 保存并返回主界面 ]          │
│   [   返回主界面    ]           │
│   [    返回游戏     ]           │
│                                 │
└─────────────────────────────────┘

阶段5:主菜单逻辑整合 ✅ 已完成

Player.gd ESC键处理:

func _input(event):
    # 按Shift+ESC打开经济菜单
    if event.is_action_pressed("ui_cancel") and Input.is_key_pressed(KEY_SHIFT):
        open_economy_menu()
    # 单独按ESC打开退出确认对话框
    elif event.is_action_pressed("ui_cancel") and not Input.is_key_pressed(KEY_SHIFT):
        open_quit_dialog()

文件结构

gdt/
├── scripts/
│   ├── core/
│   │   ├── global_data.gd                    # ✅ 阶段0 - 延迟加载
│   │   ├── data_structures/
│   │   │   └── memory_adventure_teams.gd     # ✅ 阶段1 - 添加to_sqlite_dict()
│   │   └── managers/
│   │       ├── memory_data_manager.gd        # ✅ 阶段1 - 数据同步
│   │       ├── save_game_manager.gd          # ✅ 阶段2 - 无限槽位+覆盖保存
│   │       └── scene_manager.gd              # ✅ 阶段0 - 加载检查
│   ├── player/
│   │   └── player.gd                         # ✅ 阶段5 - ESC键退出对话框
│   └── ui/
│       ├── hud/
│       │   └── welcome_ui.gd                 # ✅ 阶段0 - 移除独立DB
│       ├── dev_console.gd                    # ✅ 添加保存测试命令
│       └── menus/
│           ├── start_menu.gd                 # ✅ 阶段5 - 集成存档列表
│           ├── main_menu.gd                  # ✅ 阶段5 - 集成退出确认
│           ├── save_load_panel.gd            # ✅ 阶段3 - 存档列表(双模式)
│           └── quit_confirm_dialog.gd        # ✅ 阶段4 - 退出确认(5按钮)
├── scenes/
│   └── ui/
│       └── menus/
│           ├── SaveLoadPanel.tscn            # ✅ 阶段3
│           └── QuitConfirmDialog.tscn        # ✅ 阶段4
├── sqlite_database/
│   ├── game_trade.db                         # 原始数据库(只读)
│   └── saves/                                # 存档目录(已gitignore)
│       └── game_save_*.db                    # 时间戳存档文件
└── docs/
    └── SAVE_SYSTEM_IMPLEMENTATION-260114.md  # 本文档

API参考

GlobalData

方法 参数 返回值 描述
is_game_loaded() - bool 检查游戏是否已加载
get_current_db_path() - String 获取当前数据库路径
start_new_game() - bool 从原始数据库开始新游戏
load_database_from_file(path) String bool 从指定路径加载数据库
unload_game() - void 卸载游戏,清理所有数据

SaveGameManager

方法 参数 返回值 描述
quick_save_to_new_file() - Dictionary 创建新存档,返回保存信息
save_to_file(path) String Dictionary 覆盖指定存档
get_all_saves() - Array 获取所有存档列表(按时间倒序)
get_latest_save() - Dictionary 获取最新存档信息
delete_save_file(path) String bool 删除指定存档
get_save_count() - int 获取存档数量

数据流

启动流程

游戏启动
    │
    ▼
GlobalData._ready()
    │ (不加载数据库)
    ▼
StartMenu 显示
    │
    ├─→ [开始游戏] ─→ GlobalData.start_new_game()
    │                      │
    │                      ▼
    │               加载原始 game_trade.db
    │                      │
    │                      ▼
    │               初始化所有管理器
    │                      │
    │                      ▼
    │               SceneManager.quick_switch_to_main_game()
    │
    └─→ [加载游戏] ─→ 显示 SaveLoadPanel (加载模式)
                           │
                           ▼
                    用户选择存档
                           │
                           ▼
                    GlobalData.load_database_from_file()

保存流程

用户按ESC / 手动保存
    │
    ▼
QuitConfirmDialog 显示
    │
    ├─→ [保存] / [保存并退出] / [保存并返回主界面]
    │         │
    │         ▼
    │   显示 SaveLoadPanel (保存模式)
    │         │
    │         ├─→ [创建新存档] ─→ quick_save_to_new_file()
    │         │
    │         └─→ [覆盖存档] ─→ save_to_file(path)
    │                   │
    │                   ▼
    │           MemoryDataManager.sync_all_dirty_to_database()
    │                   │
    │                   ▼
    │           复制数据库到存档目录
    │                   │
    │                   ▼
    │           根据选择: 继续游戏 / 退出 / 返回主界面
    │
    ├─→ [返回主界面] ─→ SceneManager.quick_switch_to_start_menu()
    │
    └─→ [返回游戏] ─→ 关闭对话框

后续计划

自动保存功能 (后续实现)

  • 定时自动保存(可配置间隔)
  • 保存到专门的自动存档槽位
  • 最多保留N个自动存档

存档管理增强 (后续实现)

  • 存档重命名
  • 存档导出/导入
  • 存档压缩
  • 云存档同步

变更日志

日期 版本 变更内容
2026-01-14 v0.6.0 阶段0完成:延迟加载重构
2026-01-14 v0.7.0 阶段1完成:数据同步功能
2026-01-14 v1.0.0 阶段2-5完成:无限槽位保存、存档列表UI、退出确认、主菜单整合
2026-01-14 v1.1.0 优化:存档目录改为项目目录、ESC菜单增加返回主界面选项
2026-02-03 v1.2.0 新增 force_reset 机制、user:// 目录迁移、WAL Checkpoint

2026-02-03 更新

新增功能

  1. force_reset 机制start_new_game() 现在会强制重置 user:// 中的数据库,确保"开始新游戏"真正从头开始
  2. user:// 目录迁移:存档从 res:// 迁移到 user://,解决导出后无法写入问题
  3. WAL Checkpoint:保存前执行 PRAGMA wal_checkpoint(TRUNCATE) 确保数据完整性

API 变更

# GlobalData - 新增 force_reset 参数
func start_new_game() -> bool:
    return load_database_from_file(ORIGINAL_DATABASE_PATH, true)  # force_reset=true

func load_database_from_file(db_path: String, force_reset: bool = false) -> bool