添加视觉小说引擎

This commit is contained in:
milimoe 2025-02-12 23:58:41 +08:00
parent ca1247cbf5
commit e623a1809c
Signed by: milimoe
GPG Key ID: 05D280912DA6C69E
5 changed files with 373 additions and 1 deletions

181
Api/Utility/NovelConfig.cs Normal file
View File

@ -0,0 +1,181 @@
using Milimoe.FunGame.Core.Library.Constant;
using Milimoe.FunGame.Core.Model;
namespace Milimoe.FunGame.Core.Api.Utility
{
/// <summary>
/// 视觉小说文本配置器<para/>
/// <para/>文件会保存为:程序目录/<see cref="RootPath"/>(通常是 novels)/<see cref="NovelName"/>/<see cref="FileName"/>.json
/// </summary>
/// <remarks>
/// 新建一个配置文件,文件会保存为:程序目录/<see cref="RootPath"/>(通常是 novels)/<paramref name="novel_name"/>/<paramref name="file_name"/>.json
/// </remarks>
/// <param name="novel_name"></param>
/// <param name="file_name"></param>
public class NovelConfig(string novel_name, string file_name) : Dictionary<string, NovelNode>
{
/// <summary>
/// 配置文件存放的根目录
/// </summary>
public static string RootPath { get; set; } = "novels";
/// <summary>
/// 模组的名称
/// </summary>
public string NovelName { get; set; } = novel_name;
/// <summary>
/// 配置文件的名称(后缀将是.json
/// </summary>
public string FileName { get; set; } = file_name;
/// <summary>
/// 使用索引器给指定key赋值不存在key会新增
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public new NovelNode this[string key]
{
get => base[key];
set
{
if (value != null) Add(key, value);
}
}
/// <summary>
/// 获取指定key的value
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public NovelNode? Get(string key)
{
if (TryGetValue(key, out NovelNode? value) && value != null)
{
return value;
}
return null;
}
/// <summary>
/// 添加一个配置如果已存在key会覆盖
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public new void Add(string key, NovelNode value)
{
if (value != null)
{
if (TryGetValue(key, out _)) base[key] = value;
else base.Add(key, value);
}
}
/// <summary>
/// 从配置文件中读取配置。
/// </summary>
/// <param name="Predicates">传入定义好的条件字典</param>
public void LoadConfig(Dictionary<string, Func<bool>>? Predicates = null)
{
string dpath = $@"{AppDomain.CurrentDomain.BaseDirectory}{RootPath}/{NovelName}";
string fpath = $@"{dpath}/{FileName}.json";
if (Directory.Exists(dpath) && File.Exists(fpath))
{
string json = File.ReadAllText(fpath, General.DefaultEncoding);
Dictionary<string, NovelNode> dict = NetworkUtility.JsonDeserialize<Dictionary<string, NovelNode>>(json) ?? [];
Clear();
foreach (string key in dict.Keys)
{
NovelNode obj = dict[key];
base.Add(key, obj);
if (obj.Values.TryGetValue(nameof(NovelNode.NextNodes), out object? value) && value is List<string> nextKeys)
{
foreach (string nextKey in nextKeys)
{
if (dict.TryGetValue(nextKey, out NovelNode? node) && node != null)
{
obj.NextNodes.Add(node);
}
}
}
if (Predicates != null)
{
if (obj.Values.TryGetValue(nameof(NovelNode.AndPredicates), out object? value2) && value2 is List<string> aps)
{
foreach (string ap in aps)
{
if (Predicates.TryGetValue(ap, out Func<bool>? value3) && value3 != null)
{
obj.AndPredicates[ap] = value3;
}
}
}
if (obj.Values.TryGetValue(nameof(NovelNode.OrPredicates), out value2) && value2 is List<string> ops)
{
foreach (string op in ops)
{
if (Predicates.TryGetValue(op, out Func<bool>? value3) && value3 != null)
{
obj.OrPredicates[op] = value3;
}
}
}
}
foreach (NovelOption option in obj.Options)
{
if (option.Values.TryGetValue(nameof(NovelOption.Targets), out object? value2) && value2 is List<string> targets)
{
foreach (string targetKey in targets)
{
if (dict.TryGetValue(targetKey, out NovelNode? node) && node != null)
{
option.Targets.Add(node);
}
}
}
if (Predicates != null)
{
if (option.Values.TryGetValue(nameof(NovelNode.AndPredicates), out object? value3) && value3 is List<string> aps)
{
foreach (string ap in aps)
{
if (Predicates.TryGetValue(ap, out Func<bool>? value4) && value4 != null)
{
option.AndPredicates[ap] = value4;
}
}
}
if (option.Values.TryGetValue(nameof(NovelNode.OrPredicates), out value3) && value3 is List<string> ops)
{
foreach (string op in ops)
{
if (Predicates.TryGetValue(op, out Func<bool>? value4) && value4 != null)
{
option.OrPredicates[op] = value4;
}
}
}
}
}
}
}
}
/// <summary>
/// 将配置保存到配置文件。调用此方法会覆盖原有的.json请注意备份
/// </summary>
public void SaveConfig()
{
string json = NetworkUtility.JsonSerialize((Dictionary<string, NovelNode>)this);
string dpath = $@"{AppDomain.CurrentDomain.BaseDirectory}{RootPath}/{NovelName}";
string fpath = $@"{dpath}/{FileName}.json";
if (!Directory.Exists(dpath))
{
Directory.CreateDirectory(dpath);
}
using StreamWriter writer = new(fpath, false, General.DefaultEncoding);
writer.WriteLine(json);
writer.Flush();
}
}
}

View File

@ -0,0 +1,85 @@
using System.Text.Json;
using Milimoe.FunGame.Core.Api.Utility;
using Milimoe.FunGame.Core.Library.Common.Architecture;
using Milimoe.FunGame.Core.Model;
namespace Milimoe.FunGame.Core.Library.Common.JsonConverter
{
public class NovelNodeConverter : BaseEntityConverter<NovelNode>
{
public override NovelNode NewInstance()
{
return new NovelNode();
}
public override void ReadPropertyName(ref Utf8JsonReader reader, string propertyName, JsonSerializerOptions options, ref NovelNode result, Dictionary<string, object> convertingContext)
{
switch (propertyName)
{
case nameof(NovelNode.Key):
result.Key = reader.GetString() ?? "";
break;
case nameof(NovelNode.Priority):
result.Priority = reader.GetInt32();
break;
case nameof(NovelNode.Previous):
result.Values[nameof(NovelNode.Previous)] = reader.GetString() ?? "";
break;
case nameof(NovelNode.NextNodes):
result.Values[nameof(NovelNode.NextNodes)] = NetworkUtility.JsonDeserialize<List<string>>(ref reader, options) ?? [];
break;
case nameof(NovelNode.Options):
result.Options = NetworkUtility.JsonDeserialize<List<NovelOption>>(ref reader, options) ?? [];
break;
case nameof(NovelNode.Name):
result.Name = reader.GetString() ?? "";
break;
case nameof(NovelNode.Name2):
result.Name2 = reader.GetString() ?? "";
break;
case nameof(NovelNode.Content):
result.Content = reader.GetString() ?? "";
break;
case nameof(NovelNode.PortraitImagePath):
result.PortraitImagePath = reader.GetString() ?? "";
break;
case nameof(NovelNode.AndPredicates):
result.Values[nameof(NovelNode.AndPredicates)] = NetworkUtility.JsonDeserialize<List<string>>(ref reader, options) ?? [];
break;
case nameof(NovelNode.OrPredicates):
result.Values[nameof(NovelNode.OrPredicates)] = NetworkUtility.JsonDeserialize<List<string>>(ref reader, options) ?? [];
break;
}
}
public override void Write(Utf8JsonWriter writer, NovelNode value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString(nameof(NovelNode.Key), value.Key);
writer.WriteNumber(nameof(NovelNode.Priority), value.Priority);
if (value.Previous != null) writer.WriteString(nameof(NovelNode.Previous), value.Previous.Key);
writer.WritePropertyName(nameof(NovelNode.NextNodes));
JsonSerializer.Serialize(writer, value.NextNodes.Select(n => n.Key).ToList(), options);
if (value.Options.Count > 0)
{
writer.WritePropertyName(nameof(NovelNode.Options));
JsonSerializer.Serialize(writer, value.Options, options);
}
writer.WriteString(nameof(NovelNode.Name), value.Name);
if (value.Name2 != "") writer.WriteString(nameof(NovelNode.Name2), value.Name2);
writer.WriteString(nameof(NovelNode.Content), value.Content);
if (value.PortraitImagePath != "") writer.WriteString(nameof(NovelNode.PortraitImagePath), value.PortraitImagePath);
if (value.AndPredicates.Count > 0)
{
writer.WritePropertyName(nameof(NovelNode.AndPredicates));
JsonSerializer.Serialize(writer, value.AndPredicates.Keys.ToList(), options);
}
if (value.OrPredicates.Count > 0)
{
writer.WritePropertyName(nameof(NovelNode.OrPredicates));
JsonSerializer.Serialize(writer, value.OrPredicates.Keys.ToList(), options);
}
writer.WriteEndObject();
}
}
}

View File

@ -0,0 +1,57 @@
using System.Text.Json;
using Milimoe.FunGame.Core.Api.Utility;
using Milimoe.FunGame.Core.Library.Common.Architecture;
using Milimoe.FunGame.Core.Model;
namespace Milimoe.FunGame.Core.Library.Common.JsonConverter
{
public class NovelOptionConverter : BaseEntityConverter<NovelOption>
{
public override NovelOption NewInstance()
{
return new NovelOption();
}
public override void ReadPropertyName(ref Utf8JsonReader reader, string propertyName, JsonSerializerOptions options, ref NovelOption result, Dictionary<string, object> convertingContext)
{
switch (propertyName)
{
case nameof(NovelOption.Key):
result.Key = reader.GetString() ?? "";
break;
case nameof(NovelOption.Name):
result.Name = reader.GetString() ?? "";
break;
case nameof(NovelOption.Targets):
result.Values[nameof(NovelOption.Targets)] = NetworkUtility.JsonDeserialize<List<string>>(ref reader, options) ?? [];
break;
case nameof(NovelNode.AndPredicates):
result.Values[nameof(NovelNode.AndPredicates)] = NetworkUtility.JsonDeserialize<List<string>>(ref reader, options) ?? [];
break;
case nameof(NovelNode.OrPredicates):
result.Values[nameof(NovelNode.OrPredicates)] = NetworkUtility.JsonDeserialize<List<string>>(ref reader, options) ?? [];
break;
}
}
public override void Write(Utf8JsonWriter writer, NovelOption value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString(nameof(NovelOption.Key), value.Key);
writer.WriteString(nameof(NovelOption.Name), value.Name);
writer.WritePropertyName(nameof(NovelOption.Targets));
JsonSerializer.Serialize(writer, value.Targets.Select(n => n.Key).ToList(), options);
if (value.AndPredicates.Count > 0)
{
writer.WritePropertyName(nameof(NovelNode.AndPredicates));
JsonSerializer.Serialize(writer, value.AndPredicates.Keys.ToList(), options);
}
if (value.OrPredicates.Count > 0)
{
writer.WritePropertyName(nameof(NovelNode.OrPredicates));
JsonSerializer.Serialize(writer, value.OrPredicates.Keys.ToList(), options);
}
writer.WriteEndObject();
}
}
}

48
Model/NovelNode.cs Normal file
View File

@ -0,0 +1,48 @@
namespace Milimoe.FunGame.Core.Model
{
public class NovelNode
{
public string Key { get; set; } = "";
public int Priority { get; set; } = 0;
public NovelNode? Previous { get; set; } = null;
public List<NovelNode> NextNodes { get; set; } = [];
public NovelNode? Next => NextNodes.OrderByDescending(n => n.Priority).Where(n => n.ShowNode).FirstOrDefault();
public List<NovelOption> Options { get; set; } = [];
public List<NovelOption> AvailableOptions => [.. Options.Where(o => o.ShowOption)];
public string Name { get; set; } = "";
public string Name2 { get; set; } = "";
public string Content { get; set; } = "";
public string PortraitImagePath { get; set; } = "";
public Dictionary<string, Func<bool>> AndPredicates { get; set; } = [];
public Dictionary<string, Func<bool>> OrPredicates { get; set; } = [];
public bool ShowNode
{
get
{
bool andResult = AndPredicates.Values.All(predicate => predicate());
bool orResult = OrPredicates.Values.Any(predicate => predicate());
return andResult && (OrPredicates.Count == 0 || orResult);
}
}
internal Dictionary<string, object> Values { get; set; } = [];
}
public class NovelOption
{
public string Key { get; set; } = "";
public string Name { get; set; } = "";
public List<NovelNode> Targets { get; set; } = [];
public Dictionary<string, Func<bool>> AndPredicates { get; set; } = [];
public Dictionary<string, Func<bool>> OrPredicates { get; set; } = [];
public bool ShowOption
{
get
{
bool andResult = AndPredicates.Values.All(predicate => predicate());
bool orResult = OrPredicates.Values.Any(predicate => predicate());
return andResult && (OrPredicates.Count == 0 || orResult);
}
}
internal Dictionary<string, object> Values { get; set; } = [];
}
}

View File

@ -21,7 +21,8 @@ namespace Milimoe.FunGame.Core.Service
ReferenceHandler = ReferenceHandler.IgnoreCycles,
Converters = { new DateTimeConverter(), new DataTableConverter(), new DataSetConverter(), new UserConverter(), new RoomConverter(),
new CharacterConverter(), new MagicResistanceConverter(), new EquipSlotConverter(), new SkillConverter(), new EffectConverter(), new ItemConverter(),
new InventoryConverter(), new NormalAttackConverter(), new ClubConverter(), new GoodsConverter(), new StoreConverter()
new InventoryConverter(), new NormalAttackConverter(), new ClubConverter(), new GoodsConverter(), new StoreConverter(),
new NovelOptionConverter(), new NovelNodeConverter()
}
};