8.30
This commit is contained in:
@@ -35,6 +35,7 @@ namespace SepCore.CustomComponent
|
||||
"tbsoundconfig",
|
||||
"tbuiformconfig",
|
||||
"tbuisoundconfig",
|
||||
"tbformattext"
|
||||
};
|
||||
|
||||
private sealed class TableAccessor
|
||||
@@ -44,24 +45,88 @@ namespace SepCore.CustomComponent
|
||||
}
|
||||
|
||||
private static readonly Dictionary<Type, TableAccessor> TableAccessors =
|
||||
new Dictionary<Type, TableAccessor>
|
||||
new()
|
||||
{
|
||||
{ typeof(CharacterConfig), Accessor(tables => id => tables.TbCharacterConfig.GetOrDefault(id), tables => tables.TbCharacterConfig.DataList) },
|
||||
{ typeof(BattleActionConfig), Accessor(tables => id => tables.TbBattleActionConfig.GetOrDefault(id), tables => tables.TbBattleActionConfig.DataList) },
|
||||
{ typeof(ThreatLevelConfig), Accessor(tables => id => tables.TbThreatLevelConfig.GetOrDefault(id), tables => tables.TbThreatLevelConfig.DataList) },
|
||||
{ typeof(EnemyConfig), Accessor(tables => id => tables.TbEnemyConfig.GetOrDefault(id), tables => tables.TbEnemyConfig.DataList) },
|
||||
{ typeof(EnemyPartyConfig), Accessor(tables => id => tables.TbEnemyPartyConfig.GetOrDefault(id), tables => tables.TbEnemyPartyConfig.DataList) },
|
||||
{ typeof(ItemConfig), Accessor(tables => id => tables.TbItemConfig.GetOrDefault(id), tables => tables.TbItemConfig.DataList) },
|
||||
{ typeof(ResourcePointConfig), Accessor(tables => id => tables.TbResourcePointConfig.GetOrDefault(id), tables => tables.TbResourcePointConfig.DataList) },
|
||||
{ typeof(EnemyDropConfig), Accessor(tables => id => tables.TbEnemyDropConfig.GetOrDefault(id), tables => tables.TbEnemyDropConfig.DataList) },
|
||||
{ typeof(RarityConfig), Accessor(tables => id => tables.TbRarityConfig.GetOrDefault((Rarity)id), tables => tables.TbRarityConfig.DataList) },
|
||||
{ typeof(DifficultyConfig), Accessor(tables => id => tables.TbDifficultyConfig.GetOrDefault((DifficultyTier)id), tables => tables.TbDifficultyConfig.DataList) },
|
||||
{ typeof(EntityConfig), Accessor(tables => id => tables.TbEntityConfig.GetOrDefault(id), tables => tables.TbEntityConfig.DataList) },
|
||||
{ typeof(MusicConfig), Accessor(tables => id => tables.TbMusicConfig.GetOrDefault(id), tables => tables.TbMusicConfig.DataList) },
|
||||
{ typeof(SceneConfig), Accessor(tables => id => tables.TbSceneConfig.GetOrDefault(id), tables => tables.TbSceneConfig.DataList) },
|
||||
{ typeof(SoundConfig), Accessor(tables => id => tables.TbSoundConfig.GetOrDefault(id), tables => tables.TbSoundConfig.DataList) },
|
||||
{ typeof(UIFormConfig), Accessor(tables => id => tables.TbUIFormConfig.GetOrDefault(id), tables => tables.TbUIFormConfig.DataList) },
|
||||
{ typeof(UISoundConfig), Accessor(tables => id => tables.TbUISoundConfig.GetOrDefault(id), tables => tables.TbUISoundConfig.DataList) },
|
||||
{
|
||||
typeof(CharacterConfig),
|
||||
Accessor(tables => id => tables.TbCharacterConfig.GetOrDefault(id),
|
||||
tables => tables.TbCharacterConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(BattleActionConfig),
|
||||
Accessor(tables => id => tables.TbBattleActionConfig.GetOrDefault(id),
|
||||
tables => tables.TbBattleActionConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(ThreatLevelConfig),
|
||||
Accessor(tables => id => tables.TbThreatLevelConfig.GetOrDefault(id),
|
||||
tables => tables.TbThreatLevelConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(EnemyConfig),
|
||||
Accessor(tables => id => tables.TbEnemyConfig.GetOrDefault(id),
|
||||
tables => tables.TbEnemyConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(EnemyPartyConfig),
|
||||
Accessor(tables => id => tables.TbEnemyPartyConfig.GetOrDefault(id),
|
||||
tables => tables.TbEnemyPartyConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(ItemConfig),
|
||||
Accessor(tables => id => tables.TbItemConfig.GetOrDefault(id),
|
||||
tables => tables.TbItemConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(ResourcePointConfig),
|
||||
Accessor(tables => id => tables.TbResourcePointConfig.GetOrDefault(id),
|
||||
tables => tables.TbResourcePointConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(EnemyDropConfig),
|
||||
Accessor(tables => id => tables.TbEnemyDropConfig.GetOrDefault(id),
|
||||
tables => tables.TbEnemyDropConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(RarityConfig),
|
||||
Accessor(tables => id => tables.TbRarityConfig.GetOrDefault((Rarity)id),
|
||||
tables => tables.TbRarityConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(DifficultyConfig),
|
||||
Accessor(tables => id => tables.TbDifficultyConfig.GetOrDefault((DifficultyTier)id),
|
||||
tables => tables.TbDifficultyConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(EntityConfig),
|
||||
Accessor(tables => id => tables.TbEntityConfig.GetOrDefault(id),
|
||||
tables => tables.TbEntityConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(MusicConfig),
|
||||
Accessor(tables => id => tables.TbMusicConfig.GetOrDefault(id),
|
||||
tables => tables.TbMusicConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(SceneConfig),
|
||||
Accessor(tables => id => tables.TbSceneConfig.GetOrDefault((SceneType)id),
|
||||
tables => tables.TbSceneConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(SoundConfig),
|
||||
Accessor(tables => id => tables.TbSoundConfig.GetOrDefault(id),
|
||||
tables => tables.TbSoundConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(UIFormConfig),
|
||||
Accessor(tables => id => tables.TbUIFormConfig.GetOrDefault((UIFormType)id),
|
||||
tables => tables.TbUIFormConfig.DataList)
|
||||
},
|
||||
{
|
||||
typeof(UISoundConfig),
|
||||
Accessor(tables => id => tables.TbUISoundConfig.GetOrDefault(id),
|
||||
tables => tables.TbUISoundConfig.DataList)
|
||||
},
|
||||
};
|
||||
|
||||
private static TableAccessor Accessor<TValue>(Func<Tables, Func<int, object>> rowGetterFactory,
|
||||
@@ -82,13 +147,15 @@ namespace SepCore.CustomComponent
|
||||
/// <summary>
|
||||
/// 获取 Luban 数据表管理器,加载完成后可用。
|
||||
/// </summary>
|
||||
public SepCore.Definition.Tables Tables { get; private set; }
|
||||
public Tables Tables { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取数据表是否已加载完成。
|
||||
/// </summary>
|
||||
public bool IsReady => Tables != null;
|
||||
|
||||
public TbGlobalConfig Global => Tables.TbGlobalConfig;
|
||||
|
||||
/// <summary>
|
||||
/// 按 id 获取数据行,未找到时返回 null。
|
||||
/// </summary>
|
||||
@@ -193,7 +260,8 @@ namespace SepCore.CustomComponent
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoadTableFailure(string assetName, LoadResourceStatus status, string errorMessage, object userData)
|
||||
private void OnLoadTableFailure(string assetName, LoadResourceStatus status, string errorMessage,
|
||||
object userData)
|
||||
{
|
||||
Log.Error("Can not load data table '{0}' from '{1}' with error message '{2}'.", (string)userData, assetName,
|
||||
errorMessage);
|
||||
@@ -208,4 +276,4 @@ namespace SepCore.CustomComponent
|
||||
return GameFramework.Utility.Text.Format("Assets/GameMain/DataTables/{0}.bytes", tableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8247612c245cc47be82009199a823d68
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SepCore.Definition;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace SepCore.CustomComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// 存档组件。
|
||||
/// 负责游戏存档的读取与写入,数据结构见 Docs/Tech/02_SaveData.md。
|
||||
/// 存档只包含局外状态与已结束单局的结算记录;进行中的单局使用临时状态,不写入存档。
|
||||
/// </summary>
|
||||
public class SaveComponent : GameFrameworkComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// 存档文件名。
|
||||
/// </summary>
|
||||
[SerializeField] private string _fileName = "save.json";
|
||||
|
||||
private SaveData _data = null;
|
||||
private bool _hasSave = false;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前内存中的存档数据;未加载或未创建时为 null。
|
||||
/// </summary>
|
||||
public SaveData Data => _data;
|
||||
|
||||
/// <summary>
|
||||
/// 获取磁盘上是否已存在存档文件。
|
||||
/// </summary>
|
||||
public bool HasSave => _hasSave;
|
||||
|
||||
/// <summary>
|
||||
/// 获取存档数据是否可用。
|
||||
/// </summary>
|
||||
public bool IsReady => _data != null;
|
||||
|
||||
/// <summary>
|
||||
/// 获取存档文件的完整路径。
|
||||
/// </summary>
|
||||
public string SaveFilePath => GameFramework.Utility.Path.GetRegularPath(
|
||||
Path.Combine(Application.persistentDataPath, _fileName));
|
||||
|
||||
/// <summary>
|
||||
/// 从磁盘加载存档。
|
||||
/// 文件不存在时返回 true 且 HasSave 为 false;解析失败时返回 false。
|
||||
/// </summary>
|
||||
public bool Load()
|
||||
{
|
||||
string path = SaveFilePath;
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
_hasSave = false;
|
||||
_data = null;
|
||||
Log.Info("Save file '{0}' does not exist.", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
SaveData data = SaveData.FromJson(json);
|
||||
if (data == null)
|
||||
{
|
||||
Log.Error("Parse save file '{0}' failure.", path);
|
||||
_hasSave = true;
|
||||
_data = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data.version != SaveData.CurrentVersion)
|
||||
{
|
||||
Log.Warning("Save file '{0}' version '{1}' does not match current version '{2}'.",
|
||||
path, data.version, SaveData.CurrentVersion);
|
||||
}
|
||||
|
||||
_data = data;
|
||||
_hasSave = true;
|
||||
Log.Info("Load save file '{0}' OK.", path);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error("Load save file '{0}' failure with exception '{1}'.", path, exception);
|
||||
_hasSave = true;
|
||||
_data = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建新存档数据(不写盘)。
|
||||
/// 角色与初始装备来自配表:GlobalConfig.NewGameCharacterIds 与 CharacterConfig 的初始装备栏。
|
||||
/// </summary>
|
||||
public bool CreateNewGame()
|
||||
{
|
||||
if (!GameEntry.Luban.IsReady)
|
||||
{
|
||||
Log.Error("Can not create new game before Luban data tables are loaded.");
|
||||
return false;
|
||||
}
|
||||
|
||||
GlobalConfig global = GameEntry.Luban.Global.Data;
|
||||
SaveData data = new SaveData
|
||||
{
|
||||
version = SaveData.CurrentVersion,
|
||||
updatedAt = GetTimestamp(),
|
||||
mainWarehouse = CreateInitialWarehouse(),
|
||||
characters = new List<CharacterSave>(),
|
||||
loadout = new LoadoutSave(),
|
||||
runHistory = new List<RunRecord>(),
|
||||
};
|
||||
|
||||
foreach (int characterId in global.NewGameCharacterIds)
|
||||
{
|
||||
CharacterConfig config = GameEntry.Luban.Get<CharacterConfig>(characterId);
|
||||
int weaponItemId = config != null ? config.WeaponItemId : 0;
|
||||
int armorItemId = config != null ? config.ArmorItemId : 0;
|
||||
data.characters.Add(new CharacterSave(characterId, weaponItemId, armorItemId));
|
||||
}
|
||||
|
||||
data.loadout.Normalize();
|
||||
_data = data;
|
||||
_hasSave = false;
|
||||
Log.Info("Create new game save data OK.");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前存档数据写入磁盘。
|
||||
/// 写入前自动更新 updatedAt;使用临时文件替换,避免写入中断损坏存档。
|
||||
/// </summary>
|
||||
public bool Save()
|
||||
{
|
||||
if (_data == null)
|
||||
{
|
||||
Log.Warning("Can not save because save data does not exist.");
|
||||
return false;
|
||||
}
|
||||
|
||||
_data.updatedAt = GetTimestamp();
|
||||
string json = _data.ToJson();
|
||||
|
||||
string path = SaveFilePath;
|
||||
string tempPath = path + ".tmp";
|
||||
try
|
||||
{
|
||||
File.WriteAllText(tempPath, json);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
File.Move(tempPath, path);
|
||||
_hasSave = true;
|
||||
Log.Info("Save save file '{0}' OK.", path);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error("Save save file '{0}' failure with exception '{1}'.", path, exception);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ItemStack> CreateInitialWarehouse()
|
||||
{
|
||||
// 原型调试种子:覆盖全部物品类型与稀有度,用于筛选等功能开发。
|
||||
// 后续由配表或正式新存档规则替代。
|
||||
return new List<ItemStack>
|
||||
{
|
||||
new ItemStack(5001, 20), // 旧硬币 战利品/白
|
||||
new ItemStack(5002, 7), // 宝石碎片 战利品/绿
|
||||
new ItemStack(5003, 3), // 古代零件 战利品/蓝
|
||||
new ItemStack(5004, 1), // 精致核心 战利品/金
|
||||
new ItemStack(5005, 1), // 神秘遗物 战利品/红
|
||||
new ItemStack(5101, 1), // 训练剑 装备/白
|
||||
new ItemStack(5102, 1), // 法杖 装备/蓝
|
||||
new ItemStack(5201, 1), // 布甲 装备/绿
|
||||
new ItemStack(5202, 1), // 术士长袍 装备/金
|
||||
new ItemStack(5301, 5), // 恢复药 消耗品/白
|
||||
};
|
||||
}
|
||||
|
||||
private static long GetTimestamp()
|
||||
{
|
||||
return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e65ab245d7674846b4409282a610b3a9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user