改动说明 - 实现内容加载器并支持 res:///user:// 路径解析与 JSON 枚举解析:scripts/Core/ContentRegistry.cs - 新增 .tres 资源定义接口与样例资源类(学科定义):scripts/Core/ContentResources.cs - 默认注册资源/JSON 数据源,启动时自动合并进内容库:scripts/Core/GameSession.cs - 样例 .tres 与 JSON 内容定义:resources/definitions/discipline_biology.tres, resources/definitions/disciplines.json, resources/definitions/archetypes.json - 当前 .tres 走“扁平字段 + RuleIds”,数值型 Modifier 更适合先用 JSON 落地,后续可以把更多字段迁入资源类。 - JSON 采用与 Models 定义一致的结构(DefinitionHeader/LocalizedText/ModifierBundle),便于后续扩展。
300 lines
6.1 KiB
C#
300 lines
6.1 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Text.Json;
|
||
using System.Text.Json.Serialization;
|
||
using Godot;
|
||
using Models;
|
||
|
||
namespace Core;
|
||
|
||
/// <summary>
|
||
/// 内容加载与合并(基础 + Mod)
|
||
/// 设计说明:
|
||
/// 1) 通过 IContentSource 抽象读取来源,支持 res:// 与 user://mods。
|
||
/// 2) ContentRegistry 负责合并,同 Id 以后加载覆盖先加载。
|
||
/// 注意事项:
|
||
/// - 真实加载逻辑应避免在主线程做大规模 IO。
|
||
/// 未来扩展:
|
||
/// - 可加入“补丁合并策略”(例如列表合并/字段覆盖)。
|
||
/// </summary>
|
||
public interface IContentSource
|
||
{
|
||
int Priority { get; }
|
||
IEnumerable<T> LoadAll<T>() where T : class;
|
||
}
|
||
|
||
public enum ContentMergeMode
|
||
{
|
||
Override,
|
||
KeepFirst
|
||
}
|
||
|
||
public sealed class ContentRegistry
|
||
{
|
||
private readonly List<IContentSource> _sources = new();
|
||
public ContentMergeMode MergeMode { get; set; } = ContentMergeMode.Override;
|
||
|
||
public void RegisterSource(IContentSource source)
|
||
{
|
||
_sources.Add(source);
|
||
_sources.Sort((a, b) => a.Priority.CompareTo(b.Priority));
|
||
}
|
||
|
||
public GameContentDatabase BuildDatabase()
|
||
{
|
||
var db = new GameContentDatabase();
|
||
Merge(db.Disciplines, LoadAll<DisciplineDefinition>(), d => d.Header.Id);
|
||
Merge(db.Archetypes, LoadAll<ArchetypeDefinition>(), d => d.Header.Id);
|
||
Merge(db.Roles, LoadAll<RoleDefinition>(), d => d.Header.Id);
|
||
Merge(db.Traits, LoadAll<TraitDefinition>(), d => d.Header.Id);
|
||
Merge(db.Tasks, LoadAll<TaskDefinition>(), d => d.Header.Id);
|
||
Merge(db.Items, LoadAll<ItemDefinition>(), d => d.Header.Id);
|
||
Merge(db.Papers, LoadAll<PaperDefinition>(), d => d.Header.Id);
|
||
Merge(db.RoguelitePerks, LoadAll<RoguelitePerkDefinition>(), d => d.Header.Id);
|
||
return db;
|
||
}
|
||
|
||
private IEnumerable<T> LoadAll<T>() where T : class
|
||
{
|
||
foreach (var source in _sources)
|
||
{
|
||
foreach (var item in source.LoadAll<T>())
|
||
{
|
||
yield return item;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void Merge<T>(Dictionary<string, T> target, IEnumerable<T> items, Func<T, string> idSelector) where T : class
|
||
{
|
||
foreach (var item in items)
|
||
{
|
||
var id = idSelector(item);
|
||
if (string.IsNullOrWhiteSpace(id))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (target.ContainsKey(id))
|
||
{
|
||
if (MergeMode == ContentMergeMode.Override)
|
||
{
|
||
target[id] = item;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
target[id] = item;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 资源读取示例:res:// 中的 .tres/.res
|
||
/// 这里只给出接口骨架,具体解析留给后续实现。
|
||
/// </summary>
|
||
public sealed class ResourceContentSource : IContentSource
|
||
{
|
||
public int Priority { get; }
|
||
public List<string> ResourcePaths { get; } = new();
|
||
|
||
public ResourceContentSource(int priority)
|
||
{
|
||
Priority = priority;
|
||
}
|
||
|
||
public IEnumerable<T> LoadAll<T>() where T : class
|
||
{
|
||
foreach (var path in ResourcePaths)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(path))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var resource = ResourceLoader.Load<Resource>(path);
|
||
if (resource == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
foreach (var item in ExtractResources<T>(resource))
|
||
{
|
||
yield return item;
|
||
}
|
||
}
|
||
}
|
||
|
||
private IEnumerable<T> ExtractResources<T>(Resource resource) where T : class
|
||
{
|
||
if (resource is IContentResource content)
|
||
{
|
||
if (content.GetDefinitionType() == typeof(T))
|
||
{
|
||
if (content.ToDefinition() is T typed)
|
||
{
|
||
yield return typed;
|
||
}
|
||
}
|
||
|
||
yield break;
|
||
}
|
||
|
||
if (resource is IContentResourceCollection collection)
|
||
{
|
||
foreach (var item in collection.GetItems())
|
||
{
|
||
if (item.GetDefinitionType() == typeof(T))
|
||
{
|
||
if (item.ToDefinition() is T typed)
|
||
{
|
||
yield return typed;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 资源读取示例:json/yaml/自定义格式(Mod 友好)
|
||
/// </summary>
|
||
public sealed class JsonContentSource : IContentSource
|
||
{
|
||
public int Priority { get; }
|
||
public List<string> DataPaths { get; } = new();
|
||
private readonly JsonSerializerOptions _options;
|
||
|
||
public JsonContentSource(int priority)
|
||
{
|
||
Priority = priority;
|
||
_options = new JsonSerializerOptions
|
||
{
|
||
PropertyNameCaseInsensitive = true
|
||
};
|
||
_options.Converters.Add(new JsonStringEnumConverter());
|
||
}
|
||
|
||
public IEnumerable<T> LoadAll<T>() where T : class
|
||
{
|
||
foreach (var path in DataPaths)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(path))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var resolvedPath = ResolvePath(path);
|
||
if (!File.Exists(resolvedPath))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var json = File.ReadAllText(resolvedPath);
|
||
if (string.IsNullOrWhiteSpace(json))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (TryDeserializeList(json, out List<T> list))
|
||
{
|
||
foreach (var item in list)
|
||
{
|
||
yield return item;
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (TryDeserializeEnvelope(json, out JsonContentEnvelope<T> envelope))
|
||
{
|
||
if (envelope.Items != null)
|
||
{
|
||
foreach (var item in envelope.Items)
|
||
{
|
||
yield return item;
|
||
}
|
||
}
|
||
else if (envelope.Item != null)
|
||
{
|
||
yield return envelope.Item;
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
if (TryDeserializeSingle(json, out T single))
|
||
{
|
||
yield return single;
|
||
}
|
||
}
|
||
}
|
||
|
||
private string ResolvePath(string path)
|
||
{
|
||
if (path.StartsWith("res://") || path.StartsWith("user://"))
|
||
{
|
||
return ProjectSettings.GlobalizePath(path);
|
||
}
|
||
|
||
return path;
|
||
}
|
||
|
||
private bool TryDeserializeList<T>(string json, out List<T> list) where T : class
|
||
{
|
||
try
|
||
{
|
||
list = JsonSerializer.Deserialize<List<T>>(json, _options);
|
||
return list != null;
|
||
}
|
||
catch
|
||
{
|
||
list = null;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private bool TryDeserializeSingle<T>(string json, out T item) where T : class
|
||
{
|
||
try
|
||
{
|
||
item = JsonSerializer.Deserialize<T>(json, _options);
|
||
return item != null;
|
||
}
|
||
catch
|
||
{
|
||
item = null;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private bool TryDeserializeEnvelope<T>(string json, out JsonContentEnvelope<T> envelope) where T : class
|
||
{
|
||
try
|
||
{
|
||
envelope = JsonSerializer.Deserialize<JsonContentEnvelope<T>>(json, _options);
|
||
if (envelope == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return envelope.Items != null || envelope.Item != null;
|
||
}
|
||
catch
|
||
{
|
||
envelope = null;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private sealed class JsonContentEnvelope<T> where T : class
|
||
{
|
||
public string Type { get; set; }
|
||
public List<T> Items { get; set; }
|
||
public T Item { get; set; }
|
||
}
|
||
}
|
||
|