diff --git a/Api/Utility/NovelConfig.cs b/Api/Utility/NovelConfig.cs
new file mode 100644
index 0000000..fa1b670
--- /dev/null
+++ b/Api/Utility/NovelConfig.cs
@@ -0,0 +1,181 @@
+using Milimoe.FunGame.Core.Library.Constant;
+using Milimoe.FunGame.Core.Model;
+
+namespace Milimoe.FunGame.Core.Api.Utility
+{
+ ///
+ /// 视觉小说文本配置器
+ /// 文件会保存为:程序目录/(通常是 novels)//.json
+ ///
+ ///
+ /// 新建一个配置文件,文件会保存为:程序目录/(通常是 novels)//.json
+ ///
+ ///
+ ///
+ public class NovelConfig(string novel_name, string file_name) : Dictionary
+ {
+ ///
+ /// 配置文件存放的根目录
+ ///
+ public static string RootPath { get; set; } = "novels";
+
+ ///
+ /// 模组的名称
+ ///
+ public string NovelName { get; set; } = novel_name;
+
+ ///
+ /// 配置文件的名称(后缀将是.json)
+ ///
+ public string FileName { get; set; } = file_name;
+
+ ///
+ /// 使用索引器给指定key赋值,不存在key会新增
+ ///
+ ///
+ ///
+ public new NovelNode this[string key]
+ {
+ get => base[key];
+ set
+ {
+ if (value != null) Add(key, value);
+ }
+ }
+
+ ///
+ /// 获取指定key的value
+ ///
+ ///
+ ///
+ public NovelNode? Get(string key)
+ {
+ if (TryGetValue(key, out NovelNode? value) && value != null)
+ {
+ return value;
+ }
+ return null;
+ }
+
+ ///
+ /// 添加一个配置,如果已存在key会覆盖
+ ///
+ ///
+ ///
+ public new void Add(string key, NovelNode value)
+ {
+ if (value != null)
+ {
+ if (TryGetValue(key, out _)) base[key] = value;
+ else base.Add(key, value);
+ }
+ }
+
+ ///
+ /// 从配置文件中读取配置。
+ ///
+ /// 传入定义好的条件字典
+ public void LoadConfig(Dictionary>? 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 dict = NetworkUtility.JsonDeserialize>(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 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 aps)
+ {
+ foreach (string ap in aps)
+ {
+ if (Predicates.TryGetValue(ap, out Func? value3) && value3 != null)
+ {
+ obj.AndPredicates[ap] = value3;
+ }
+ }
+ }
+ if (obj.Values.TryGetValue(nameof(NovelNode.OrPredicates), out value2) && value2 is List ops)
+ {
+ foreach (string op in ops)
+ {
+ if (Predicates.TryGetValue(op, out Func? 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 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 aps)
+ {
+ foreach (string ap in aps)
+ {
+ if (Predicates.TryGetValue(ap, out Func? value4) && value4 != null)
+ {
+ option.AndPredicates[ap] = value4;
+ }
+ }
+ }
+ if (option.Values.TryGetValue(nameof(NovelNode.OrPredicates), out value3) && value3 is List ops)
+ {
+ foreach (string op in ops)
+ {
+ if (Predicates.TryGetValue(op, out Func? value4) && value4 != null)
+ {
+ option.OrPredicates[op] = value4;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// 将配置保存到配置文件。调用此方法会覆盖原有的.json,请注意备份
+ ///
+ public void SaveConfig()
+ {
+ string json = NetworkUtility.JsonSerialize((Dictionary)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();
+ }
+ }
+}
diff --git a/Library/Common/JsonConverter/NovelNodeConverter.cs b/Library/Common/JsonConverter/NovelNodeConverter.cs
new file mode 100644
index 0000000..c52d402
--- /dev/null
+++ b/Library/Common/JsonConverter/NovelNodeConverter.cs
@@ -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
+ {
+ public override NovelNode NewInstance()
+ {
+ return new NovelNode();
+ }
+
+ public override void ReadPropertyName(ref Utf8JsonReader reader, string propertyName, JsonSerializerOptions options, ref NovelNode result, Dictionary 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>(ref reader, options) ?? [];
+ break;
+ case nameof(NovelNode.Options):
+ result.Options = NetworkUtility.JsonDeserialize>(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>(ref reader, options) ?? [];
+ break;
+ case nameof(NovelNode.OrPredicates):
+ result.Values[nameof(NovelNode.OrPredicates)] = NetworkUtility.JsonDeserialize>(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();
+ }
+ }
+}
diff --git a/Library/Common/JsonConverter/NovelOptionConverter.cs b/Library/Common/JsonConverter/NovelOptionConverter.cs
new file mode 100644
index 0000000..da29c99
--- /dev/null
+++ b/Library/Common/JsonConverter/NovelOptionConverter.cs
@@ -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
+ {
+ public override NovelOption NewInstance()
+ {
+ return new NovelOption();
+ }
+
+ public override void ReadPropertyName(ref Utf8JsonReader reader, string propertyName, JsonSerializerOptions options, ref NovelOption result, Dictionary 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>(ref reader, options) ?? [];
+ break;
+ case nameof(NovelNode.AndPredicates):
+ result.Values[nameof(NovelNode.AndPredicates)] = NetworkUtility.JsonDeserialize>(ref reader, options) ?? [];
+ break;
+ case nameof(NovelNode.OrPredicates):
+ result.Values[nameof(NovelNode.OrPredicates)] = NetworkUtility.JsonDeserialize>(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();
+ }
+ }
+}
diff --git a/Model/NovelNode.cs b/Model/NovelNode.cs
new file mode 100644
index 0000000..27416bb
--- /dev/null
+++ b/Model/NovelNode.cs
@@ -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 NextNodes { get; set; } = [];
+ public NovelNode? Next => NextNodes.OrderByDescending(n => n.Priority).Where(n => n.ShowNode).FirstOrDefault();
+ public List Options { get; set; } = [];
+ public List 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> AndPredicates { get; set; } = [];
+ public Dictionary> 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 Values { get; set; } = [];
+ }
+
+ public class NovelOption
+ {
+ public string Key { get; set; } = "";
+ public string Name { get; set; } = "";
+ public List Targets { get; set; } = [];
+ public Dictionary> AndPredicates { get; set; } = [];
+ public Dictionary> 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 Values { get; set; } = [];
+ }
+}
diff --git a/Service/JsonManager.cs b/Service/JsonManager.cs
index 1d9e161..4a00757 100644
--- a/Service/JsonManager.cs
+++ b/Service/JsonManager.cs
@@ -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()
}
};