supervisor-simulator/scripts/Core/ContentRegistry.cs
wjsjwr 39682f14fe 已落地 .tres/json 的内容定义入口,并把 ResourceContentSource/JsonContentSource 实现为可用的加载器;同时给出可运行的样例定义文件,默认会在 GameSession.CreateDefault() 中注册加载。
改动说明
  - 实现内容加载器并支持 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),便于后续扩展。
2026-01-01 01:59:57 +08:00

300 lines
6.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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; }
}
}