diff --git a/Common/Configuration/ConfigContainer.cs b/Common/Configuration/ConfigContainer.cs index f6422ed5..9da92d55 100644 --- a/Common/Configuration/ConfigContainer.cs +++ b/Common/Configuration/ConfigContainer.cs @@ -91,6 +91,7 @@ public class ServerOption { return Math.Max(Math.Min(FarmingDropRate, 999), 1); } + public bool UseCache { get; set; } = true; } public class ServerAnnounce diff --git a/Common/Data/Config/Scene/FloorInfo.cs b/Common/Data/Config/Scene/FloorInfo.cs index 3f5168c4..8508f7ab 100644 --- a/Common/Data/Config/Scene/FloorInfo.cs +++ b/Common/Data/Config/Scene/FloorInfo.cs @@ -1,18 +1,22 @@ using System.Collections.Concurrent; using EggLink.DanhengServer.Enums.Scene; +using EggLink.DanhengServer.Util; using Newtonsoft.Json; namespace EggLink.DanhengServer.Data.Config.Scene; public class FloorInfo { - [JsonIgnore] public ConcurrentDictionary CachedTeleports = []; + [JsonConverter(typeof(ConcurrentDictionaryConverter))] + public ConcurrentDictionary CachedTeleports = []; - [JsonIgnore] public ConcurrentDictionary Groups = []; + [JsonConverter(typeof(ConcurrentDictionaryConverter))] + public ConcurrentDictionary Groups = []; [JsonIgnore] public bool Loaded; - [JsonIgnore] public ConcurrentBag UnlockedCheckpoints = []; + [JsonConverter(typeof(ConcurrentBagConverter))] + public ConcurrentBag UnlockedCheckpoints = []; public int FloorID { get; set; } public int StartGroupIndex { get; set; } diff --git a/Common/Data/Custom/BaseRogueBuffExcel.cs b/Common/Data/Custom/BaseRogueBuffExcel.cs index 73c94abc..ddf385e5 100644 --- a/Common/Data/Custom/BaseRogueBuffExcel.cs +++ b/Common/Data/Custom/BaseRogueBuffExcel.cs @@ -5,7 +5,7 @@ using Newtonsoft.Json.Converters; namespace EggLink.DanhengServer.Data.Custom; -public abstract class BaseRogueBuffExcel : ExcelResource +public class BaseRogueBuffExcel : ExcelResource { public int MazeBuffID { get; set; } public int MazeBuffLevel { get; set; } @@ -16,6 +16,11 @@ public abstract class BaseRogueBuffExcel : ExcelResource public int RogueBuffTag { get; set; } + public override int GetId() + { + return MazeBuffID * 100 + MazeBuffLevel; + } + public RogueCommonBuff ToProto() { return new RogueCommonBuff diff --git a/Common/Data/Custom/BaseRogueBuffGroupExcel.cs b/Common/Data/Custom/BaseRogueBuffGroupExcel.cs index 5fc3f833..681d5dd1 100644 --- a/Common/Data/Custom/BaseRogueBuffGroupExcel.cs +++ b/Common/Data/Custom/BaseRogueBuffGroupExcel.cs @@ -2,8 +2,14 @@ namespace EggLink.DanhengServer.Data.Custom; -public abstract class BaseRogueBuffGroupExcel : ExcelResource +public class BaseRogueBuffGroupExcel : ExcelResource { + public int GroupId { get; set; } [JsonIgnore] public List BuffList { get; set; } = []; [JsonIgnore] public bool IsLoaded { get; set; } + + public override int GetId() + { + return GroupId; + } } \ No newline at end of file diff --git a/Common/Data/Excel/ChallengeConfigExcel.cs b/Common/Data/Excel/ChallengeConfigExcel.cs index 21ca9cd0..4b67bf3c 100644 --- a/Common/Data/Excel/ChallengeConfigExcel.cs +++ b/Common/Data/Excel/ChallengeConfigExcel.cs @@ -90,6 +90,7 @@ public class ChallengeConfigExcel : ExcelResource GameData.ChallengeConfigData[ID] = this; } + [method: JsonConstructor] public class ChallengeMonsterInfo(int ConfigId, int NpcMonsterId, int EventId) { public int ConfigId = ConfigId; diff --git a/Common/Data/Excel/InteractConfigExcel.cs b/Common/Data/Excel/InteractConfigExcel.cs index 03f5b7bb..b6ed60be 100644 --- a/Common/Data/Excel/InteractConfigExcel.cs +++ b/Common/Data/Excel/InteractConfigExcel.cs @@ -1,5 +1,5 @@ -using System.Text.Json.Serialization; -using EggLink.DanhengServer.Enums.Scene; +using EggLink.DanhengServer.Enums.Scene; +using Newtonsoft.Json; using Newtonsoft.Json.Converters; namespace EggLink.DanhengServer.Data.Excel; diff --git a/Common/Data/Excel/MainMissionExcel.cs b/Common/Data/Excel/MainMissionExcel.cs index fb94655c..a88e5e64 100644 --- a/Common/Data/Excel/MainMissionExcel.cs +++ b/Common/Data/Excel/MainMissionExcel.cs @@ -25,33 +25,9 @@ public class MainMissionExcel : ExcelResource public int RewardID { get; set; } public List SubRewardList { get; set; } = []; - [JsonIgnore] private MissionInfo? InnerMissionInfo { get; set; } - - [JsonIgnore] - public MissionInfo? MissionInfo - { - get => InnerMissionInfo; - set - { - InnerMissionInfo = value; - if (value != null) - foreach (var sub in value.SubMissionList) - { - SubMissionIds.Add(sub.ID); - GameData.SubMissionData.TryGetValue(sub.ID, out var subMission); - if (subMission != null) - { - subMission.MainMissionID = MainMissionID; - subMission.MainMissionInfo = InnerMissionInfo; - subMission.SubMissionInfo = sub; - } - } - } - } - + [JsonIgnore] public MissionInfo MissionInfo { get; protected set; } = new(); [JsonIgnore] public List SubMissionIds { get; set; } = []; - public override int GetId() { return MainMissionID; @@ -62,6 +38,23 @@ public class MainMissionExcel : ExcelResource GameData.MainMissionData[GetId()] = this; } + public void SetMissionInfo(MissionInfo missionInfo) + { + MissionInfo = missionInfo; + if (missionInfo != null) + foreach (var sub in missionInfo.SubMissionList) + { + SubMissionIds.Add(sub.ID); + GameData.SubMissionData.TryGetValue(sub.ID, out var subMission); + if (subMission != null) + { + subMission.MainMissionID = MainMissionID; + subMission.MainMissionInfo = MissionInfo; + subMission.SubMissionInfo = sub; + } + } + } + public bool IsEqual(MissionData data) { var result = TakeOperation == OperationEnum.And; diff --git a/Common/Data/Excel/RogueBuffExcel.cs b/Common/Data/Excel/RogueBuffExcel.cs index b38821fd..44beb1f5 100644 --- a/Common/Data/Excel/RogueBuffExcel.cs +++ b/Common/Data/Excel/RogueBuffExcel.cs @@ -15,11 +15,6 @@ public class RogueBuffExcel : BaseRogueBuffExcel public bool IsAeonBuff => BattleEventBuffType != RogueBuffAeonTypeEnum.Normal; - public override int GetId() - { - return MazeBuffID * 100 + MazeBuffLevel; - } - public override void Loaded() { GameData.RogueBuffData.Add(GetId(), this); diff --git a/Common/Data/Excel/RogueBuffGroupExcel.cs b/Common/Data/Excel/RogueBuffGroupExcel.cs index df4ce3d9..dd3fa093 100644 --- a/Common/Data/Excel/RogueBuffGroupExcel.cs +++ b/Common/Data/Excel/RogueBuffGroupExcel.cs @@ -1,15 +1,13 @@ using EggLink.DanhengServer.Data.Custom; using EggLink.DanhengServer.Util; -using Newtonsoft.Json; namespace EggLink.DanhengServer.Data.Excel; [ResourceEntity("RogueBuffGroup.json")] public class RogueBuffGroupExcel : BaseRogueBuffGroupExcel { - [JsonProperty("IDLBMIHBAPB")] public int GroupID { get; set; } - - [JsonProperty("GNGDPDOMDFH")] public List BuffTagList { get; set; } = []; + public int GroupID { get; set; } + public List BuffTagList { get; set; } = []; public override int GetId() { diff --git a/Common/Data/Excel/RogueTournBuffExcel.cs b/Common/Data/Excel/RogueTournBuffExcel.cs index 9b8aa427..eac8bf57 100644 --- a/Common/Data/Excel/RogueTournBuffExcel.cs +++ b/Common/Data/Excel/RogueTournBuffExcel.cs @@ -7,11 +7,6 @@ public class RogueTournBuffExcel : BaseRogueBuffExcel { public bool IsInHandbook { get; set; } - public override int GetId() - { - return MazeBuffID * 100 + MazeBuffLevel; - } - public override void Loaded() { GameData.RogueBuffData.TryAdd(GetId(), this); diff --git a/Common/Data/Excel/RogueTournBuffGroupExcel.cs b/Common/Data/Excel/RogueTournBuffGroupExcel.cs index d943a74d..8b1cdc76 100644 --- a/Common/Data/Excel/RogueTournBuffGroupExcel.cs +++ b/Common/Data/Excel/RogueTournBuffGroupExcel.cs @@ -9,13 +9,9 @@ public class RogueTournBuffGroupExcel : BaseRogueBuffGroupExcel public int RogueBuffGroupID { get; set; } public List RogueBuffDrop { get; set; } = []; - public override int GetId() - { - return RogueBuffGroupID; - } - public override void Loaded() { + GroupId = RogueBuffGroupID; GameData.RogueBuffGroupData.Add(GetId(), this); LoadBuff(); } diff --git a/Common/Data/GameData.cs b/Common/Data/GameData.cs index 41cfd8b3..a90b3090 100644 --- a/Common/Data/GameData.cs +++ b/Common/Data/GameData.cs @@ -5,6 +5,8 @@ using EggLink.DanhengServer.Data.Custom; using EggLink.DanhengServer.Data.Excel; using EggLink.DanhengServer.Enums.Rogue; using EggLink.DanhengServer.Enums.TournRogue; +using EggLink.DanhengServer.Util; +using Newtonsoft.Json; namespace EggLink.DanhengServer.Data; @@ -41,7 +43,7 @@ public static class GameData public static Dictionary AvatarExpItemConfigData { get; private set; } = []; public static Dictionary AvatarSkillTreeConfigData { get; private set; } = []; public static Dictionary AvatarDemoConfigData { get; private set; } = []; - public static Dictionary ExpTypeData { get; } = []; + public static Dictionary ExpTypeData { get; private set; } = []; public static Dictionary MultiplePathAvatarConfigData { get; private set; } = []; @@ -106,7 +108,7 @@ public static class GameData public static Dictionary QuestDataData { get; private set; } = []; public static Dictionary FinishWayData { get; private set; } = []; - public static Dictionary PlayerLevelConfigData { get; } = []; + public static Dictionary PlayerLevelConfigData { get; private set; } = []; public static Dictionary BackGroundMusicData { get; private set; } = []; public static Dictionary ChatBubbleConfigData { get; private set; } = []; @@ -121,8 +123,10 @@ public static class GameData #region Maze + [JsonConverter(typeof(ConcurrentDictionaryConverter))] + public static ConcurrentDictionary FloorInfoData { get; private set; } = []; + public static Dictionary NpcDataData { get; private set; } = []; - public static ConcurrentDictionary FloorInfoData { get; } = []; public static Dictionary MapEntranceData { get; private set; } = []; public static Dictionary MazePlaneData { get; private set; } = []; public static Dictionary MazeChestData { get; private set; } = []; @@ -166,7 +170,7 @@ public static class GameData public static Dictionary ItemConfigData { get; private set; } = []; public static Dictionary ItemUseBuffDataData { get; private set; } = []; public static Dictionary EquipmentConfigData { get; private set; } = []; - public static Dictionary EquipmentExpTypeData { get; } = []; + public static Dictionary EquipmentExpTypeData { get; private set; } = []; public static Dictionary EquipmentExpItemConfigData { get; private set; } = []; public static Dictionary EquipmentPromotionConfigData { get; private set; } = diff --git a/Common/Data/ResourceCache.cs b/Common/Data/ResourceCache.cs new file mode 100644 index 00000000..840d7d6c --- /dev/null +++ b/Common/Data/ResourceCache.cs @@ -0,0 +1,175 @@ +using Newtonsoft.Json; +using System.Reflection; +using System.Text; +using EggLink.DanhengServer.Util; +using EggLink.DanhengServer.Data.Config.Scene; +using Newtonsoft.Json.Serialization; +using EggLink.DanhengServer.Internationalization; +using System.IO.MemoryMappedFiles; +using System.IO.Compression; + +namespace EggLink.DanhengServer.Data; + +public static class CompressionHelper +{ + public static byte[] Compress(byte[] data) + { + ArgumentNullException.ThrowIfNull(data); + if (data.Length == 0) return []; + + try + { + if (data.Length < 1024) + { + var result = new byte[data.Length + 1]; + result[0] = 0; + Buffer.BlockCopy(data, 0, result, 1, data.Length); + return result; + } + + using var output = new MemoryStream(); + output.WriteByte(1); + using (var compressor = new DeflateStream(output, CompressionMode.Compress, true)) + { + compressor.Write(data, 0, data.Length); + } + return output.ToArray(); + } + catch + { + var result = new byte[data.Length + 1]; + result[0] = 0; + Buffer.BlockCopy(data, 0, result, 1, data.Length); + return result; + } + } + + public static byte[] Decompress(byte[] data) + { + ArgumentNullException.ThrowIfNull(data); + if (data.Length == 0) return []; + + try + { + if (data[0] == 0) + { + var result = new byte[data.Length - 1]; + Buffer.BlockCopy(data, 1, result, 0, result.Length); + return result; + } + + using var input = new MemoryStream(data, 1, data.Length - 1); + using var decompressor = new DeflateStream(input, CompressionMode.Decompress); + using var output = new MemoryStream(); + decompressor.CopyTo(output); + return output.ToArray(); + } + catch + { + return data; + } + } +} + +public class ResourceCacheData +{ + public Dictionary GameDataValues { get; set; } = []; +} + +public class IgnoreJsonIgnoreContractResolver : DefaultContractResolver +{ + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var property = base.CreateProperty(member, memberSerialization); + property.Ignored = false; + return property; + } +} + +public class ResourceCache +{ + public static Logger Logger { get; } = new("ResourceCache"); + public static string CachePath { get; } = ConfigManager.Config.Path.ConfigPath + "/Resource.cache"; + public static bool IsComplete { get; set; } = true; // Custom in errors to ignore some error + + public static readonly JsonSerializerSettings Serializer = new() + { + ContractResolver = new IgnoreJsonIgnoreContractResolver(), + TypeNameHandling = TypeNameHandling.Auto, + Converters = + { + new ConcurrentBagConverter(), + new ConcurrentDictionaryConverter() + } + }; + + public static Task SaveCache() + { + return Task.Run(() => + { + var cacheData = new ResourceCacheData + { + GameDataValues = typeof(GameData) + .GetProperties(BindingFlags.Public | BindingFlags.Static) + .Where(p => p.GetValue(null) != null) + .ToDictionary( + p => p.Name, + p => CompressionHelper.Compress( + Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject(p.GetValue(null), Serializer) + ) + ) + ) + }; + + File.WriteAllText(CachePath, JsonConvert.SerializeObject(cacheData)); + Logger.Info(I18NManager.Translate("Server.ServerInfo.GeneratedItem", + I18NManager.Translate("Word.Cache"))); + }); + } + + public static bool LoadCache() + { + var buffer = new byte[new FileInfo(CachePath).Length]; + var viewAccessor = MemoryMappedFile.CreateFromFile(CachePath, FileMode.Open).CreateViewAccessor(); + viewAccessor.ReadArray(0, buffer, 0, buffer.Length); + + var cacheData = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(buffer)); + if (cacheData == null) return false; + + Parallel.ForEachAsync( + typeof(GameData).GetProperties(BindingFlags.Public | BindingFlags.Static), + async (prop, token) => { + if (cacheData.GameDataValues.TryGetValue(prop.Name, out var valueBytes)) + prop.SetValue(null, await Task.Run(() => JsonConvert.DeserializeObject( + Encoding.UTF8.GetString( + CompressionHelper.Decompress(valueBytes)), prop.PropertyType, Serializer + ) + ) + ); + } + ); + + Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadedItem", + I18NManager.Translate("Word.Cache"))); + + return true; + } + + public static void ClearGameData() + { + var properties = typeof(GameData).GetProperties(BindingFlags.Public | BindingFlags.Static); + + foreach (var prop in properties) + { + var propType = prop.PropertyType; + var emptyValue = propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(Dictionary<,>) + ? Activator.CreateInstance(propType) + : propType.IsGenericType && propType.GetGenericTypeDefinition() == typeof(List<>) + ? Activator.CreateInstance(propType) : propType.IsClass + ? Activator.CreateInstance(propType) : null; + + prop.SetValue(null, emptyValue); + } + } +} \ No newline at end of file diff --git a/Common/Data/ResourceManager.cs b/Common/Data/ResourceManager.cs index 1fdf643d..d1c6aa44 100644 --- a/Common/Data/ResourceManager.cs +++ b/Common/Data/ResourceManager.cs @@ -89,6 +89,7 @@ public class ResourceManager var file = new FileInfo(path); if (!file.Exists) { + // ResourceCache.IsComplete = false; Logger.Error(I18NManager.Translate("Server.ServerInfo.FailedToReadItem", fileName, I18NManager.Translate("Word.NotFound"))); continue; @@ -154,6 +155,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error( I18NManager.Translate("Server.ServerInfo.FailedToReadItem", fileName, I18NManager.Translate("Word.Error")), ex); @@ -231,6 +233,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error( I18NManager.Translate("Server.ServerInfo.FailedToReadItem", groupFile.Name, I18NManager.Translate("Word.Error")), ex); @@ -243,6 +246,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error( I18NManager.Translate("Server.ServerInfo.FailedToReadItem", file.Name, I18NManager.Translate("Word.Error")), ex); @@ -262,7 +266,6 @@ public class ResourceManager I18NManager.Translate("Word.FloorInfo"))); } - public static void LoadMissionInfo() { Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadingItem", I18NManager.Translate("Word.MissionInfo"))); @@ -292,7 +295,7 @@ public class ResourceManager var missionInfo = JsonConvert.DeserializeObject(json); if (missionInfo != null) { - GameData.MainMissionData[missionExcel.Key].MissionInfo = missionInfo; + GameData.MainMissionData[missionExcel.Key].SetMissionInfo(missionInfo); count++; } else @@ -335,6 +338,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error("Error in reading " + file.Name, ex); } @@ -394,6 +398,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error( I18NManager.Translate("Server.ServerInfo.FailedToReadItem", adventurePath, I18NManager.Translate("Word.Error")), ex); @@ -437,6 +442,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error( I18NManager.Translate("Server.ServerInfo.FailedToReadItem", summonUnit.JsonPath, I18NManager.Translate("Word.Error")), ex); @@ -478,6 +484,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error( I18NManager.Translate("Server.ServerInfo.FailedToReadItem", file.Name, I18NManager.Translate("Word.Error")), ex); @@ -526,6 +533,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error( I18NManager.Translate("Server.ServerInfo.FailedToReadItem", file.Name, I18NManager.Translate("Word.Error")), ex); @@ -555,6 +563,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error( I18NManager.Translate("Server.ServerInfo.FailedToReadItem", file.Name, I18NManager.Translate("Word.Error")), ex); @@ -598,6 +607,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error( I18NManager.Translate("Server.ServerInfo.FailedToReadItem", file.Name, I18NManager.Translate("Word.Error")), ex); @@ -652,6 +662,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error( I18NManager.Translate("Server.ServerInfo.FailedToReadItem", file.Name, I18NManager.Translate("Word.Error")), ex); @@ -705,6 +716,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error( I18NManager.Translate("Server.ServerInfo.FailedToReadItem", file.Name, I18NManager.Translate("Word.Error")), ex); @@ -782,6 +794,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error("Error in reading " + file.Name, ex); } @@ -803,7 +816,6 @@ public class ResourceManager I18NManager.Translate("Word.RogueTournRoomInfo"), $"{ConfigManager.Config.Path.ConfigPath}/TournRogueRoomGen.json", I18NManager.Translate("Word.RogueTournRoom"))); - return; } @@ -827,6 +839,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error("Error in reading " + file.Name, ex); } @@ -872,6 +885,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error("Error in reading " + file.Name, ex); } @@ -910,6 +924,7 @@ public class ResourceManager } catch (Exception ex) { + ResourceCache.IsComplete = false; Logger.Error("Error in reading " + file.Name, ex); } diff --git a/Common/Internationalization/Message/LanguageCHS.cs b/Common/Internationalization/Message/LanguageCHS.cs index fbf8d6d5..4213fa0b 100644 --- a/Common/Internationalization/Message/LanguageCHS.cs +++ b/Common/Internationalization/Message/LanguageCHS.cs @@ -54,6 +54,7 @@ public class WordTextCHS public string Language => "语言"; public string Log => "日志"; public string GameData => "游戏数据"; + public string Cache => "资源缓存"; public string Database => "数据库"; public string Command => "命令"; public string WebServer => "Web服务器"; @@ -152,6 +153,8 @@ public class ServerInfoTextCHS public string CancelKeyPressed => "已按下取消键 (Ctrl + C),服务器即将关闭…"; public string StartingServer => "正在启动 DanhengServer…"; public string LoadingItem => "正在加载 {0}…"; + public string GeneratingItem => "正在生成 {0}…"; + public string WaitingItem => "正在等待进程 {0} 完成…"; public string RegisterItem => "注册了 {0} 个 {1}。"; public string FailedToLoadItem => "加载 {0} 失败。"; public string NewClientSecretKey => "客户端密钥不存在,正在生成新的客户端密钥。"; @@ -163,6 +166,7 @@ public class ServerInfoTextCHS public string ServerRunning => "{0} 服务器正在监听 {1}"; public string ServerStarted => "启动完成!用时 {0}s,击败了99%的用户,输入 ‘help’ 来获取命令帮助"; // 玩梗,考虑英语版本将其本土化 public string MissionEnabled => "任务系统已启用,此功能仍在开发中,且可能不会按预期工作,如果遇见任何bug,请汇报给开发者。"; + public string CacheLoadSkip => "已跳过缓存加载。"; public string ConfigMissing => "{0} 缺失,请检查你的资源文件夹:{1},{2} 可能不能使用。"; public string UnloadedItems => "卸载了所有 {0}。"; diff --git a/Common/Internationalization/Message/LanguageCHT.cs b/Common/Internationalization/Message/LanguageCHT.cs index 7fa44bdb..7ec2e6f4 100644 --- a/Common/Internationalization/Message/LanguageCHT.cs +++ b/Common/Internationalization/Message/LanguageCHT.cs @@ -54,6 +54,7 @@ public class WordTextCHT public string Language => "語言"; public string Log => "日誌"; public string GameData => "遊戲數據"; + public string Cache => "資源緩存"; public string Database => "數據庫"; public string Command => "命令"; public string WebServer => "Web服務器"; @@ -152,6 +153,8 @@ public class ServerInfoTextCHT public string CancelKeyPressed => "已按下取消鍵 (Ctrl + C),服務器即將關閉…"; public string StartingServer => "正在啟動 DanhengServer…"; public string LoadingItem => "正在加載 {0}…"; + public string GeneratingItem => "正在生成 {0}…"; + public string WaitingItem => "正在等待進程 {0} 完成…"; public string RegisterItem => "註冊了 {0} 個 {1}。"; public string FailedToLoadItem => "加載 {0} 失敗。"; public string NewClientSecretKey => "客戶端密鑰不存在,正在生成新的客戶端密鑰。"; @@ -163,6 +166,7 @@ public class ServerInfoTextCHT public string ServerRunning => "{0} 服務器正在監聽 {1}"; public string ServerStarted => "啟動完成!用時 {0}s,擊敗了99%的用戶,輸入 『help』 來獲取命令幫助"; // 玩梗,考慮英語版本將其本土化 public string MissionEnabled => "任務系統已啟用,此功能仍在開發中,且可能不會按預期工作,如果遇見任何bug,請匯報給開發者。"; + public string CacheLoadSkip => "已跳過緩存加載。"; public string ConfigMissing => "{0} 缺失,請檢查你的資源文件夾:{1},{2} 可能不能使用。"; public string UnloadedItems => "卸載了所有 {0}。"; diff --git a/Common/Internationalization/Message/LanguageEN.cs b/Common/Internationalization/Message/LanguageEN.cs index 5a1da6a6..448a6c57 100644 --- a/Common/Internationalization/Message/LanguageEN.cs +++ b/Common/Internationalization/Message/LanguageEN.cs @@ -54,6 +54,7 @@ public class WordTextEN public string Language => "Language"; public string Log => "Log"; public string GameData => "Game Data"; + public string Cache => "Resource Cache"; public string Database => "Database"; public string Command => "Command"; public string WebServer => "Web Server"; @@ -152,6 +153,8 @@ public class ServerInfoTextEN public string CancelKeyPressed => "Cancel key pressed (Ctrl + C), server shutting down..."; public string StartingServer => "Starting DanhengServer..."; public string LoadingItem => "Loading {0}..."; + public string GeneratingItem => "Building {0}..."; + public string WaitingItem => "Waiting for process {0} to complete..."; public string RegisterItem => "Registered {0} {1}(s)."; public string FailedToLoadItem => "Failed to load {0}."; @@ -170,6 +173,7 @@ public class ServerInfoTextEN public string MissionEnabled => "Mission system enabled. This feature is still in development and may not work as expected. Please report any bugs to the developers."; + public string CacheLoadSkip => "Skipped cache loading."; public string ConfigMissing => "{0} is missing. Please check your resource folder: {1}, {2} may not be available."; public string UnloadedItems => "Unloaded all {0}."; diff --git a/Common/Util/CustomConverters.cs b/Common/Util/CustomConverters.cs new file mode 100644 index 00000000..352adbe6 --- /dev/null +++ b/Common/Util/CustomConverters.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; +using System.Collections.Concurrent; + +namespace EggLink.DanhengServer.Util; + +public class ConcurrentBagConverter : JsonConverter> +{ + public override void WriteJson(JsonWriter writer, ConcurrentBag? value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + serializer.Serialize(writer, value.ToArray()); + } + + public override ConcurrentBag? ReadJson(JsonReader reader, Type objectType, ConcurrentBag? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + + var array = serializer.Deserialize(reader); + return array != null ? new ConcurrentBag(array) : new ConcurrentBag(); + } +} + +public class ConcurrentDictionaryConverter : JsonConverter> where TKey : notnull +{ + public override void WriteJson(JsonWriter writer, ConcurrentDictionary? value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + serializer.Serialize(writer, value.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); + } + + public override ConcurrentDictionary? ReadJson(JsonReader reader, Type objectType, ConcurrentDictionary? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + + var dictionary = serializer.Deserialize>(reader); + return dictionary != null ? new ConcurrentDictionary(dictionary) : new ConcurrentDictionary(); + } +} \ No newline at end of file diff --git a/Program/Handbook/HandbookGenerator.cs b/Program/Handbook/HandbookGenerator.cs index f7cfe350..0d7b01c2 100644 --- a/Program/Handbook/HandbookGenerator.cs +++ b/Program/Handbook/HandbookGenerator.cs @@ -23,6 +23,16 @@ public static class HandbookGenerator { if (langFile.Extension != ".json") return; var lang = langFile.Name.Replace("TextMap", "").Replace(".json", ""); + + // Check if handbook needs to regenerate + var handbookPath = $"GM Handbook/GM Handbook {lang}.txt"; + if (File.Exists(handbookPath)) + { + var handbookInfo = new FileInfo(handbookPath); + if (handbookInfo.LastWriteTime >= langFile.LastWriteTime) + continue; // Skip if handbook is newer than language file + } + Generate(lang); } diff --git a/Program/Program/EntryPoint.cs b/Program/Program/EntryPoint.cs index 47d1d980..74b59763 100644 --- a/Program/Program/EntryPoint.cs +++ b/Program/Program/EntryPoint.cs @@ -127,10 +127,40 @@ public class EntryPoint GenerateLogMap(); // Load the game data - Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadingItem", I18NManager.Translate("Word.GameData"))); try { - ResourceManager.LoadGameData(); + var isCache = false; + if (File.Exists(ResourceCache.CachePath)) + if (ConfigManager.Config.ServerOption.UseCache) + { + Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadingItem", I18NManager.Translate("Word.Cache"))); + isCache = ResourceCache.LoadCache(); + + // Clear all game data if cache loading fails + if (!isCache) + { + ResourceCache.ClearGameData(); + Logger.Warn(I18NManager.Translate("Server.ServerInfo.CacheLoadFailed")); + } + } + else + { + File.Delete(ResourceCache.CachePath); + Logger.Warn(I18NManager.Translate("Server.ServerInfo.CacheLoadSkip")); + } + + if (!isCache) + { + Logger.Info(I18NManager.Translate("Server.ServerInfo.LoadingItem", I18NManager.Translate("Word.GameData"))); + ResourceManager.LoadGameData(); + + // Async process cache saving + if (ConfigManager.Config.ServerOption.UseCache && ResourceCache.IsComplete) + { + Logger.Warn(I18NManager.Translate("Server.ServerInfo.WaitingItem", I18NManager.Translate("Word.Cache"))); + _ = ResourceCache.SaveCache(); + } + } } catch (Exception e) {