106 lines
4.5 KiB
C#
106 lines
4.5 KiB
C#
using System.Text.Json;
|
||
using Ananta.Server.Configuration;
|
||
|
||
namespace Ananta.Server.ClientData.Client4229938;
|
||
|
||
/// <summary>一条台词索引项。字段与 <c>DialogIndex.json</c> 的行一一对应。</summary>
|
||
internal sealed record DialogEntry(
|
||
uint Id,
|
||
string Message,
|
||
string Speaker,
|
||
uint CameraId,
|
||
int Type)
|
||
{
|
||
/// <summary>带镜头(cam != 0)= 这条是「过场对话」,会拉运镜;纯台词 cam 为 0。</summary>
|
||
internal bool IsCutscene => CameraId != 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 台詞索引(<c>Configs/DialogIndex.json</c>,58932 条,其中带镜头过场 1205 条)。
|
||
///
|
||
/// ★ 用途:GM 面板「剧情对话」子页用 <see cref="Search"/> 列出可播的台词,
|
||
/// 玩家/开发者选中一条后由服务端推 <c>SyncShowDialog</c>(64967628) 让客户端播出来。
|
||
/// type 分布(前几类):15=18941 / 14=16625 / 10=9503 / 13=6724 / 22=5258。
|
||
///
|
||
/// ⛑ 这份表只用于「列清单 + 取文案」;真正播什么由客户端按自己的 DialogConfig 演,
|
||
/// 服务端发的只是 id —— 所以表里查不到的 id 也照样能下发(客户端有自己的数据)。
|
||
/// </summary>
|
||
internal static class StoryDialogue4229938
|
||
{
|
||
private static readonly Lazy<(IReadOnlyList<DialogEntry> All, Dictionary<uint, DialogEntry> ById)> Cache = new(Load);
|
||
|
||
internal static IReadOnlyList<DialogEntry> All => Cache.Value.All;
|
||
|
||
internal static int Count => Cache.Value.All.Count;
|
||
|
||
internal static int CutsceneCount => Cache.Value.All.Count(x => x.IsCutscene);
|
||
|
||
internal static DialogEntry? Find(uint id)
|
||
=> Cache.Value.ById.TryGetValue(id, out var e) ? e : null;
|
||
|
||
/// <summary>按关键词(文案 / 说话人 / id 片段)搜索;<paramref name="cutsceneOnly"/> 可只看过场。</summary>
|
||
internal static IReadOnlyList<DialogEntry> Search(string? keyword, bool? cutsceneOnly, int limit)
|
||
{
|
||
IEnumerable<DialogEntry> q = Cache.Value.All;
|
||
|
||
if (cutsceneOnly is { } want)
|
||
q = q.Where(x => x.IsCutscene == want);
|
||
|
||
if (!string.IsNullOrWhiteSpace(keyword))
|
||
{
|
||
var k = keyword.Trim();
|
||
q = q.Where(x =>
|
||
x.Message.Contains(k, StringComparison.OrdinalIgnoreCase)
|
||
|| x.Speaker.Contains(k, StringComparison.OrdinalIgnoreCase)
|
||
|| x.Id.ToString().Contains(k, StringComparison.Ordinal));
|
||
}
|
||
|
||
return q.Take(Math.Clamp(limit, 1, 3000)).ToList();
|
||
}
|
||
|
||
private static (IReadOnlyList<DialogEntry>, Dictionary<uint, DialogEntry>) Load()
|
||
{
|
||
try
|
||
{
|
||
var root = PrivateServerConfigStore.ResolveProjectPath(
|
||
PrivateServerConfigStore.Current.Paths.ClientConfigs);
|
||
var path = Path.Combine(root, "DialogIndex.json");
|
||
if (!File.Exists(path))
|
||
return ([], new Dictionary<uint, DialogEntry>());
|
||
|
||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||
if (!doc.RootElement.TryGetProperty("dialogs", out var arr) || arr.ValueKind != JsonValueKind.Array)
|
||
return ([], new Dictionary<uint, DialogEntry>());
|
||
|
||
var list = new List<DialogEntry>(arr.GetArrayLength());
|
||
var byId = new Dictionary<uint, DialogEntry>(arr.GetArrayLength());
|
||
foreach (var item in arr.EnumerateArray())
|
||
{
|
||
static string Str(JsonElement e, string name)
|
||
=> e.TryGetProperty(name, out var n) && n.ValueKind == JsonValueKind.String
|
||
? n.GetString() ?? string.Empty
|
||
: string.Empty;
|
||
|
||
var entry = new DialogEntry(
|
||
item.TryGetProperty("id", out var pi) && pi.TryGetUInt32(out var id) ? id : 0u,
|
||
Str(item, "msg"),
|
||
Str(item, "spk"),
|
||
item.TryGetProperty("cam", out var pc) && pc.TryGetUInt32(out var cam) ? cam : 0u,
|
||
item.TryGetProperty("type", out var pt) && pt.TryGetInt32(out var tv) ? tv : 0);
|
||
|
||
list.Add(entry);
|
||
if (entry.Id != 0)
|
||
byId[entry.Id] = entry;
|
||
}
|
||
|
||
Console.WriteLine($"[STORY] 台词索引已载入 {list.Count} 条(带镜头过场 {list.Count(x => x.IsCutscene)} 条)");
|
||
return (list, byId);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"[STORY] 载入台词索引失败: {ex.GetType().Name}: {ex.Message}");
|
||
return ([], new Dictionary<uint, DialogEntry>());
|
||
}
|
||
}
|
||
}
|