From 915de4bc36ec080713becc1d5d4ce3be4e7850b5 Mon Sep 17 00:00:00 2001 From: milimoe <110188673+milimoe@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:32:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8A=A0=E8=BD=BD=E9=A1=B9?= =?UTF-8?q?=E7=83=AD=E6=9B=B4=E6=96=B0=E5=8A=9F=E8=83=BD=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 添加加载项热更新功能 * 添加加载项卸载的内部实现,完善示例代码 --- Api/Utility/Factory.cs | 37 ++ Api/Utility/GameModuleLoader.cs | 111 +++- Api/Utility/General.cs | 25 + Api/Utility/PluginLoader.cs | 47 +- Api/Utility/ServerPluginLoader.cs | 47 +- Api/Utility/SkillExtension.cs | 122 ++++ Api/Utility/TaskScheduler.cs | 9 + Api/Utility/TextReader.cs | 1 + Api/Utility/WebAPIPluginLoader.cs | 47 +- Entity/Character/Character.cs | 4 +- Entity/Skill/Effect.cs | 2 +- Entity/Skill/Skill.cs | 2 +- Interface/Base/Addons/IAddon.cs | 1 + Interface/Base/Addons/IGameMap.cs | 6 +- Interface/Base/Addons/IGameModule.cs | 4 +- Interface/Base/Addons/IGameModuleServer.cs | 5 +- Interface/Base/Addons/IHotReloadAware.cs | 13 + Library/Common/Addon/CharacterModule.cs | 13 +- .../Common/Addon/Example/ExampleGameModule.cs | 461 ++++++++++++-- Library/Common/Addon/Example/ExampleItem.cs | 189 ++++++ Library/Common/Addon/Example/ExampleSkill.cs | 386 ++++++++++++ Library/Common/Addon/GameMap.cs | 21 + Library/Common/Addon/GameModule.cs | 266 ++++++-- Library/Common/Addon/GameModuleServer.cs | 14 + Library/Common/Addon/ItemModule.cs | 9 + Library/Common/Addon/Plugin.cs | 331 +++++++--- Library/Common/Addon/ServerPlugin.cs | 331 +++++++--- Library/Common/Addon/SkillModule.cs | 10 + Library/Common/Addon/WebAPIPlugin.cs | 341 ++++++++--- Library/Common/Architecture/SyncAwaiter.cs | 45 ++ Library/Exception/Exception.cs | 15 + Model/DamageCalculationOptions.cs | 2 +- Model/GamingQueue.cs | 4 +- .../CourageCommandSkill.cs | 2 +- README.md | 4 +- Service/AddonManager.cs | 12 +- Service/HotLoadAddonManager.cs | 566 ++++++++++++++++++ 37 files changed, 3096 insertions(+), 409 deletions(-) create mode 100644 Api/Utility/SkillExtension.cs create mode 100644 Interface/Base/Addons/IHotReloadAware.cs create mode 100644 Library/Common/Addon/Example/ExampleItem.cs create mode 100644 Library/Common/Addon/Example/ExampleSkill.cs create mode 100644 Library/Common/Architecture/SyncAwaiter.cs create mode 100644 Service/HotLoadAddonManager.cs diff --git a/Api/Utility/Factory.cs b/Api/Utility/Factory.cs index 1949dc9..0463706 100644 --- a/Api/Utility/Factory.cs +++ b/Api/Utility/Factory.cs @@ -68,6 +68,43 @@ namespace Milimoe.FunGame.Core.Api.Utility } } + /// + /// 移除工厂方法 + /// + /// + /// + public void UnRegisterFactory(EntityFactoryDelegate d) + { + if (typeof(T) == typeof(Character) && d is EntityFactoryDelegate character) + { + CharacterFactories.Remove(character); + } + if (typeof(T) == typeof(Inventory) && d is EntityFactoryDelegate inventory) + { + InventoryFactories.Remove(inventory); + } + if (typeof(T) == typeof(Skill) && d is EntityFactoryDelegate skill) + { + SkillFactories.Remove(skill); + } + if (typeof(T) == typeof(Effect) && d is EntityFactoryDelegate effect) + { + EffectFactories.Remove(effect); + } + if (typeof(T) == typeof(Item) && d is EntityFactoryDelegate item) + { + ItemFactories.Remove(item); + } + if (typeof(T) == typeof(Room) && d is EntityFactoryDelegate room) + { + RoomFactories.Remove(room); + } + if (typeof(T) == typeof(User) && d is EntityFactoryDelegate user) + { + UserFactories.Remove(user); + } + } + /// /// 构造一个实体实例 /// diff --git a/Api/Utility/GameModuleLoader.cs b/Api/Utility/GameModuleLoader.cs index e5596b3..47a72dd 100644 --- a/Api/Utility/GameModuleLoader.cs +++ b/Api/Utility/GameModuleLoader.cs @@ -39,9 +39,17 @@ namespace Milimoe.FunGame.Core.Api.Utility /// /// 已加载的模组DLL名称对应的路径 /// - public static Dictionary ModuleFilePaths => new(AddonManager.ModuleFilePaths); + public Dictionary ModuleFilePaths => IsHotLoadMode ? new(HotLoadAddonManager.ModuleFilePaths) : new(AddonManager.ModuleFilePaths); - private GameModuleLoader() { } + /// + /// 使用可热更新的加载项模式 + /// + public bool IsHotLoadMode { get; } = false; + + private GameModuleLoader(bool hotMode = false) + { + IsHotLoadMode = hotMode; + } /// /// 传入 类型来创建指定端的模组读取器 @@ -61,11 +69,13 @@ namespace Milimoe.FunGame.Core.Api.Utility AddonManager.LoadGameMaps(loader.Maps, otherobjs); foreach (GameMap map in loader.Maps.Values.ToList()) { + map.ModuleLoader = loader; map.AfterLoad(loader, otherobjs); } AddonManager.LoadGameModules(loader.Modules, loader.Characters, loader.Skills, loader.Items, delegates, otherobjs); foreach (GameModule module in loader.Modules.Values.ToList()) { + module.ModuleLoader = loader; // 读取模组的依赖集合 module.GameModuleDepend.GetDependencies(loader); // 如果模组加载后需要执行代码,请重写AfterLoad方法 @@ -77,11 +87,13 @@ namespace Milimoe.FunGame.Core.Api.Utility AddonManager.LoadGameMaps(loader.Maps, otherobjs); foreach (GameMap map in loader.Maps.Values.ToList()) { + map.ModuleLoader = loader; map.AfterLoad(loader, otherobjs); } AddonManager.LoadGameModulesForServer(loader.ModuleServers, loader.Characters, loader.Skills, loader.Items, delegates, otherobjs); foreach (GameModuleServer server in loader.ModuleServers.Values.ToList()) { + server.ModuleLoader = loader; server.GameModuleDepend.GetDependencies(loader); server.AfterLoad(loader, otherobjs); } @@ -89,6 +101,101 @@ namespace Milimoe.FunGame.Core.Api.Utility return loader; } + /// + /// 传入 类型来创建指定端的模组读取器 [ 可热更新模式 ] + /// runtime = 时,仅读取 + /// runtime = 时,仅读取 + /// 都会读取 + /// + /// 传入 类型来创建指定端的模组读取器 + /// 用于构建 + /// 其他需要传入给插件初始化的对象 + /// + public static GameModuleLoader LoadGameModulesByHotLoadMode(FunGameInfo.FunGame runtime, Dictionary delegates, params object[] otherobjs) + { + GameModuleLoader loader = new(true); + if (runtime == FunGameInfo.FunGame.FunGame_Desktop) + { + List updated = HotLoadAddonManager.LoadGameMaps(loader.Maps, otherobjs); + foreach (GameMap map in updated) + { + map.ModuleLoader = loader; + map.AfterLoad(loader, otherobjs); + } + List updatedModule = HotLoadAddonManager.LoadGameModules(loader.Modules, loader.Characters, loader.Skills, loader.Items, delegates, otherobjs); + foreach (GameModule module in updatedModule) + { + module.ModuleLoader = loader; + // 读取模组的依赖集合 + module.GameModuleDepend.GetDependencies(loader); + // 如果模组加载后需要执行代码,请重写AfterLoad方法 + module.AfterLoad(loader, otherobjs); + } + } + else if (runtime == FunGameInfo.FunGame.FunGame_Server) + { + List updated = HotLoadAddonManager.LoadGameMaps(loader.Maps, otherobjs); + foreach (GameMap map in updated) + { + map.ModuleLoader = loader; + map.AfterLoad(loader, otherobjs); + } + List updatedServer = HotLoadAddonManager.LoadGameModulesForServer(loader.ModuleServers, loader.Characters, loader.Skills, loader.Items, delegates, otherobjs); + foreach (GameModuleServer server in updatedServer) + { + server.ModuleLoader = loader; + server.GameModuleDepend.GetDependencies(loader); + server.AfterLoad(loader, otherobjs); + } + } + return loader; + } + + /// + /// 热更新 + /// + /// + /// + /// + public void HotReload(FunGameInfo.FunGame runtime, Dictionary delegates, params object[] otherobjs) + { + if (!IsHotLoadMode) return; + if (runtime == FunGameInfo.FunGame.FunGame_Desktop) + { + List updated = HotLoadAddonManager.LoadGameMaps(Maps, otherobjs); + foreach (GameMap map in updated) + { + map.ModuleLoader = this; + map.AfterLoad(this, otherobjs); + } + List updatedModule = HotLoadAddonManager.LoadGameModules(Modules, Characters, Skills, Items, delegates, otherobjs); + foreach (GameModule module in updatedModule) + { + module.ModuleLoader = this; + // 读取模组的依赖集合 + module.GameModuleDepend.GetDependencies(this); + // 如果模组加载后需要执行代码,请重写AfterLoad方法 + module.AfterLoad(this, otherobjs); + } + } + else if (runtime == FunGameInfo.FunGame.FunGame_Server) + { + List updated = HotLoadAddonManager.LoadGameMaps(Maps, otherobjs); + foreach (GameMap map in updated) + { + map.ModuleLoader = this; + map.AfterLoad(this, otherobjs); + } + List updatedServer = HotLoadAddonManager.LoadGameModulesForServer(ModuleServers, Characters, Skills, Items, delegates, otherobjs); + foreach (GameModuleServer server in updatedServer) + { + server.ModuleLoader = this; + server.GameModuleDepend.GetDependencies(this); + server.AfterLoad(this, otherobjs); + } + } + } + /// /// 获取对应名称的模组实例 /// 如果需要取得服务器模组的实例,请调用 diff --git a/Api/Utility/General.cs b/Api/Utility/General.cs index 0091d43..bab6154 100644 --- a/Api/Utility/General.cs +++ b/Api/Utility/General.cs @@ -6,8 +6,10 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; +using Milimoe.FunGame.Core.Interface.Addons; using Milimoe.FunGame.Core.Library.Common.Architecture; using Milimoe.FunGame.Core.Library.Constant; +using Milimoe.FunGame.Core.Service; // 通用工具类,客户端和服务器端都可以直接调用的工具方法都可以写在这里 namespace Milimoe.FunGame.Core.Api.Utility @@ -850,4 +852,27 @@ namespace Milimoe.FunGame.Core.Api.Utility } #endregion + + #region 加载项服务 + + public class HotLoadAddonUtility + { + /// + /// 热更新 DLL + /// + /// DLL 完整路径 + /// 是否成功热更新 + public static bool HotReload(string filePath) => HotLoadAddonManager.HotReload(filePath); + + /// + /// 尝试获取当前最新的实例 + /// + /// 预期类型 + /// 插件/模组名称 + /// 最新的实例 + /// 是否找到 + public static bool TryGetLiveInstance(string addonName, out T? instance) where T : class, IAddon => HotLoadAddonManager.TryGetLiveInstance(addonName, out instance); + } + + #endregion } diff --git a/Api/Utility/PluginLoader.cs b/Api/Utility/PluginLoader.cs index 40684a9..5bda4e7 100644 --- a/Api/Utility/PluginLoader.cs +++ b/Api/Utility/PluginLoader.cs @@ -15,11 +15,16 @@ namespace Milimoe.FunGame.Core.Api.Utility /// /// 已加载的插件DLL名称对应的路径 /// - public static Dictionary PluginFilePaths => new(AddonManager.PluginFilePaths); + public Dictionary PluginFilePaths => IsHotLoadMode ? new(HotLoadAddonManager.PluginFilePaths) : new(AddonManager.PluginFilePaths); - private PluginLoader() + /// + /// 使用可热更新的加载项模式 + /// + public bool IsHotLoadMode { get; } = false; + + private PluginLoader(bool hotMode = false) { - + IsHotLoadMode = hotMode; } /// @@ -34,12 +39,48 @@ namespace Milimoe.FunGame.Core.Api.Utility AddonManager.LoadPlugins(loader.Plugins, delegates, otherobjs); foreach (Plugin plugin in loader.Plugins.Values.ToList()) { + plugin.PluginLoader = loader; // 如果插件加载后需要执行代码,请重写AfterLoad方法 plugin.AfterLoad(loader, otherobjs); } return loader; } + /// + /// 构建一个插件读取器并读取插件 [ 可热更新模式 ] + /// + /// 用于构建 + /// 其他需要传入给插件初始化的对象 + /// + public static PluginLoader LoadPluginsByHotLoadMode(Dictionary delegates, params object[] otherobjs) + { + PluginLoader loader = new(); + List updated = HotLoadAddonManager.LoadPlugins(loader.Plugins, delegates, otherobjs); + foreach (Plugin plugin in updated) + { + plugin.PluginLoader = loader; + // 如果插件加载后需要执行代码,请重写AfterLoad方法 + plugin.AfterLoad(loader, otherobjs); + } + return loader; + } + + /// + /// 热更新 + /// + /// + /// + public void HotReload(Dictionary delegates, params object[] otherobjs) + { + if (!IsHotLoadMode) return; + List updated = HotLoadAddonManager.LoadPlugins(Plugins, delegates, otherobjs); + foreach (Plugin plugin in updated) + { + plugin.PluginLoader = this; + plugin.AfterLoad(this, otherobjs); + } + } + public Plugin this[string name] { get diff --git a/Api/Utility/ServerPluginLoader.cs b/Api/Utility/ServerPluginLoader.cs index 4ec721c..cc005c5 100644 --- a/Api/Utility/ServerPluginLoader.cs +++ b/Api/Utility/ServerPluginLoader.cs @@ -15,11 +15,16 @@ namespace Milimoe.FunGame.Core.Api.Utility /// /// 已加载的插件DLL名称对应的路径 /// - public static Dictionary PluginFilePaths => new(AddonManager.PluginFilePaths); + public Dictionary PluginFilePaths => IsHotLoadMode ? new(HotLoadAddonManager.PluginFilePaths) : new(AddonManager.PluginFilePaths); - private ServerPluginLoader() + /// + /// 使用可热更新的加载项模式 + /// + public bool IsHotLoadMode { get; } = false; + + private ServerPluginLoader(bool hotMode = false) { - + IsHotLoadMode = hotMode; } /// @@ -34,12 +39,48 @@ namespace Milimoe.FunGame.Core.Api.Utility AddonManager.LoadServerPlugins(loader.Plugins, delegates, otherobjs); foreach (ServerPlugin plugin in loader.Plugins.Values.ToList()) { + plugin.PluginLoader = loader; // 如果插件加载后需要执行代码,请重写AfterLoad方法 plugin.AfterLoad(loader, otherobjs); } return loader; } + /// + /// 构建一个插件读取器并读取插件 [ 可热更新模式 ] + /// + /// 用于构建 + /// 其他需要传入给插件初始化的对象 + /// + public static ServerPluginLoader LoadPluginsByHotLoadMode(Dictionary delegates, params object[] otherobjs) + { + ServerPluginLoader loader = new(true); + List updated = HotLoadAddonManager.LoadServerPlugins(loader.Plugins, delegates, otherobjs); + foreach (ServerPlugin plugin in updated) + { + plugin.PluginLoader = loader; + // 如果插件加载后需要执行代码,请重写AfterLoad方法 + plugin.AfterLoad(loader, otherobjs); + } + return loader; + } + + /// + /// 热更新 + /// + /// + /// + public void HotReload(Dictionary delegates, params object[] otherobjs) + { + if (!IsHotLoadMode) return; + List updated = HotLoadAddonManager.LoadServerPlugins(Plugins, delegates, otherobjs); + foreach (ServerPlugin plugin in updated) + { + plugin.PluginLoader = this; + plugin.AfterLoad(this, otherobjs); + } + } + public ServerPlugin this[string name] { get diff --git a/Api/Utility/SkillExtension.cs b/Api/Utility/SkillExtension.cs new file mode 100644 index 0000000..8fc6629 --- /dev/null +++ b/Api/Utility/SkillExtension.cs @@ -0,0 +1,122 @@ +using Milimoe.FunGame.Core.Entity; +using Milimoe.FunGame.Core.Library.Constant; + +namespace Milimoe.FunGame.Core.Api.Utility +{ + public static class SkillExtension + { + public static string SkillOwner(this Skill skill, Character? character = null) + { + if (character is null && skill.Character is not null) + { + character = skill.Character; + } + if (character is null) + { + return "你"; + } + return character.NickName != "" ? character.NickName : character.GetName(); + } + + public static string TargetDescription(this Skill skill) + { + if (skill.IsNonDirectional && skill.GamingQueue?.Map != null) + { + return skill.RangeTargetDescription(); + } + + string str; + + if (skill.SelectAllTeammates) + { + str = "友方全体角色"; + } + else if (skill.SelectAllEnemies) + { + str = "敌方全体角色"; + } + else if (skill.CanSelectTeammate && !skill.CanSelectEnemy) + { + str = $"目标{(skill.CanSelectTargetCount > 1 ? $"至多 {skill.CanSelectTargetCount} 个" : "")}友方角色{(!skill.CanSelectSelf ? "(不可选择自身)" : "")}"; + } + else if (!skill.CanSelectTeammate && skill.CanSelectEnemy) + { + str = $"目标{(skill.CanSelectTargetCount > 1 ? $"至多 {skill.CanSelectTargetCount} 个" : "")}敌方角色"; + } + else if (!skill.CanSelectTeammate && !skill.CanSelectEnemy && skill.CanSelectSelf) + { + str = $"自身"; + } + else + { + str = $"{(skill.CanSelectTargetCount > 1 ? $"至多 {skill.CanSelectTargetCount} 个" : "")}目标"; + } + + if (skill.CanSelectTargetRange > 0 && skill.GamingQueue?.Map != null) + { + str += $"以及以{(skill.CanSelectTargetCount > 1 ? "这些" : "该")}目标为中心,半径为 {skill.CanSelectTargetRange} 格的菱形区域中的等同阵营角色"; + } + + return str; + } + + public static string RangeTargetDescription(this Skill skill) + { + string str = ""; + + int range = skill.CanSelectTargetRange; + if (range <= 0) + { + str = "目标地点"; + } + else + { + switch (skill.SkillRangeType) + { + case SkillRangeType.Diamond: + str = $"目标半径为 {skill.CanSelectTargetRange} 格的菱形区域"; + break; + case SkillRangeType.Circle: + str = $"目标半径为 {skill.CanSelectTargetRange} 格的圆形区域"; + break; + case SkillRangeType.Square: + str = $"目标边长为 {skill.CanSelectTargetRange * 2 + 1} 格的正方形区域"; + break; + case SkillRangeType.Line: + str = $"自身与目标地点之间的、宽度为 {skill.CanSelectTargetRange} 格的直线区域"; + break; + case SkillRangeType.LinePass: + str = $"自身与目标地点之间的、宽度为 {skill.CanSelectTargetRange} 格的直线区域以及贯穿该目标地点直至地图边缘的等宽直线区域"; + break; + case SkillRangeType.Sector: + str = $"目标最大半径为 {skill.CanSelectTargetRange} 格的扇形区域"; + break; + default: + break; + } + } + + if (skill.SelectIncludeCharacterGrid) + { + if (skill.CanSelectTeammate && !skill.CanSelectEnemy) + { + str = $"{str}中的所有友方角色{(!skill.CanSelectSelf ? "(包括自身)" : "")}"; + } + else if (!skill.CanSelectTeammate && skill.CanSelectEnemy) + { + str = $"{str}中的所有敌方角色"; + } + else + { + str = $"{str}中的所有角色"; + } + } + else + { + str = "一个未被角色占据的"; + } + + return str; + } + } +} diff --git a/Api/Utility/TaskScheduler.cs b/Api/Utility/TaskScheduler.cs index ad695aa..5ca3381 100644 --- a/Api/Utility/TaskScheduler.cs +++ b/Api/Utility/TaskScheduler.cs @@ -1,6 +1,7 @@ using Milimoe.FunGame.Core.Interface.Base; using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Model; +using Milimoe.FunGame.Core.Service; namespace Milimoe.FunGame.Core.Api.Utility { @@ -154,6 +155,14 @@ namespace Milimoe.FunGame.Core.Api.Utility return msg.Trim(); } + /// + /// 开启循环检查是否有未清除的加载项上下文 + /// + public static void StartCleanUnusedAddonContexts(Action? error = null) + { + Shared.AddRecurringTask("CleanUnusedContexts", TimeSpan.FromMinutes(2), HotLoadAddonManager.CleanUnusedContexts, true, error); + } + /// /// 执行任务 /// diff --git a/Api/Utility/TextReader.cs b/Api/Utility/TextReader.cs index f0cad4d..a1f572e 100644 --- a/Api/Utility/TextReader.cs +++ b/Api/Utility/TextReader.cs @@ -81,6 +81,7 @@ namespace Milimoe.FunGame.Core.Api.Utility * Console */ WriteINI("Console", "LogLevel", "INFO"); + WriteINI("Console", "UseHotLoadAddons", "false"); /** * Server */ diff --git a/Api/Utility/WebAPIPluginLoader.cs b/Api/Utility/WebAPIPluginLoader.cs index 7e892d0..00c0b0f 100644 --- a/Api/Utility/WebAPIPluginLoader.cs +++ b/Api/Utility/WebAPIPluginLoader.cs @@ -15,11 +15,16 @@ namespace Milimoe.FunGame.Core.Api.Utility /// /// 已加载的插件DLL名称对应的路径 /// - public static Dictionary PluginFilePaths => new(AddonManager.PluginFilePaths); + public Dictionary PluginFilePaths => IsHotLoadMode ? new(HotLoadAddonManager.PluginFilePaths) : new(AddonManager.PluginFilePaths); - private WebAPIPluginLoader() + /// + /// 使用可热更新的加载项模式 + /// + public bool IsHotLoadMode { get; } = false; + + private WebAPIPluginLoader(bool hotMode = false) { - + IsHotLoadMode = hotMode; } /// @@ -34,12 +39,48 @@ namespace Milimoe.FunGame.Core.Api.Utility AddonManager.LoadWebAPIPlugins(loader.Plugins, delegates, otherobjs); foreach (WebAPIPlugin plugin in loader.Plugins.Values.ToList()) { + plugin.PluginLoader = loader; // 如果插件加载后需要执行代码,请重写AfterLoad方法 plugin.AfterLoad(loader, otherobjs); } return loader; } + /// + /// 构建一个插件读取器并读取插件 [ 可热更新模式 ] + /// + /// 用于构建 + /// 其他需要传入给插件初始化的对象 + /// + public static WebAPIPluginLoader LoadPluginsByHotLoadMode(Dictionary delegates, params object[] otherobjs) + { + WebAPIPluginLoader loader = new(true); + List updated = HotLoadAddonManager.LoadWebAPIPlugins(loader.Plugins, delegates, otherobjs); + foreach (WebAPIPlugin plugin in updated) + { + plugin.PluginLoader = loader; + // 如果插件加载后需要执行代码,请重写AfterLoad方法 + plugin.AfterLoad(loader, otherobjs); + } + return loader; + } + + /// + /// 热更新 + /// + /// + /// + public void HotReload(Dictionary delegates, params object[] otherobjs) + { + if (!IsHotLoadMode) return; + List updated = HotLoadAddonManager.LoadWebAPIPlugins(Plugins, delegates, otherobjs); + foreach (WebAPIPlugin plugin in updated) + { + plugin.PluginLoader = this; + plugin.AfterLoad(this, otherobjs); + } + } + public WebAPIPlugin this[string name] { get diff --git a/Entity/Character/Character.cs b/Entity/Character/Character.cs index 565c662..6bcf17f 100644 --- a/Entity/Character/Character.cs +++ b/Entity/Character/Character.cs @@ -707,7 +707,7 @@ namespace Milimoe.FunGame.Core.Entity /// 力量豁免 /// public double STRExemption => STR * GameplayEquilibriumConstant.STRtoExemptionRateMultiplier; - + /// /// 敏捷豁免 /// @@ -1531,7 +1531,7 @@ namespace Milimoe.FunGame.Core.Entity builder.AppendLine($"等级:{Level} / {GameplayEquilibriumConstant.MaxLevel}(突破进度:{LevelBreak + 1} / {GameplayEquilibriumConstant.LevelBreakList.Count})"); builder.AppendLine($"经验值:{EXP:0.##}{(Level != GameplayEquilibriumConstant.MaxLevel && GameplayEquilibriumConstant.EXPUpperLimit.TryGetValue(Level, out double need) ? " / " + need : "")}"); } - + builder.AppendLine(GetSimpleAttributeInfo(showGrowth, showBasicOnly).Trim()); if (showMapRelated) diff --git a/Entity/Skill/Effect.cs b/Entity/Skill/Effect.cs index d839c3e..badd996 100644 --- a/Entity/Skill/Effect.cs +++ b/Entity/Skill/Effect.cs @@ -142,7 +142,7 @@ namespace Milimoe.FunGame.Core.Entity /// 无视免疫类型 /// public virtual ImmuneType IgnoreImmune { get; set; } = ImmuneType.None; - + /// /// 豁免性的具体说明 /// diff --git a/Entity/Skill/Skill.cs b/Entity/Skill/Skill.cs index 86145f2..590ec86 100644 --- a/Entity/Skill/Skill.cs +++ b/Entity/Skill/Skill.cs @@ -474,7 +474,7 @@ namespace Milimoe.FunGame.Core.Entity /// public virtual void ResolveInquiryBeforeTargetSelection(Character character, DecisionPoints dp, InquiryOptions options, InquiryResponse response) { - + } /// diff --git a/Interface/Base/Addons/IAddon.cs b/Interface/Base/Addons/IAddon.cs index 0107881..ea8079e 100644 --- a/Interface/Base/Addons/IAddon.cs +++ b/Interface/Base/Addons/IAddon.cs @@ -8,5 +8,6 @@ public string Author { get; } public bool Load(params object[] objs); + public void UnLoad(params object[] objs); } } diff --git a/Interface/Base/Addons/IGameMap.cs b/Interface/Base/Addons/IGameMap.cs index 93e5800..5bd51c3 100644 --- a/Interface/Base/Addons/IGameMap.cs +++ b/Interface/Base/Addons/IGameMap.cs @@ -1,7 +1,9 @@ -namespace Milimoe.FunGame.Core.Interface.Addons +using Milimoe.FunGame.Core.Api.Utility; + +namespace Milimoe.FunGame.Core.Interface.Addons { public interface IGameMap : IAddon { - + public GameModuleLoader? ModuleLoader { get; } } } diff --git a/Interface/Base/Addons/IGameModule.cs b/Interface/Base/Addons/IGameModule.cs index 866ba17..8dafcd0 100644 --- a/Interface/Base/Addons/IGameModule.cs +++ b/Interface/Base/Addons/IGameModule.cs @@ -1,4 +1,5 @@ -using Milimoe.FunGame.Core.Model; +using Milimoe.FunGame.Core.Api.Utility; +using Milimoe.FunGame.Core.Model; namespace Milimoe.FunGame.Core.Interface.Addons { @@ -6,6 +7,7 @@ namespace Milimoe.FunGame.Core.Interface.Addons IGamingRandomEventHandler, IGamingRoundEventHandler, IGamingLevelUpEventHandler, IGamingMoveEventHandler, IGamingAttackEventHandler, IGamingSkillEventHandler, IGamingItemEventHandler, IGamingMagicEventHandler, IGamingBuyEventHandler, IGamingSuperSkillEventHandler, IGamingPauseEventHandler, IGamingUnpauseEventHandler, IGamingSurrenderEventHandler, IGamingUpdateInfoEventHandler, IGamingPunishEventHandler, IGameModuleDepend { + public GameModuleLoader? ModuleLoader { get; } public bool HideMain { get; } public void StartGame(Gaming instance, params object[] args); public void StartUI(params object[] args); diff --git a/Interface/Base/Addons/IGameModuleServer.cs b/Interface/Base/Addons/IGameModuleServer.cs index 11e1632..1b91d5f 100644 --- a/Interface/Base/Addons/IGameModuleServer.cs +++ b/Interface/Base/Addons/IGameModuleServer.cs @@ -1,4 +1,5 @@ -using Milimoe.FunGame.Core.Interface.Base; +using Milimoe.FunGame.Core.Api.Utility; +using Milimoe.FunGame.Core.Interface.Base; using Milimoe.FunGame.Core.Library.Common.Addon; using Milimoe.FunGame.Core.Library.Constant; @@ -6,6 +7,8 @@ namespace Milimoe.FunGame.Core.Interface.Addons { public interface IGameModuleServer : IAddon, IAddonController, IGameModuleDepend { + public GameModuleLoader? ModuleLoader { get; } + public bool StartServer(GamingObject obj, params object[] args); public Task> GamingMessageHandler(IServerModel model, GamingType type, Dictionary data); diff --git a/Interface/Base/Addons/IHotReloadAware.cs b/Interface/Base/Addons/IHotReloadAware.cs new file mode 100644 index 0000000..8659511 --- /dev/null +++ b/Interface/Base/Addons/IHotReloadAware.cs @@ -0,0 +1,13 @@ +namespace Milimoe.FunGame.Core.Interface.Base.Addons +{ + /// + /// 实现此接口的插件/模组才能被热更新模式加载 + /// + public interface IHotReloadAware + { + /// + /// 在卸载前调用,自行做一些清理,否则卸载不安全 + /// + public void OnBeforeUnload(); + } +} diff --git a/Library/Common/Addon/CharacterModule.cs b/Library/Common/Addon/CharacterModule.cs index 3c01ffc..33359b9 100644 --- a/Library/Common/Addon/CharacterModule.cs +++ b/Library/Common/Addon/CharacterModule.cs @@ -51,17 +51,26 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon // 模组加载后,不允许再次加载此模组 _isLoaded = true; // 注册工厂 - Factory.OpenFactory.RegisterFactory(EntityFactory()); + Factory.OpenFactory.RegisterFactory(CharacterFactory()); // 如果加载后需要执行代码,请重写AfterLoad方法 AfterLoad(); } return _isLoaded; } + /// + /// 卸载模组 + /// + /// + public void UnLoad(params object[] objs) + { + Factory.OpenFactory.UnRegisterFactory(CharacterFactory()); + } + /// /// 注册工厂 /// - protected virtual Factory.EntityFactoryDelegate EntityFactory() + protected virtual Factory.EntityFactoryDelegate CharacterFactory() { return (id, name, args) => { diff --git a/Library/Common/Addon/Example/ExampleGameModule.cs b/Library/Common/Addon/Example/ExampleGameModule.cs index a92f724..88a0b04 100644 --- a/Library/Common/Addon/Example/ExampleGameModule.cs +++ b/Library/Common/Addon/Example/ExampleGameModule.cs @@ -1,9 +1,12 @@ using System.Collections.Concurrent; +using System.Text; using Milimoe.FunGame.Core.Api.Transmittal; using Milimoe.FunGame.Core.Api.Utility; using Milimoe.FunGame.Core.Entity; using Milimoe.FunGame.Core.Interface; using Milimoe.FunGame.Core.Interface.Base; +using Milimoe.FunGame.Core.Interface.Base.Addons; +using Milimoe.FunGame.Core.Library.Common.Architecture; using Milimoe.FunGame.Core.Library.Common.Event; using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Model; @@ -30,7 +33,8 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example } /// - /// 模组:必须继承基类: + /// GameModule 是用于客户端的模组。每个模组都有一个对应的服务器模组,可以简单理解为“一种游戏模式” + /// 必须继承基类: /// 继承事件接口并实现其方法来使模组生效。例如继承: /// public class ExampleGameModule : GameModule, IGamingUpdateInfoEvent @@ -78,8 +82,7 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example public override void StartUI(params object[] args) { - // 如果你是一个WPF或者Winform项目,可以在这里启动你的界面 - // 如果没有,则不需要重写此方法 + /// 如果模组不依附 类启动,或者没有UI,则不需要重写此方法 } public void GamingUpdateInfoEvent(object sender, GamingEventArgs e, Dictionary data) @@ -112,7 +115,7 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example /// 模组服务器:必须继承基类: /// 使用switch块分类处理 。 /// - public class ExampleGameModuleServer : GameModuleServer + public class ExampleGameModuleServer : GameModuleServer, IHotReloadAware { /// /// 注意:服务器模组的名称必须和模组名称相同。除非你指定了 @@ -137,6 +140,8 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example { public GamingObject GamingObject { get; } = obj; public List ConnectedUser { get; } = []; + public List CharactersForPick { get; } = []; + public Dictionary UserCharacters { get; } = []; public Dictionary> UserData { get; } = []; } @@ -184,48 +189,24 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example await SendGamingMessage(obj.All.Values, GamingType.UpdateInfo, data); // 新建一个线程等待所有玩家确认,如果超时则取消游戏,30秒 - CancellationTokenSource cts = new(); - CancellationToken ct = cts.Token; - Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(30), ct); - - Task completionTask = Task.Run(async () => + // 每200ms确认一次,不需要太频繁 + await WaitForUsers(30, async () => { - while (!ct.IsCancellationRequested) + if (worker.ConnectedUser.Count == obj.Users.Count) { - if (worker.ConnectedUser.Count == obj.Users.Count) - { - Controller.WriteLine("所有玩家都已经连接。"); - return; - } - // 每200ms确认一次,不需要太频繁 - await Task.Delay(200); + Controller.WriteLine("所有玩家都已经连接。"); + return true; } - }, ct); - - // 等待完成或超时 - Task completedTask = await Task.WhenAny(completionTask, timeoutTask); - - if (completedTask == timeoutTask) + return false; + }, 200, async () => { Controller.WriteLine("等待玩家连接超时,放弃该局游戏!", LogLevel.Warning); - cts.Cancel(); - - // 通知已连接的玩家 - Dictionary timeoutData = new() - { - { "msg", "由于等待超时,游戏已取消!" } - }; - // 结束 - SendEndGame(obj); - worker.ConnectedUser.Clear(); - Workers.Remove(obj.Room.Roomid, out _); - } - else + await CancelGame(obj, worker, "由于等待超时,游戏已取消!"); + }, async () => { - cts.Cancel(); - } - - cts.Dispose(); + // 所有玩家都连接完毕了,可以建立一个回合制游戏了 + await StartGame(obj, worker); + }); } public override async Task> GamingMessageHandler(IServerModel model, GamingType type, Dictionary data) @@ -239,17 +220,45 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example switch (type) { case GamingType.Connect: - // 编写处理“连接”命令的逻辑 - // 如果需要处理客户端传递的参数:获取与客户端约定好的参数key对应的值 - string un = NetworkUtility.JsonDeserializeFromDictionary(data, "username") ?? ""; - Guid token = NetworkUtility.JsonDeserializeFromDictionary(data, "connect_token"); - if (un == username && worker.UserData.TryGetValue(username, out Dictionary? value) && value != null && (value["connect_token"]?.Equals(token) ?? false)) { - worker.ConnectedUser.Add(obj.Users.Where(u => u.Username == username).First()); - Controller.WriteLine(username + " 已经连接。"); + // 编写处理“连接”命令的逻辑 + // 如果需要处理客户端传递的参数:获取与客户端约定好的参数key对应的值 + string un = NetworkUtility.JsonDeserializeFromDictionary(data, "username") ?? ""; + Guid token = NetworkUtility.JsonDeserializeFromDictionary(data, "connect_token"); + if (un == username && worker.UserData.TryGetValue(username, out Dictionary? value) && value != null && (value["connect_token"]?.Equals(token) ?? false)) + { + worker.ConnectedUser.Add(obj.Users.Where(u => u.Username == username).First()); + Controller.WriteLine(username + " 已经连接。"); + } + else Controller.WriteLine(username + " 确认连接失败!", LogLevel.Warning); + break; + } + case GamingType.PickCharacter: + { + // 客户端选完了角色这里就要处理了 + long id = NetworkUtility.JsonDeserializeFromDictionary(data, "id"); + if (worker.CharactersForPick.FirstOrDefault(c => c.Id == id) is Character character) + { + // 如果有人选一样的,你还没有做特殊处理的话,为了防止意外,最好复制一份 + worker.UserCharacters[username] = character.Copy(); + } + break; + } + case GamingType.Skill: + { + string e = NetworkUtility.JsonDeserializeFromDictionary(data, "event") ?? ""; + if (e.Equals("SelectSkillTargets", StringComparison.CurrentCultureIgnoreCase)) + { + long caster = NetworkUtility.JsonDeserializeFromDictionary(data, "caster"); + long[] targets = NetworkUtility.JsonDeserializeFromDictionary(data, "targets") ?? []; + // 接收客户端传来的目标序号并记录 + if (worker.UserData.TryGetValue(username, out Dictionary? value) && value != null) + { + value.Add("SkillTargets", targets); + } + } + break; } - else Controller.WriteLine(username + " 确认连接失败!", LogLevel.Warning); - break; default: await Task.Delay(1); break; @@ -258,9 +267,307 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example return result; } + private async Task CancelGame(GamingObject obj, ModuleServerWorker worker, string reason) + { + // 通知所有玩家 + await SendAllTextMessage(obj, reason); + // 结束 + SendEndGame(obj); + worker.ConnectedUser.Clear(); + Workers.Remove(obj.Room.Roomid, out _); + } + + private async Task WaitForUsers(int waitSeconds, Func> waitSomething, int delay, Func onTimeout, Func onCompleted) + { + // 这是一个用于等待的通用辅助方法 + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(waitSeconds)); + CancellationToken ct = cts.Token; + + while (!ct.IsCancellationRequested) + { + try + { + if (await waitSomething()) + { + await onCompleted(); + return; + } + await Task.Delay(delay, ct); + } + catch (System.Exception e) when (e is not OperationCanceledException) + { + Controller.Error(e); + await onTimeout(); + return; + } + } + + // 异常和超时都走超时逻辑 + await onTimeout(); + } + + private async Task StartGame(GamingObject obj, ModuleServerWorker worker) + { + Dictionary characters = []; + List characterPickeds = []; + Dictionary data = []; + + // 首先,让玩家们选择角色 + // 需要一个待选的角色池 + // 这些角色可以从工厂中获取,比如: + Character character1 = Factory.OpenFactory.GetInstance(1, "", []); + worker.CharactersForPick.Add(character1); + // 或者在什么地方已经有个列表?则使用复制方法 + if (ModuleLoader != null && ModuleLoader.Characters.Count > 0) + { + CharacterModule characterModule = ModuleLoader.Characters.Values.First(); + Character character2 = characterModule.Characters.Values.FirstOrDefault()?.Copy() ?? Factory.GetCharacter(); + if (character2.Id > 0) + { + worker.CharactersForPick.Add(character2); + } + } + // 传整个对象或者id都可以,看你心情,推荐用id,轻量,方便 + data["list"] = worker.CharactersForPick.Select(c => c.Id); + await SendGamingMessage(obj.All.Values, GamingType.PickCharacter, data); + + // 依然等待 + await WaitForUsers(30, async () => + { + if (worker.UserCharacters.Count == obj.Users.Count) + { + Controller.WriteLine("所有玩家都已经完成选择。"); + return true; + } + return false; + }, 200, async () => + { + await CancelGame(obj, worker, "由于等待超时,游戏已取消!"); + }, async () => + { + try + { + // 得到一个最终列表 + List finalList = [.. worker.UserCharacters.Values]; + + // 这里我们可以随意对角色们进行升级和赋能 + int clevel = 60; + int slevel = 6; + int mlevel = 8; + + foreach (Character c in finalList) + { + c.Level = clevel; + c.NormalAttack.Level = mlevel; + // 假设要给所有角色发一个编号为1的技能 + Skill s = Factory.OpenFactory.GetInstance(1, "", []); + s.Level = slevel; + c.Skills.Add(s); + } + + // 重点,创建一个战斗队列 + // 注意,整个游戏中,finalList及其内部角色对象的引用始终不变,请放心使用 + MixGamingQueue queue = new(finalList, (str) => + { + // 战斗日志可以直接通过传输信息的方式输出回客户端 + _ = SendAllTextMessage(obj, str); + }); + + // 如果你需要开启战棋地图模式 + GameMap? map = GameModuleDepend.Maps.FirstOrDefault(); + if (map != null) + { + queue.LoadGameMap(map); + } + + // 关键,监听任何事件 + // 在客户端中,通过事件可以很方便地对UI进行操作以同步界面状态,而在服务端则需要套一层网络层 + //queue.TurnStartEvent += Queue_TurnStartEvent; + //queue.DecideActionEvent += Queue_DecideActionEvent; + //queue.SelectNormalAttackTargetsEvent += Queue_SelectNormalAttackTargetsEvent; + //queue.SelectSkillEvent += Queue_SelectSkillEvent; + //queue.SelectNonDirectionalSkillTargetsEvent += Queue_SelectNonDirectionalSkillTargetsEvent; + //queue.SelectItemEvent += Queue_SelectItemEvent; + //queue.QueueUpdatedEvent += Queue_QueueUpdatedEvent; + //queue.TurnEndEvent += Queue_TurnEndEvent; + + // 我们示范两个事件,一是选择技能目标,需要和客户端交互的事件 + queue.SelectSkillTargetsEvent += (queue, caster, skill, enemys, teammates, castRange) => + { + /// 如果你的逻辑都写在 里就不用这么麻烦每次都传 obj 和 worker 了。 + return Queue_SelectSkillTargetsEvent(worker, caster, skill, enemys, teammates, castRange); + }; + + // 二是角色行动完毕,需要通知客户端更新状态的事件 + queue.CharacterActionTakenEvent += Queue_CharacterActionTakenEvent; + + // 战棋地图模式需要额外绑定的事件(如果你在map类里没有处理的话,这里还可以处理) + if (queue.Map != null) + { + //queue.SelectTargetGridEvent += Queue_SelectTargetGridEvent; + //queue.CharacterMoveEvent += Queue_CharacterMoveEvent; + } + + queue.InitActionQueue(); + // 这里我们仅演示自动化战斗,指令战斗还需要实现其他的消息处理类型/事件 + // 自动化战斗时上述绑定的事件可能不会触发,参见GamingQueue的内部实现 + queue.SetCharactersToAIControl(cancel: false, finalList); + + // 总游戏时长 + double totalTime = 0; + // 总死亡数 + int deaths = 0; + + // 总回合数 + int max = 999; + int i = 1; + while (i < max) + { + if (i == (max - 1)) + { + // 为了防止回合数超标,游戏近乎死局,可以设置一个上限,然后随便让一个人赢 + await SendAllTextMessage(obj, $"=== 终局审判 ==="); + Dictionary hp = []; + foreach (Character c in finalList) + { + hp.TryAdd(c, Calculation.Round4Digits(c.HP / c.MaxHP)); + } + double maxhp = hp.Values.Max(); + Character winner = hp.Keys.Where(c => hp[c] == maxhp).First(); + await SendAllTextMessage(obj, "[ " + winner + " ] 成为了天选之人!!"); + foreach (Character c in finalList.Where(c => c != winner && c.HP > 0)) + { + await SendAllTextMessage(obj, "[ " + winner + " ] 对 [ " + c + " ] 造成了 99999999999 点真实伤害。"); + queue.DeathCalculation(winner, c); + } + queue.EndGameInfo(winner); + break; + } + + // 检查是否有角色可以行动 + Character? characterToAct = queue.NextCharacter(); + + // 处理回合 + if (characterToAct != null) + { + await SendAllTextMessage(obj, $"=== Round {i++} ==="); + await SendAllTextMessage(obj, "现在是 [ " + characterToAct + " ] 的回合!"); + + if (queue.Queue.Count == 0) + { + break; + } + + bool isGameEnd = queue.ProcessTurn(characterToAct); + if (isGameEnd) + { + break; + } + + queue.DisplayQueue(); + } + + // 时间流逝,这样能知道下一个是谁可以行动 + totalTime += queue.TimeLapse(); + + if (queue.Eliminated.Count > deaths) + { + deaths = queue.Eliminated.Count; + } + } + + await SendAllTextMessage(obj, "--- End ---"); + await SendAllTextMessage(obj, "总游戏时长:" + Calculation.Round2Digits(totalTime)); + + // 赛后统计,充分利用 GamingQueue 提供的功能 + await SendAllTextMessage(obj, "=== 伤害排行榜 ==="); + int top = finalList.Count; + int count = 1; + foreach (Character character in queue.CharacterStatistics.OrderByDescending(d => d.Value.TotalDamage).Select(d => d.Key)) + { + StringBuilder builder = new(); + CharacterStatistics stats = queue.CharacterStatistics[character]; + builder.AppendLine($"{count++}. [ {character.ToStringWithLevel()} ] ({stats.Kills} / {stats.Assists})"); + builder.AppendLine($"存活时长:{stats.LiveTime} / 存活回合数:{stats.LiveRound} / 行动回合数:{stats.ActionTurn} / 总计决策数:{stats.TurnDecisions} / 总计决策点:{stats.UseDecisionPoints}"); + builder.AppendLine($"总计伤害:{stats.TotalDamage} / 总计物理伤害:{stats.TotalPhysicalDamage} / 总计魔法伤害:{stats.TotalMagicDamage}"); + builder.AppendLine($"总承受伤害:{stats.TotalTakenDamage} / 总承受物理伤害:{stats.TotalTakenPhysicalDamage} / 总承受魔法伤害:{stats.TotalTakenMagicDamage}"); + builder.Append($"每秒伤害:{stats.DamagePerSecond} / 每回合伤害:{stats.DamagePerTurn}"); + + await SendAllTextMessage(obj, builder.ToString()); + } + } + catch (System.Exception e) + { + TXTHelper.AppendErrorLog(e.ToString()); + Controller.Error(e); + } + finally + { + // 结束 + SendEndGame(obj); + worker.ConnectedUser.Clear(); + Workers.Remove(obj.Room.Roomid, out _); + } + }); + } + + private List Queue_SelectSkillTargetsEvent(ModuleServerWorker worker, Character caster, Skill skill, List enemys, List teammates, List castRange) + { + // 这是一个需要与客户端交互的事件,其他的选择事件与之做法相同 + // SyncAwaiter是一个允许同步方法安全等待异步任务完成的工具类 + return SyncAwaiter.WaitResult(RequestClientSelectSkillTargets(worker, caster, skill, enemys, teammates, castRange)); + } + + private async Task> RequestClientSelectSkillTargets(ModuleServerWorker worker, Character caster, Skill skill, List enemys, List teammates, List castRange) + { + List selectTargets = []; + Dictionary data = []; + data.Add("event", "SelectSkillTargets"); + data.Add("caster", caster.Id); + data.Add("skill", skill.Id); + data.Add("enemys", enemys.Select(c => c.Id)); + data.Add("teammates", teammates.Select(c => c.Id)); + data.Add("castRange", castRange.Select(g => g.Id)); + await SendGamingMessage(_clientModels, GamingType.Skill, data); + await WaitForUsers(30, async () => + { + string username = worker.UserCharacters.FirstOrDefault(kv => kv.Value == caster).Key; + return worker.UserData.TryGetValue(username, out Dictionary? value) && value != null && value.ContainsKey("SkillTargets"); + }, 200, async () => await Task.CompletedTask, async () => + { + string username = worker.UserCharacters.FirstOrDefault(kv => kv.Value == caster).Key; + if (worker.UserData.TryGetValue(username, out Dictionary? value) && value != null && value.TryGetValue("SkillTargets", out object? value2) && value2 is long[] targets) + { + selectTargets.AddRange(worker.UserCharacters.Values.Where(c => targets.Contains(c.Id))); + } + }); + return selectTargets; + } + + private void Queue_CharacterActionTakenEvent(GamingQueue queue, Character actor, DecisionPoints dp, CharacterActionType type, RoundRecord record) + { + Dictionary data = []; + data.Add("event", "CharacterActionTaken"); + data.Add("actor", actor.Id); + data.Add("dp", dp); + data.Add("type", type); + // 通知就行,无需等待 + _ = SendGamingMessage(_clientModels, GamingType.Round, data); + } + + private async Task SendAllTextMessage(GamingObject obj, string str) + { + // 工具方法,向所有人推送文本消息 + Dictionary data = []; + data.Add("showmessage", true); + data.Add("msg", str); + await SendGamingMessage(obj.All.Values, GamingType.UpdateInfo, data); + } + protected HashSet _clientModels = []; /// + /// 匿名服务器允许客户端不经过FunGameServer的登录验证就能建立一个游戏模组连接 /// 匿名服务器示例 /// /// @@ -268,7 +575,7 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example /// public override bool StartAnonymousServer(IServerModel model, Dictionary data) { - // 可以做验证处理 + // 可以做验证处理(这只是个演示,具体实现只需要双方约定,收发什么都无所谓) string access_token = NetworkUtility.JsonDeserializeFromDictionary(data, "access_token") ?? ""; if (access_token == "approval_access_token") { @@ -304,6 +611,24 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example return result; } + + /// + /// 热更新示例:必须实现 接口才会被热更新模式加载这个模组 + /// 如果想要实现端运行的所有模组都能热更新,那么这些模组都必须实现了这个接口(包括 等等……) + /// + public void OnBeforeUnload() + { + // 这个方法会在模组被卸载前调用,因此,这里要清理一些状态,让框架可以正确卸载模组 + // 假设,这是个匿名服务器,因此它需要清理匿名连接 + GamingObjects.Clear(); + _ = Send(_clientModels, SocketMessageType.EndGame, Factory.GetRoom(), Factory.GetUser()); + IServerModel[] models = [.. _clientModels]; + foreach (IServerModel model in models) + { + model.NowGamingServer = null; + CloseAnonymousServer(model); + } + } } /// @@ -385,6 +710,22 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example return dict; } } + + protected override Factory.EntityFactoryDelegate CharacterFactory() + { + // 上面示例用 Characters 是预定义的 + // 这里的工厂模式则是根据传进来的参数定制生成角色,只要重写这个方法就能注册工厂了 + return (id, name, args) => + { + return null; + }; + } + + public static Character CreateCharacter(long id, string name, Dictionary args) + { + // 注册工厂后,后续创建角色只需要这样调用 + return Factory.OpenFactory.GetInstance(id, name, args); + } } /// @@ -405,14 +746,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example get { Dictionary dict = []; - // 技能应该在GameModule中新建类继承Skill实现,再自行构造。 + /// 技能应该在新建类继承Skill实现,再自行构造并加入此列表。 + /// 技能的实现示例参见: return dict; } } protected override Factory.EntityFactoryDelegate SkillFactory() { - // 注册一个工厂,根据id和name,返回一个你继承实现了的类对象。 + // 注册一个工厂,根据id和name,返回一个你继承实现了的类对象。所有的工厂使用方法参考 Character,都是一样的 return (id, name, args) => { return null; @@ -423,6 +765,18 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example { return (id, name, args) => { + // 以下是一个示例,实际开发中 id,name,args 怎么处置,看你心情 + Skill? skill = null; + if (args.TryGetValue("skill", out object? value) && value is Skill s) + { + skill = s; + } + skill ??= new OpenSkill(id, name, args); + /// 如 中所说,特效需要在工厂中注册,方便重用 + if (id == 1001) + { + return new ExampleOpenEffectExATK2(skill, args); + } return null; }; } @@ -446,7 +800,8 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example get { Dictionary dict = []; - // 物品应该在GameModule中新建类继承Item实现,再自行构造。 + /// 物品应该新建类继承Item实现,再自行构造并加入此列表。 + /// 物品的实现示例参见: return dict; } } diff --git a/Library/Common/Addon/Example/ExampleItem.cs b/Library/Common/Addon/Example/ExampleItem.cs new file mode 100644 index 0000000..8cdf6c2 --- /dev/null +++ b/Library/Common/Addon/Example/ExampleItem.cs @@ -0,0 +1,189 @@ +using Milimoe.FunGame.Core.Api.Utility; +using Milimoe.FunGame.Core.Entity; +using Milimoe.FunGame.Core.Library.Constant; + +namespace Milimoe.FunGame.Core.Library.Common.Addon.Example +{ + public class ExampleItem : Item + { + public override long Id => 1; + public override string Name => "ExampleItem"; + public override string Description => $"{Skills.Passives.First().Description}{Skills.Passives.Last().Name}:{Skills.Passives.Last().Description}"; + public override string BackgroundStory => "Item's Background Story"; + public override QualityType QualityType => QualityType.Gold; + public override WeaponType WeaponType => WeaponType.Staff; + + public ExampleItem(Character? character = null) : base(ItemType.Weapon) + { + // 如果属性不支持重写,可以在构造函数中初始化 + Price = 0; + IsSellable = false; + IsTradable = false; + IsLock = true; + // 作为装备物品,添加被动技能是必要的。技能一定要设置等级大于0,否则不会生效 + Skills.Passives.Add(new ExampleItemSkill(character, this)); + Skills.Passives.Add(new ExamplePassiveSkill(character) + { + Level = 1 + }); + // 也可以添加主动技能 + Skills.Active = new ExampleNonDirectionalSkill2(character) + { + Level = 4 + }; + } + } + + public class ExampleItemSkill : Skill + { + public override long Id => 6; + public override string Name => "ExampleItemSkill"; + public override string Description => string.Join("", Effects.Select(e => e.Description)); + + private readonly double 攻击力加成 = 0.46; + + public ExampleItemSkill(Character? character = null, Item? item = null) : base(SkillType.Passive, character) + { + Level = 1; + Item = item; + Dictionary values = new() + { + { "exatk", 攻击力加成 } + }; + Effects.Add(new ExampleOpenEffectExATK2(this, values, character)); + } + + public override IEnumerable AddPassiveEffectToCharacter() + { + return Effects; + } + } + + public class ExampleOpenEffectExATK2 : Effect + { + public override long Id => 1001; // 赋予独特ID可以方便重用(在SkillModule的工厂方法中注册有利于动态创建) + public override string Name { get; set; } = "攻击力加成"; + public override string Description => $"{(ActualBonus >= 0 ? "增加" : "减少")}角色 {Math.Abs(BonusFactor) * 100:0.##}% [ {(ActualBonus == 0 ? "基于基础攻击力" : $"{Math.Abs(ActualBonus):0.##}")} ] 点攻击力。" + (Source != null && (Skill.Character != Source || Skill is not OpenSkill) ? $"来自:[ {Source} ]" + (Skill.Item != null ? $" 的 [ {Skill.Item.Name} ]" : (Skill is OpenSkill ? "" : $" 的 [ {Skill.Name} ]")) : ""); + public double Value => ActualBonus; + + private readonly double BonusFactor = 0; + private double ActualBonus = 0; + + public override void OnEffectGained(Character character) + { + if (Durative && RemainDuration == 0) + { + RemainDuration = Duration; + } + else if (RemainDurationTurn == 0) + { + RemainDurationTurn = DurationTurn; + } + ActualBonus = character.BaseATK * BonusFactor; + character.ExATKPercentage += BonusFactor; + } + + public override void OnEffectLost(Character character) + { + character.ExATKPercentage -= BonusFactor; + } + + public override void OnAttributeChanged(Character character) + { + // 刷新加成 + OnEffectLost(character); + OnEffectGained(character); + } + + public ExampleOpenEffectExATK2(Skill skill, Dictionary args, Character? source = null) : base(skill, args) + { + EffectType = EffectType.Item; + GamingQueue = skill.GamingQueue; + Source = source; + if (Values.Count > 0) + { + // 如果希望技能可以动态读取参数和创建,就这么写 + string key = Values.Keys.FirstOrDefault(s => s.Equals("exatk", StringComparison.CurrentCultureIgnoreCase)) ?? ""; + if (key.Length > 0 && double.TryParse(Values[key].ToString(), out double exATK)) + { + BonusFactor = exATK; + } + } + } + } + + public class ExampleOpenItemByJson + { + public static Item CreateAJsonItem() + { + // 演示使用JSON动态创建物品 + string json = @" + { + ""Id"": 10001, + ""Name"": ""木杖"", + ""Description"": ""增加角色 20 点攻击力。"", + ""BackgroundStory"": ""魔法使的起点。"", + ""ItemType"": 1, + ""WeaponType"": 8, + ""QualityType"": 0, + ""Skills"": { + ""Active"": null, + ""Passives"": [ + { + ""Id"": 2001, + ""Name"": ""木杖"", + ""SkillType"": 3, + ""Effects"": [ + { + ""Id"": 1001, + ""exatk"": 20 + } + ] + } + ] + } + }"; + /// 如果想了解JSON结构,参见 + + /// 属性和值解释: + /// Active 的值是一个技能对象;Passives 则是一个技能对象的数组 + /// 这里的技能是是动态创建的,Id可以随便填一个没有经过编码的 + /// SkillType = 3 代表枚举 + /// Effects 中传入一个特效对象的数组,其JSON结构参见 + + /// 通常,框架要求所有的特效都要有一个编码的类并经过工厂注册,这样才能正常的动态构建对象 + /// 没有在转换器上出现的属性,都会进入 字典,特效可以自行解析,就像上面的 ExATK2 一样 + + /// 如果1001这个特效已经过工厂注册,那么它的工作流程如下: + Skill skill = new OpenSkill(2001, "木杖", []); + Effect effect = Factory.OpenFactory.GetInstance(1001, "", new() + { + { "skill", skill }, + { "exatk", 20 } + }); + skill.Effects.Add(effect); + Item item = new OpenItem(10001, "木杖", []) + { + Description = "增加角色 20 点攻击力。", + BackgroundStory = "魔法使的起点。", + WeaponType = WeaponType.Staff, + QualityType = QualityType.White, + }; + item.Skills.Passives.Add(skill); + + /// 以下代码等效于上述代码 + item = NetworkUtility.JsonDeserialize(json) ?? Factory.GetItem(); + + /// 如果你有一个JSON文件专门用来定义这些动态物品,可以这样加载 + /// 此方法使用 配置文件读取器 + Dictionary exItems = Factory.GetGameModuleInstances("module_name", "file_name"); + if (exItems.Count > 0) + { + item = exItems.Values.First(); + } + + /// 不止物品,角色和技能都支持动态创建,在工厂中注册较为关键,既能享受动态数值,也能享受编码的具体逻辑 + return item; + } + } +} diff --git a/Library/Common/Addon/Example/ExampleSkill.cs b/Library/Common/Addon/Example/ExampleSkill.cs new file mode 100644 index 0000000..c443e1d --- /dev/null +++ b/Library/Common/Addon/Example/ExampleSkill.cs @@ -0,0 +1,386 @@ +using Milimoe.FunGame.Core.Api.Utility; +using Milimoe.FunGame.Core.Entity; +using Milimoe.FunGame.Core.Library.Constant; +using Milimoe.FunGame.Core.Model; + +namespace Milimoe.FunGame.Core.Library.Common.Addon.Example +{ + /// + /// 非指向性技能示例1:迷踪步 + /// 立即将角色传送到范围内的任意一个未被角色占据的地点 + /// 类型:战技 + /// + public class ExampleNonDirectionalSkill1 : Skill + { + public override long Id => 1; + public override string Name => "迷踪步"; + public override string Description => string.Join("", Effects.Select(e => e.Description)); + public override string DispelDescription => string.Join("", Effects.Select(e => e.DispelDescription)); + public override double EPCost => 25; + public override double CD => 35 - 1.5 * Level; + public override double HardnessTime { get; set; } = 3; + public override bool IsNonDirectional => true; + public override bool CanSelectSelf => true; + public override bool CanSelectEnemy => false; + public override bool CanSelectTeammate => false; + public override int CanSelectTargetRange => 0; + public override bool SelectIncludeCharacterGrid => false; + public override bool AllowSelectNoCharacterGrid => true; + + public ExampleNonDirectionalSkill1(Character? character = null) : base(SkillType.Skill, character) + { + CastRange = 9; + // 所有的技能特效,如果能直接 new,建议就直接 new,提高性能和可读性(工厂效率低且不好调试,工厂更偏向于动态创建技能,而对于编码实现的技能来说,怎么简单怎么来) + Effects.Add(new ExampleNonDirectionalSkill1Effect(this)); + } + } + + /// + /// 注意:特效包含于技能之中,多个特效组合成一个技能 + /// + /// + public class ExampleNonDirectionalSkill1Effect(Skill skill) : Effect(skill) + { + public override long Id => Skill.Id; + public override string Name => Skill.Name; + public override string Description => $"立即将角色传送到范围内的任意{Skill.TargetDescription()}。"; + public override string DispelDescription => ""; + + public override void OnSkillCasted(Character caster, List targets, List grids, Dictionary others) + { + // 只有开启了地图模式才有效 + if (GamingQueue?.Map is GameMap map && grids.Count > 0) + { + map.CharacterMove(caster, map.GetCharacterCurrentGrid(caster), grids[0]); + } + } + } + + /// + /// 主动技能特效示例:基于攻击力的伤害(带基础伤害) + /// + public class ExampleDamageBasedOnATKWithBasicDamage : Effect + { + public override long Id => Skill.Id; + public override string Name => Skill.Name; + public override string Description => $"对{Skill.TargetDescription()}造成 {BaseDamage:0.##} + {ATKCoefficient * 100:0.##}% 攻击力 [ {Damage:0.##} ] 点{CharacterSet.GetDamageTypeName(DamageType, MagicType)}。"; + + private double BaseDamage => Skill.Level > 0 ? BaseNumericDamage + BaseNumericDamageLevelGrowth * (Skill.Level - 1) : BaseNumericDamage; + private double ATKCoefficient => Skill.Level > 0 ? BaseATKCoefficient + BaseATKCoefficientLevelGrowth * (Skill.Level - 1) : BaseATKCoefficient; + private double Damage => BaseDamage + (ATKCoefficient * Skill.Character?.ATK ?? 0); + private double BaseNumericDamage { get; set; } = 100; + private double BaseNumericDamageLevelGrowth { get; set; } = 50; + private double BaseATKCoefficient { get; set; } = 0.2; + private double BaseATKCoefficientLevelGrowth { get; set; } = 0.2; + private DamageType DamageType { get; set; } = DamageType.Magical; + + public ExampleDamageBasedOnATKWithBasicDamage(Skill skill, double baseNumericDamage, double baseNumericDamageLevelGrowth, double baseATKCoefficient, double baseATKCoefficientLevelGrowth, DamageType damageType = DamageType.Magical, MagicType magicType = MagicType.None) : base(skill) + { + GamingQueue = skill.GamingQueue; + BaseNumericDamage = baseNumericDamage; + BaseNumericDamageLevelGrowth = baseNumericDamageLevelGrowth; + BaseATKCoefficient = baseATKCoefficient; + BaseATKCoefficientLevelGrowth = baseATKCoefficientLevelGrowth; + DamageType = damageType; + MagicType = magicType; + } + + public override void OnSkillCasted(Character caster, List targets, List grids, Dictionary others) + { + foreach (Character enemy in targets) + { + DamageToEnemy(caster, enemy, DamageType, MagicType, Damage); + } + // 或者: + //double damage = Damage; + //foreach (Character enemy in targets) + //{ + // DamageToEnemy(caster, enemy, DamageType, MagicType, damage); + //} + } + } + + /// + /// 非指向性技能示例2:钻石星尘 + /// 对半径为 2 格的圆形区域造成魔法伤害 + /// 类型:魔法 + /// + public class ExampleNonDirectionalSkill2 : Skill + { + public override long Id => 2; + public override string Name => "钻石星尘"; + public override string Description => string.Join("", Effects.Select(e => e.Description)); + public override double MPCost => Level > 0 ? 80 + (75 * (Level - 1)) : 80; + public override double CD => Level > 0 ? 35 + (2 * (Level - 1)) : 35; + public override double CastTime => 9; + public override double HardnessTime { get; set; } = 6; + public override int CanSelectTargetCount + { + get + { + return Level switch + { + 4 or 5 or 6 => 2, + 7 or 8 => 3, + _ => 1 + }; + } + } + public override bool IsNonDirectional => true; + public override SkillRangeType SkillRangeType => SkillRangeType.Circle; + public override int CanSelectTargetRange => 2; + public override double MagicBottleneck => 35 + 24 * (Level - 1); + + public ExampleNonDirectionalSkill2(Character? character = null) : base(SkillType.Magic, character) + { + Effects.Add(new ExampleDamageBasedOnATKWithBasicDamage(this, 20, 20, 0.03, 0.02, DamageType.Magical)); + } + } + + /// + /// 指向性技能示例:全力一击 + /// 对目标造成物理伤害并打断施法 + /// 类型:战技 + /// + public class ExampleSkill : Skill + { + public override long Id => 3; + public override string Name => "全力一击"; + public override string Description => string.Join("", Effects.Select(e => e.Description)); + public override string DispelDescription => string.Join("", Effects.Select(e => e.Description)); + public override string ExemptionDescription => Effects.Count > 0 ? Effects.First(e => e is ExampleInterruptCastingEffect).ExemptionDescription : ""; + public override double EPCost => 60; + public override double CD => 20; + public override double HardnessTime { get; set; } = 8; + // 豁免检定有两种方式,通过直接设置技能的属性可自动触发豁免,但是豁免成功让整个技能都失效,包括伤害(或其他 Effects),另一种方式比较安全,但需要手动调用方法,看下面 + public override Effect? EffectForExemptionCheck => Effects.FirstOrDefault(e => e is ExampleInterruptCastingEffect); + + public ExampleSkill(Character? character = null) : base(SkillType.Skill, character) + { + Effects.Add(new ExampleDamageBasedOnATKWithBasicDamage(this, 65, 65, 0.09, 0.04, DamageType.Physical)); + Effects.Add(new ExampleInterruptCastingEffect(this)); + } + } + + public class ExampleInterruptCastingEffect : Effect + { + public override long Id => Skill.Id; + public override string Name => Skill.Name; + public override string Description => $"对{Skill.TargetDescription()}施加打断施法效果:中断其正在进行的吟唱。"; + public override EffectType EffectType => EffectType.InterruptCasting; + + public ExampleInterruptCastingEffect(Skill skill) : base(skill) + { + GamingQueue = skill.GamingQueue; + } + + public override void OnSkillCasted(Character caster, List targets, List grids, Dictionary others) + { + foreach (Character target in targets) + { + // 这是另一种豁免检定方式,在技能实现时,自行调用 CheckExemption,只对该特效有效 + if (!CheckExemption(caster, target, this)) + { + InterruptCasting(target, caster); + } + } + } + } + + /// + /// 被动技能示例:心灵之弦 + /// + public class ExamplePassiveSkill : Skill + { + public override long Id => 4; + public override string Name => "心灵之弦"; + public override string Description => Effects.Count > 0 ? Effects.First().Description : ""; + + public ExamplePassiveSkill(Character? character = null) : base(SkillType.Passive, character) + { + Effects.Add(new ExamplePassiveSkillEffect(this)); + } + + /// + /// 特别注意:被动技能必须重写此方法,否则它不会自动添加到角色身上 + /// + /// + public override IEnumerable AddPassiveEffectToCharacter() + { + return Effects; + } + } + + public class ExamplePassiveSkillEffect(Skill skill) : Effect(skill) + { + public override long Id => Skill.Id; + public override string Name => Skill.Name; + public override string Description => $"普通攻击硬直时间减少 20%。每次使用普通攻击时,额外再发动一次普通攻击,伤害特效可叠加,但伤害折减一半,冷却 {CD:0.##} {GameplayEquilibriumConstant.InGameTime}。额外普通攻击立即发动,不占用决策点配额。" + + (CurrentCD > 0 ? $"(正在冷却:剩余 {CurrentCD:0.##} {GameplayEquilibriumConstant.InGameTime})" : ""); + + /// + /// 被动技能的冷却时间可以借用技能的冷却时间( 等属性)实现,也可以内部实现,看喜好 + /// + public double CurrentCD { get; set; } = 0; + public double CD { get; set; } = 10; + private bool IsNested = false; + + // 该钩子属于伤害计算流程的特效乘区2 + public override double AlterActualDamageAfterCalculation(Character character, Character enemy, double damage, bool isNormalAttack, DamageType damageType, MagicType magicType, DamageResult damageResult, ref bool isEvaded, Dictionary totalDamageBonus) + { + if (character == Skill.Character && IsNested && isNormalAttack && damage > 0) + { + // 此方法返回的是加值 + return -(damage / 2); + } + return 0; + } + + public override void AfterDamageCalculation(Character character, Character enemy, double damage, double actualDamage, bool isNormalAttack, DamageType damageType, MagicType magicType, DamageResult damageResult) + { + if (character == Skill.Character && isNormalAttack && CurrentCD == 0 && !IsNested && GamingQueue != null && enemy.HP > 0) + { + WriteLine($"[ {character} ] 发动了{Skill.Name}!额外进行一次普通攻击!"); + CurrentCD = CD; + IsNested = true; + character.NormalAttack.Attack(GamingQueue, character, null, enemy); + } + + if (character == Skill.Character && IsNested) + { + IsNested = false; + } + } + + public override void OnTimeElapsed(Character character, double elapsed) + { + // 时间流逝时,手动减少CD。如果借用了技能的冷却时间属性,就不需要写了 + if (CurrentCD > 0) + { + CurrentCD -= elapsed; + if (CurrentCD <= 0) + { + CurrentCD = 0; + } + } + } + + public override void AlterHardnessTimeAfterNormalAttack(Character character, ref double baseHardnessTime, ref bool isCheckProtected) + { + // 普攻后调整硬直时间。ref 变量直接修改 + baseHardnessTime *= 0.8; + } + } + + /// + /// 爆发技示例:千羽瞬华 + /// 给自己加属性,并联动其他技能 + /// + public class ExampleSuperSkill : Skill + { + public override long Id => 5; + public override string Name => "千羽瞬华"; + public override string Description => Effects.Count > 0 ? Effects.First().Description : ""; + public override string DispelDescription => Effects.Count > 0 ? Effects.First().DispelDescription : ""; + public override double EPCost => 100; + public override double CD => 60; + public override double HardnessTime { get; set; } = 10; + public override bool CanSelectSelf => true; + public override bool CanSelectEnemy => false; + + public ExampleSuperSkill(Character? character = null) : base(SkillType.SuperSkill, character) + { + Effects.Add(new ExampleSuperSkillEffect(this)); + } + } + + public class ExampleSuperSkillEffect(Skill skill) : Effect(skill) + { + public override long Id => Skill.Id; + public override string Name => Skill.Name; + public override string Description => $"{Duration:0.##} {GameplayEquilibriumConstant.InGameTime}内,增加{Skill.SkillOwner()} {ATKMultiplier * 100:0.##}% 攻击力 [ {ATKBonus:0.##} ]、{PhysicalPenetrationBonus * 100:0.##}% 物理穿透和 {EvadeRateBonus * 100:0.##}% 闪避率(不可叠加),普通攻击硬直时间额外减少 20%,基于 {Coefficient * 100:0.##}% 敏捷 [ {DamageBonus:0.##} ] 强化普通攻击的伤害。在持续时间内,[ 心灵之弦 ] 的冷却时间降低至 3 {GameplayEquilibriumConstant.InGameTime}。"; + public override bool Durative => true; + public override double Duration => 30; + public override DispelledType DispelledType => DispelledType.CannotBeDispelled; + + private double Coefficient => 1.2 * (1 + 0.5 * (Skill.Level - 1)); + private double DamageBonus => Coefficient * Skill.Character?.AGI ?? 0; + private double ATKMultiplier => Skill.Level > 0 ? 0.15 + 0.03 * (Skill.Level - 1) : 0.15; + private double ATKBonus => ATKMultiplier * Skill.Character?.BaseATK ?? 0; + private double PhysicalPenetrationBonus => Skill.Level > 0 ? 0.1 + 0.03 * (Skill.Level - 1) : 0.1; + private double EvadeRateBonus => Skill.Level > 0 ? 0.1 + 0.02 * (Skill.Level - 1) : 0.1; + + // 用于保存状态和恢复 + private double ActualATKBonus = 0; + private double ActualPhysicalPenetrationBonus = 0; + private double ActualEvadeRateBonus = 0; + + public override void OnEffectGained(Character character) + { + // 记录状态并修改属性 + ActualATKBonus = ATKBonus; + ActualPhysicalPenetrationBonus = PhysicalPenetrationBonus; + ActualEvadeRateBonus = EvadeRateBonus; + character.ExATK2 += ActualATKBonus; + character.PhysicalPenetration += ActualPhysicalPenetrationBonus; + character.ExEvadeRate += ActualEvadeRateBonus; + if (character.Effects.FirstOrDefault(e => e is ExamplePassiveSkillEffect) is ExamplePassiveSkillEffect e) + { + e.CD = 3; + if (e.CurrentCD > e.CD) e.CurrentCD = e.CD; + } + } + + public override void OnEffectLost(Character character) + { + // 从记录的状态中恢复 + character.ExATK2 -= ActualATKBonus; + character.PhysicalPenetration -= ActualPhysicalPenetrationBonus; + character.ExEvadeRate -= ActualEvadeRateBonus; + if (character.Effects.FirstOrDefault(e => e is ExamplePassiveSkillEffect) is ExamplePassiveSkillEffect e) + { + e.CD = 10; + } + } + + public override CharacterActionType AlterActionTypeBeforeAction(Character character, DecisionPoints dp, CharacterState state, ref bool canUseItem, ref bool canCastSkill, ref double pUseItem, ref double pCastSkill, ref double pNormalAttack, ref bool forceAction) + { + // 对于 AI,可以提高角色的普攻积极性,调整决策偏好,这样可以充分利用技能效果 + pNormalAttack += 0.1; + return CharacterActionType.None; + } + + public override double AlterExpectedDamageBeforeCalculation(Character character, Character enemy, double damage, bool isNormalAttack, DamageType damageType, MagicType magicType, Dictionary totalDamageBonus) + { + if (character == Skill.Character && isNormalAttack) + { + return DamageBonus; + } + return 0; + } + + public override void AlterHardnessTimeAfterNormalAttack(Character character, ref double baseHardnessTime, ref bool isCheckProtected) + { + // 可以和上面的心灵之弦叠加,最终硬直时间=硬直时间*0.8*0.8 + baseHardnessTime *= 0.8; + } + + public override void OnSkillCasted(Character caster, List targets, List grids, Dictionary others) + { + ActualATKBonus = 0; + ActualPhysicalPenetrationBonus = 0; + ActualEvadeRateBonus = 0; + // 不叠加的效果通常只刷新持续时间 + RemainDuration = Duration; + // 通常,this(Effect本身)在整局战斗中都是唯一的,需要只需要判断 this 就行 + if (!caster.Effects.Contains(this)) + { + // 加也是加 this + caster.Effects.Add(this); + OnEffectGained(caster); + } + // 施加状态记录到回合日志中 + RecordCharacterApplyEffects(caster, EffectType.DamageBoost, EffectType.PenetrationBoost); + } + } +} diff --git a/Library/Common/Addon/GameMap.cs b/Library/Common/Addon/GameMap.cs index 81bb5d2..78b5b51 100644 --- a/Library/Common/Addon/GameMap.cs +++ b/Library/Common/Addon/GameMap.cs @@ -98,6 +98,11 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon } } + /// + /// 记录该模组的加载器 + /// + public GameModuleLoader? ModuleLoader { get; set; } = null; + /// /// 加载标记 /// @@ -136,6 +141,22 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon return _isLoaded; } + /// + /// 卸载模组 + /// + /// + public void UnLoad(params object[] objs) + { + foreach (Grid grid in Grids.Values) + { + grid.Characters.Clear(); + grid.Effects.Clear(); + } + Characters.Clear(); + Grids.Clear(); + GridsByCoordinate.Clear(); + } + /// /// 地图完全加载后需要做的事 /// diff --git a/Library/Common/Addon/GameModule.cs b/Library/Common/Addon/GameModule.cs index 5da8ede..5b73417 100644 --- a/Library/Common/Addon/GameModule.cs +++ b/Library/Common/Addon/GameModule.cs @@ -69,6 +69,11 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon set => _associatedServerModuleName = value; } + /// + /// 记录该模组的加载器 + /// + public GameModuleLoader? ModuleLoader { get; set; } = null; + /// /// 包含了一些常用方法的控制器 /// @@ -138,6 +143,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon return _isLoaded; } + /// + /// 卸载模组 + /// + /// + public void UnLoad(params object[] objs) + { + BindEvent(false); + } + /// /// 模组完全加载后需要做的事 /// @@ -160,8 +174,14 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon /// private void Init(params object[] objs) { - if (objs.Length > 0) _session = (Session)objs[0]; - if (objs.Length > 1) _config = (FunGameConfig)objs[1]; + if (objs.Length > 0 && objs[0] is Session session) + { + _session = session; + } + if (objs.Length > 1 && objs[1] is FunGameConfig config) + { + _config = config; + } } /// @@ -182,126 +202,246 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon /// /// 绑定事件。在后触发 /// - private void BindEvent() + private void BindEvent(bool isAdd = true) { - if (this is IGamingConnectEvent) + if (this is IGamingConnectEvent connect) { - IGamingConnectEvent bind = (IGamingConnectEvent)this; - GamingConnect += bind.GamingConnectEvent; + if (isAdd) + { + GamingConnect += connect.GamingConnectEvent; + } + else + { + GamingConnect -= connect.GamingConnectEvent; + } } - if (this is IGamingDisconnectEvent) + if (this is IGamingDisconnectEvent disconnect) { - IGamingDisconnectEvent bind = (IGamingDisconnectEvent)this; - GamingDisconnect += bind.GamingDisconnectEvent; + if (isAdd) + { + GamingDisconnect += disconnect.GamingDisconnectEvent; + } + else + { + GamingDisconnect -= disconnect.GamingDisconnectEvent; + } } - if (this is IGamingReconnectEvent) + if (this is IGamingReconnectEvent reconnect) { - IGamingReconnectEvent bind = (IGamingReconnectEvent)this; - GamingReconnect += bind.GamingReconnectEvent; + if (isAdd) + { + GamingReconnect += reconnect.GamingReconnectEvent; + } + else + { + GamingReconnect -= reconnect.GamingReconnectEvent; + } } - if (this is IGamingBanCharacterEvent) + if (this is IGamingBanCharacterEvent ban) { - IGamingBanCharacterEvent bind = (IGamingBanCharacterEvent)this; - GamingBanCharacter += bind.GamingBanCharacterEvent; + if (isAdd) + { + GamingBanCharacter += ban.GamingBanCharacterEvent; + } + else + { + GamingBanCharacter -= ban.GamingBanCharacterEvent; + } } - if (this is IGamingPickCharacterEvent) + if (this is IGamingPickCharacterEvent pick) { - IGamingPickCharacterEvent bind = (IGamingPickCharacterEvent)this; - GamingPickCharacter += bind.GamingPickCharacterEvent; + if (isAdd) + { + GamingPickCharacter += pick.GamingPickCharacterEvent; + } + else + { + GamingPickCharacter -= pick.GamingPickCharacterEvent; + } } - if (this is IGamingRandomEvent) + if (this is IGamingRandomEvent random) { - IGamingRandomEvent bind = (IGamingRandomEvent)this; - GamingRandom += bind.GamingRandomEvent; + if (isAdd) + { + GamingRandom += random.GamingRandomEvent; + } + else + { + GamingRandom -= random.GamingRandomEvent; + } } - if (this is IGamingRoundEvent) + if (this is IGamingRoundEvent round) { - IGamingRoundEvent bind = (IGamingRoundEvent)this; - GamingRound += bind.GamingRoundEvent; + if (isAdd) + { + GamingRound += round.GamingRoundEvent; + } + else + { + GamingRound -= round.GamingRoundEvent; + } } - if (this is IGamingLevelUpEvent) + if (this is IGamingLevelUpEvent levelUp) { - IGamingLevelUpEvent bind = (IGamingLevelUpEvent)this; - GamingLevelUp += bind.GamingLevelUpEvent; + if (isAdd) + { + GamingLevelUp += levelUp.GamingLevelUpEvent; + } + else + { + GamingLevelUp -= levelUp.GamingLevelUpEvent; + } } - if (this is IGamingMoveEvent) + if (this is IGamingMoveEvent move) { - IGamingMoveEvent bind = (IGamingMoveEvent)this; - GamingMove += bind.GamingMoveEvent; + if (isAdd) + { + GamingMove += move.GamingMoveEvent; + } + else + { + GamingMove -= move.GamingMoveEvent; + } } - if (this is IGamingAttackEvent) + if (this is IGamingAttackEvent attack) { - IGamingAttackEvent bind = (IGamingAttackEvent)this; - GamingAttack += bind.GamingAttackEvent; + if (isAdd) + { + GamingAttack += attack.GamingAttackEvent; + } + else + { + GamingAttack -= attack.GamingAttackEvent; + } } - if (this is IGamingSkillEvent) + if (this is IGamingSkillEvent skill) { - IGamingSkillEvent bind = (IGamingSkillEvent)this; - GamingSkill += bind.GamingSkillEvent; + if (isAdd) + { + GamingSkill += skill.GamingSkillEvent; + } + else + { + GamingSkill -= skill.GamingSkillEvent; + } } - if (this is IGamingItemEvent) + if (this is IGamingItemEvent item) { - IGamingItemEvent bind = (IGamingItemEvent)this; - GamingItem += bind.GamingItemEvent; + if (isAdd) + { + GamingItem += item.GamingItemEvent; + } + else + { + GamingItem -= item.GamingItemEvent; + } } - if (this is IGamingMagicEvent) + if (this is IGamingMagicEvent magic) { - IGamingMagicEvent bind = (IGamingMagicEvent)this; - GamingMagic += bind.GamingMagicEvent; + if (isAdd) + { + GamingMagic += magic.GamingMagicEvent; + } + else + { + GamingMagic -= magic.GamingMagicEvent; + } } - if (this is IGamingBuyEvent) + if (this is IGamingBuyEvent buy) { - IGamingBuyEvent bind = (IGamingBuyEvent)this; - GamingBuy += bind.GamingBuyEvent; + if (isAdd) + { + GamingBuy += buy.GamingBuyEvent; + } + else + { + GamingBuy -= buy.GamingBuyEvent; + } } - if (this is IGamingSuperSkillEvent) + if (this is IGamingSuperSkillEvent super) { - IGamingSuperSkillEvent bind = (IGamingSuperSkillEvent)this; - GamingSuperSkill += bind.GamingSuperSkillEvent; + if (isAdd) + { + GamingSuperSkill += super.GamingSuperSkillEvent; + } + else + { + GamingSuperSkill -= super.GamingSuperSkillEvent; + } } - if (this is IGamingPauseEvent) + if (this is IGamingPauseEvent pause) { - IGamingPauseEvent bind = (IGamingPauseEvent)this; - GamingPause += bind.GamingPauseEvent; + if (isAdd) + { + GamingPause += pause.GamingPauseEvent; + } + else + { + GamingPause -= pause.GamingPauseEvent; + } } - if (this is IGamingUnpauseEvent) + if (this is IGamingUnpauseEvent unpause) { - IGamingUnpauseEvent bind = (IGamingUnpauseEvent)this; - GamingUnpause += bind.GamingUnpauseEvent; + if (isAdd) + { + GamingUnpause += unpause.GamingUnpauseEvent; + } + else + { + GamingUnpause -= unpause.GamingUnpauseEvent; + } } - if (this is IGamingSurrenderEvent) + if (this is IGamingSurrenderEvent surrender) { - IGamingSurrenderEvent bind = (IGamingSurrenderEvent)this; - GamingSurrender += bind.GamingSurrenderEvent; + if (isAdd) + { + GamingSurrender += surrender.GamingSurrenderEvent; + } + else + { + GamingSurrender -= surrender.GamingSurrenderEvent; + } } - if (this is IGamingUpdateInfoEvent) + if (this is IGamingUpdateInfoEvent update) { - IGamingUpdateInfoEvent bind = (IGamingUpdateInfoEvent)this; - GamingUpdateInfo += bind.GamingUpdateInfoEvent; + if (isAdd) + { + GamingUpdateInfo += update.GamingUpdateInfoEvent; + } + else + { + GamingUpdateInfo -= update.GamingUpdateInfoEvent; + } } - if (this is IGamingPunishEvent) + if (this is IGamingPunishEvent punish) { - IGamingPunishEvent bind = (IGamingPunishEvent)this; - GamingPunish += bind.GamingPunishEvent; + if (isAdd) + { + GamingPunish += punish.GamingPunishEvent; + } + else + { + GamingPunish -= punish.GamingPunishEvent; + } } } diff --git a/Library/Common/Addon/GameModuleServer.cs b/Library/Common/Addon/GameModuleServer.cs index 86656bb..7ba7e40 100644 --- a/Library/Common/Addon/GameModuleServer.cs +++ b/Library/Common/Addon/GameModuleServer.cs @@ -45,6 +45,11 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon /// public virtual bool IsAnonymous { get; set; } = false; + /// + /// 记录该模组的加载器 + /// + public GameModuleLoader? ModuleLoader { get; set; } = null; + /// /// 包含了一些常用方法的控制器 /// @@ -147,6 +152,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon return _isLoaded; } + /// + /// 卸载模组 + /// + /// + public void UnLoad(params object[] objs) + { + + } + /// /// 模组完全加载后需要做的事 /// diff --git a/Library/Common/Addon/ItemModule.cs b/Library/Common/Addon/ItemModule.cs index 69ef4e2..1dc60a2 100644 --- a/Library/Common/Addon/ItemModule.cs +++ b/Library/Common/Addon/ItemModule.cs @@ -58,6 +58,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon return _isLoaded; } + /// + /// 卸载模组 + /// + /// + public void UnLoad(params object[] objs) + { + Factory.OpenFactory.UnRegisterFactory(ItemFactory()); + } + /// /// 注册工厂 /// diff --git a/Library/Common/Addon/Plugin.cs b/Library/Common/Addon/Plugin.cs index 6b2c9ef..adafa3f 100644 --- a/Library/Common/Addon/Plugin.cs +++ b/Library/Common/Addon/Plugin.cs @@ -29,6 +29,11 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon /// public abstract string Author { get; } + /// + /// 记录该插件的加载器 + /// + public PluginLoader? PluginLoader { get; set; } = null; + /// /// 包含了一些常用方法的控制器 /// @@ -79,6 +84,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon return _isLoaded; } + /// + /// 卸载模组 + /// + /// + public void UnLoad(params object[] objs) + { + BindEvent(false); + } + /// /// 插件完全加载后需要做的事 /// @@ -118,153 +132,300 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon /// /// 绑定事件。在后触发 /// - private void BindEvent() + private void BindEvent(bool isAdd = true) { - if (this is IConnectEvent) + if (this is IConnectEvent connect) { - IConnectEvent bind = (IConnectEvent)this; - BeforeConnect += bind.BeforeConnectEvent; - AfterConnect += bind.AfterConnectEvent; + if (isAdd) + { + BeforeConnect += connect.BeforeConnectEvent; + AfterConnect += connect.AfterConnectEvent; + } + else + { + BeforeConnect -= connect.BeforeConnectEvent; + AfterConnect -= connect.AfterConnectEvent; + } } - if (this is IDisconnectEvent) + if (this is IDisconnectEvent disconnect) { - IDisconnectEvent bind = (IDisconnectEvent)this; - BeforeDisconnect += bind.BeforeDisconnectEvent; - AfterDisconnect += bind.AfterDisconnectEvent; + if (isAdd) + { + BeforeDisconnect += disconnect.BeforeDisconnectEvent; + AfterDisconnect += disconnect.AfterDisconnectEvent; + } + else + { + BeforeDisconnect -= disconnect.BeforeDisconnectEvent; + AfterDisconnect -= disconnect.AfterDisconnectEvent; + } } - if (this is ILoginEvent) + if (this is ILoginEvent login) { - ILoginEvent bind = (ILoginEvent)this; - BeforeLogin += bind.BeforeLoginEvent; - AfterLogin += bind.AfterLoginEvent; + if (isAdd) + { + BeforeLogin += login.BeforeLoginEvent; + AfterLogin += login.AfterLoginEvent; + } + else + { + BeforeLogin -= login.BeforeLoginEvent; + AfterLogin -= login.AfterLoginEvent; + } } - if (this is ILogoutEvent) + if (this is ILogoutEvent logout) { - ILogoutEvent bind = (ILogoutEvent)this; - BeforeLogout += bind.BeforeLogoutEvent; - AfterLogout += bind.AfterLogoutEvent; + if (isAdd) + { + BeforeLogout += logout.BeforeLogoutEvent; + AfterLogout += logout.AfterLogoutEvent; + } + else + { + BeforeLogout -= logout.BeforeLogoutEvent; + AfterLogout -= logout.AfterLogoutEvent; + } } - if (this is IRegEvent) + if (this is IRegEvent reg) { - IRegEvent bind = (IRegEvent)this; - BeforeReg += bind.BeforeRegEvent; - AfterReg += bind.AfterRegEvent; + if (isAdd) + { + BeforeReg += reg.BeforeRegEvent; + AfterReg += reg.AfterRegEvent; + } + else + { + BeforeReg -= reg.BeforeRegEvent; + AfterReg -= reg.AfterRegEvent; + } } - if (this is IIntoRoomEvent) + if (this is IIntoRoomEvent intoRoom) { - IIntoRoomEvent bind = (IIntoRoomEvent)this; - BeforeIntoRoom += bind.BeforeIntoRoomEvent; - AfterIntoRoom += bind.AfterIntoRoomEvent; + if (isAdd) + { + BeforeIntoRoom += intoRoom.BeforeIntoRoomEvent; + AfterIntoRoom += intoRoom.AfterIntoRoomEvent; + } + else + { + BeforeIntoRoom -= intoRoom.BeforeIntoRoomEvent; + AfterIntoRoom -= intoRoom.AfterIntoRoomEvent; + } } - if (this is ISendTalkEvent) + if (this is ISendTalkEvent sendTalk) { - ISendTalkEvent bind = (ISendTalkEvent)this; - BeforeSendTalk += bind.BeforeSendTalkEvent; - AfterSendTalk += bind.AfterSendTalkEvent; + if (isAdd) + { + BeforeSendTalk += sendTalk.BeforeSendTalkEvent; + AfterSendTalk += sendTalk.AfterSendTalkEvent; + } + else + { + BeforeSendTalk -= sendTalk.BeforeSendTalkEvent; + AfterSendTalk -= sendTalk.AfterSendTalkEvent; + } } - if (this is ICreateRoomEvent) + if (this is ICreateRoomEvent createRoom) { - ICreateRoomEvent bind = (ICreateRoomEvent)this; - BeforeCreateRoom += bind.BeforeCreateRoomEvent; - AfterCreateRoom += bind.AfterCreateRoomEvent; + if (isAdd) + { + BeforeCreateRoom += createRoom.BeforeCreateRoomEvent; + AfterCreateRoom += createRoom.AfterCreateRoomEvent; + } + else + { + BeforeCreateRoom -= createRoom.BeforeCreateRoomEvent; + AfterCreateRoom -= createRoom.AfterCreateRoomEvent; + } } - if (this is IQuitRoomEvent) + if (this is IQuitRoomEvent quitRoom) { - IQuitRoomEvent bind = (IQuitRoomEvent)this; - BeforeQuitRoom += bind.BeforeQuitRoomEvent; - AfterQuitRoom += bind.AfterQuitRoomEvent; + if (isAdd) + { + BeforeQuitRoom += quitRoom.BeforeQuitRoomEvent; + AfterQuitRoom += quitRoom.AfterQuitRoomEvent; + } + else + { + BeforeQuitRoom -= quitRoom.BeforeQuitRoomEvent; + AfterQuitRoom -= quitRoom.AfterQuitRoomEvent; + } } - if (this is IChangeRoomSettingEvent) + if (this is IChangeRoomSettingEvent changeRoomSetting) { - IChangeRoomSettingEvent bind = (IChangeRoomSettingEvent)this; - BeforeChangeRoomSetting += bind.BeforeChangeRoomSettingEvent; - AfterChangeRoomSetting += bind.AfterChangeRoomSettingEvent; + if (isAdd) + { + BeforeChangeRoomSetting += changeRoomSetting.BeforeChangeRoomSettingEvent; + AfterChangeRoomSetting += changeRoomSetting.AfterChangeRoomSettingEvent; + } + else + { + BeforeChangeRoomSetting -= changeRoomSetting.BeforeChangeRoomSettingEvent; + AfterChangeRoomSetting -= changeRoomSetting.AfterChangeRoomSettingEvent; + } } - if (this is IStartMatchEvent) + if (this is IStartMatchEvent startMatch) { - IStartMatchEvent bind = (IStartMatchEvent)this; - BeforeStartMatch += bind.BeforeStartMatchEvent; - AfterStartMatch += bind.AfterStartMatchEvent; + if (isAdd) + { + BeforeStartMatch += startMatch.BeforeStartMatchEvent; + AfterStartMatch += startMatch.AfterStartMatchEvent; + } + else + { + BeforeStartMatch -= startMatch.BeforeStartMatchEvent; + AfterStartMatch -= startMatch.AfterStartMatchEvent; + } } - if (this is IStartGameEvent) + if (this is IStartGameEvent startGame) { - IStartGameEvent bind = (IStartGameEvent)this; - BeforeStartGame += bind.BeforeStartGameEvent; - AfterStartGame += bind.AfterStartGameEvent; + if (isAdd) + { + BeforeStartGame += startGame.BeforeStartGameEvent; + AfterStartGame += startGame.AfterStartGameEvent; + } + else + { + BeforeStartGame -= startGame.BeforeStartGameEvent; + AfterStartGame -= startGame.AfterStartGameEvent; + } } - if (this is IChangeProfileEvent) + if (this is IChangeProfileEvent changeProfile) { - IChangeProfileEvent bind = (IChangeProfileEvent)this; - BeforeChangeProfile += bind.BeforeChangeProfileEvent; - AfterChangeProfile += bind.AfterChangeProfileEvent; + if (isAdd) + { + BeforeChangeProfile += changeProfile.BeforeChangeProfileEvent; + AfterChangeProfile += changeProfile.AfterChangeProfileEvent; + } + else + { + BeforeChangeProfile -= changeProfile.BeforeChangeProfileEvent; + AfterChangeProfile -= changeProfile.AfterChangeProfileEvent; + } } - if (this is IChangeAccountSettingEvent) + if (this is IChangeAccountSettingEvent changeAccountSetting) { - IChangeAccountSettingEvent bind = (IChangeAccountSettingEvent)this; - BeforeChangeAccountSetting += bind.BeforeChangeAccountSettingEvent; - AfterChangeAccountSetting += bind.AfterChangeAccountSettingEvent; + if (isAdd) + { + BeforeChangeAccountSetting += changeAccountSetting.BeforeChangeAccountSettingEvent; + AfterChangeAccountSetting += changeAccountSetting.AfterChangeAccountSettingEvent; + } + else + { + BeforeChangeAccountSetting -= changeAccountSetting.BeforeChangeAccountSettingEvent; + AfterChangeAccountSetting -= changeAccountSetting.AfterChangeAccountSettingEvent; + } } - if (this is IOpenInventoryEvent) + if (this is IOpenInventoryEvent openInventory) { - IOpenInventoryEvent bind = (IOpenInventoryEvent)this; - BeforeOpenInventory += bind.BeforeOpenInventoryEvent; - AfterOpenInventory += bind.AfterOpenInventoryEvent; + if (isAdd) + { + BeforeOpenInventory += openInventory.BeforeOpenInventoryEvent; + AfterOpenInventory += openInventory.AfterOpenInventoryEvent; + } + else + { + BeforeOpenInventory -= openInventory.BeforeOpenInventoryEvent; + AfterOpenInventory -= openInventory.AfterOpenInventoryEvent; + } } - if (this is ISignInEvent) + if (this is ISignInEvent signIn) { - ISignInEvent bind = (ISignInEvent)this; - BeforeSignIn += bind.BeforeSignInEvent; - AfterSignIn += bind.AfterSignInEvent; + if (isAdd) + { + BeforeSignIn += signIn.BeforeSignInEvent; + AfterSignIn += signIn.AfterSignInEvent; + } + else + { + BeforeSignIn -= signIn.BeforeSignInEvent; + AfterSignIn -= signIn.AfterSignInEvent; + } } - if (this is IOpenStoreEvent) + if (this is IOpenStoreEvent openStore) { - IOpenStoreEvent bind = (IOpenStoreEvent)this; - BeforeOpenStore += bind.BeforeOpenStoreEvent; - AfterOpenStore += bind.AfterOpenStoreEvent; + if (isAdd) + { + BeforeOpenStore += openStore.BeforeOpenStoreEvent; + AfterOpenStore += openStore.AfterOpenStoreEvent; + } + else + { + BeforeOpenStore -= openStore.BeforeOpenStoreEvent; + AfterOpenStore -= openStore.AfterOpenStoreEvent; + } } - if (this is IBuyItemEvent) + if (this is IBuyItemEvent buyItem) { - IBuyItemEvent bind = (IBuyItemEvent)this; - BeforeBuyItem += bind.BeforeBuyItemEvent; - AfterBuyItem += bind.AfterBuyItemEvent; + if (isAdd) + { + BeforeBuyItem += buyItem.BeforeBuyItemEvent; + AfterBuyItem += buyItem.AfterBuyItemEvent; + } + else + { + BeforeBuyItem -= buyItem.BeforeBuyItemEvent; + AfterBuyItem -= buyItem.AfterBuyItemEvent; + } } - if (this is IShowRankingEvent) + if (this is IShowRankingEvent showRanking) { - IShowRankingEvent bind = (IShowRankingEvent)this; - BeforeShowRanking += bind.BeforeShowRankingEvent; - AfterShowRanking += bind.AfterShowRankingEvent; + if (isAdd) + { + BeforeShowRanking += showRanking.BeforeShowRankingEvent; + AfterShowRanking += showRanking.AfterShowRankingEvent; + } + else + { + BeforeShowRanking -= showRanking.BeforeShowRankingEvent; + AfterShowRanking -= showRanking.AfterShowRankingEvent; + } } - if (this is IUseItemEvent) + if (this is IUseItemEvent useItem) { - IUseItemEvent bind = (IUseItemEvent)this; - BeforeUseItem += bind.BeforeUseItemEvent; - AfterUseItem += bind.AfterUseItemEvent; + if (isAdd) + { + BeforeUseItem += useItem.BeforeUseItemEvent; + AfterUseItem += useItem.AfterUseItemEvent; + } + else + { + BeforeUseItem -= useItem.BeforeUseItemEvent; + AfterUseItem -= useItem.AfterUseItemEvent; + } } - if (this is IEndGameEvent) + if (this is IEndGameEvent endGame) { - IEndGameEvent bind = (IEndGameEvent)this; - BeforeEndGame += bind.BeforeEndGameEvent; - AfterEndGame += bind.AfterEndGameEvent; + if (isAdd) + { + BeforeEndGame += endGame.BeforeEndGameEvent; + AfterEndGame += endGame.AfterEndGameEvent; + } + else + { + BeforeEndGame -= endGame.BeforeEndGameEvent; + AfterEndGame -= endGame.AfterEndGameEvent; + } } } diff --git a/Library/Common/Addon/ServerPlugin.cs b/Library/Common/Addon/ServerPlugin.cs index b5bdaf0..f3c59a6 100644 --- a/Library/Common/Addon/ServerPlugin.cs +++ b/Library/Common/Addon/ServerPlugin.cs @@ -28,6 +28,11 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon /// public abstract string Author { get; } + /// + /// 记录该插件的加载器 + /// + public ServerPluginLoader? PluginLoader { get; set; } = null; + /// /// 包含了一些常用方法的控制器 /// @@ -76,6 +81,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon return _isLoaded; } + /// + /// 卸载模组 + /// + /// + public void UnLoad(params object[] objs) + { + BindEvent(false); + } + /// /// 接收服务器控制台的输入 /// @@ -102,153 +116,300 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon /// /// 绑定事件。在后触发 /// - private void BindEvent() + private void BindEvent(bool isAdd = true) { - if (this is IConnectEvent) + if (this is IConnectEvent connect) { - IConnectEvent bind = (IConnectEvent)this; - BeforeConnect += bind.BeforeConnectEvent; - AfterConnect += bind.AfterConnectEvent; + if (isAdd) + { + BeforeConnect += connect.BeforeConnectEvent; + AfterConnect += connect.AfterConnectEvent; + } + else + { + BeforeConnect -= connect.BeforeConnectEvent; + AfterConnect -= connect.AfterConnectEvent; + } } - if (this is IDisconnectEvent) + if (this is IDisconnectEvent disconnect) { - IDisconnectEvent bind = (IDisconnectEvent)this; - BeforeDisconnect += bind.BeforeDisconnectEvent; - AfterDisconnect += bind.AfterDisconnectEvent; + if (isAdd) + { + BeforeDisconnect += disconnect.BeforeDisconnectEvent; + AfterDisconnect += disconnect.AfterDisconnectEvent; + } + else + { + BeforeDisconnect -= disconnect.BeforeDisconnectEvent; + AfterDisconnect -= disconnect.AfterDisconnectEvent; + } } - if (this is ILoginEvent) + if (this is ILoginEvent login) { - ILoginEvent bind = (ILoginEvent)this; - BeforeLogin += bind.BeforeLoginEvent; - AfterLogin += bind.AfterLoginEvent; + if (isAdd) + { + BeforeLogin += login.BeforeLoginEvent; + AfterLogin += login.AfterLoginEvent; + } + else + { + BeforeLogin -= login.BeforeLoginEvent; + AfterLogin -= login.AfterLoginEvent; + } } - if (this is ILogoutEvent) + if (this is ILogoutEvent logout) { - ILogoutEvent bind = (ILogoutEvent)this; - BeforeLogout += bind.BeforeLogoutEvent; - AfterLogout += bind.AfterLogoutEvent; + if (isAdd) + { + BeforeLogout += logout.BeforeLogoutEvent; + AfterLogout += logout.AfterLogoutEvent; + } + else + { + BeforeLogout -= logout.BeforeLogoutEvent; + AfterLogout -= logout.AfterLogoutEvent; + } } - if (this is IRegEvent) + if (this is IRegEvent reg) { - IRegEvent bind = (IRegEvent)this; - BeforeReg += bind.BeforeRegEvent; - AfterReg += bind.AfterRegEvent; + if (isAdd) + { + BeforeReg += reg.BeforeRegEvent; + AfterReg += reg.AfterRegEvent; + } + else + { + BeforeReg -= reg.BeforeRegEvent; + AfterReg -= reg.AfterRegEvent; + } } - if (this is IIntoRoomEvent) + if (this is IIntoRoomEvent intoRoom) { - IIntoRoomEvent bind = (IIntoRoomEvent)this; - BeforeIntoRoom += bind.BeforeIntoRoomEvent; - AfterIntoRoom += bind.AfterIntoRoomEvent; + if (isAdd) + { + BeforeIntoRoom += intoRoom.BeforeIntoRoomEvent; + AfterIntoRoom += intoRoom.AfterIntoRoomEvent; + } + else + { + BeforeIntoRoom -= intoRoom.BeforeIntoRoomEvent; + AfterIntoRoom -= intoRoom.AfterIntoRoomEvent; + } } - if (this is ISendTalkEvent) + if (this is ISendTalkEvent sendTalk) { - ISendTalkEvent bind = (ISendTalkEvent)this; - BeforeSendTalk += bind.BeforeSendTalkEvent; - AfterSendTalk += bind.AfterSendTalkEvent; + if (isAdd) + { + BeforeSendTalk += sendTalk.BeforeSendTalkEvent; + AfterSendTalk += sendTalk.AfterSendTalkEvent; + } + else + { + BeforeSendTalk -= sendTalk.BeforeSendTalkEvent; + AfterSendTalk -= sendTalk.AfterSendTalkEvent; + } } - if (this is ICreateRoomEvent) + if (this is ICreateRoomEvent createRoom) { - ICreateRoomEvent bind = (ICreateRoomEvent)this; - BeforeCreateRoom += bind.BeforeCreateRoomEvent; - AfterCreateRoom += bind.AfterCreateRoomEvent; + if (isAdd) + { + BeforeCreateRoom += createRoom.BeforeCreateRoomEvent; + AfterCreateRoom += createRoom.AfterCreateRoomEvent; + } + else + { + BeforeCreateRoom -= createRoom.BeforeCreateRoomEvent; + AfterCreateRoom -= createRoom.AfterCreateRoomEvent; + } } - if (this is IQuitRoomEvent) + if (this is IQuitRoomEvent quitRoom) { - IQuitRoomEvent bind = (IQuitRoomEvent)this; - BeforeQuitRoom += bind.BeforeQuitRoomEvent; - AfterQuitRoom += bind.AfterQuitRoomEvent; + if (isAdd) + { + BeforeQuitRoom += quitRoom.BeforeQuitRoomEvent; + AfterQuitRoom += quitRoom.AfterQuitRoomEvent; + } + else + { + BeforeQuitRoom -= quitRoom.BeforeQuitRoomEvent; + AfterQuitRoom -= quitRoom.AfterQuitRoomEvent; + } } - if (this is IChangeRoomSettingEvent) + if (this is IChangeRoomSettingEvent changeRoomSetting) { - IChangeRoomSettingEvent bind = (IChangeRoomSettingEvent)this; - BeforeChangeRoomSetting += bind.BeforeChangeRoomSettingEvent; - AfterChangeRoomSetting += bind.AfterChangeRoomSettingEvent; + if (isAdd) + { + BeforeChangeRoomSetting += changeRoomSetting.BeforeChangeRoomSettingEvent; + AfterChangeRoomSetting += changeRoomSetting.AfterChangeRoomSettingEvent; + } + else + { + BeforeChangeRoomSetting -= changeRoomSetting.BeforeChangeRoomSettingEvent; + AfterChangeRoomSetting -= changeRoomSetting.AfterChangeRoomSettingEvent; + } } - if (this is IStartMatchEvent) + if (this is IStartMatchEvent startMatch) { - IStartMatchEvent bind = (IStartMatchEvent)this; - BeforeStartMatch += bind.BeforeStartMatchEvent; - AfterStartMatch += bind.AfterStartMatchEvent; + if (isAdd) + { + BeforeStartMatch += startMatch.BeforeStartMatchEvent; + AfterStartMatch += startMatch.AfterStartMatchEvent; + } + else + { + BeforeStartMatch -= startMatch.BeforeStartMatchEvent; + AfterStartMatch -= startMatch.AfterStartMatchEvent; + } } - if (this is IStartGameEvent) + if (this is IStartGameEvent startGame) { - IStartGameEvent bind = (IStartGameEvent)this; - BeforeStartGame += bind.BeforeStartGameEvent; - AfterStartGame += bind.AfterStartGameEvent; + if (isAdd) + { + BeforeStartGame += startGame.BeforeStartGameEvent; + AfterStartGame += startGame.AfterStartGameEvent; + } + else + { + BeforeStartGame -= startGame.BeforeStartGameEvent; + AfterStartGame -= startGame.AfterStartGameEvent; + } } - if (this is IChangeProfileEvent) + if (this is IChangeProfileEvent changeProfile) { - IChangeProfileEvent bind = (IChangeProfileEvent)this; - BeforeChangeProfile += bind.BeforeChangeProfileEvent; - AfterChangeProfile += bind.AfterChangeProfileEvent; + if (isAdd) + { + BeforeChangeProfile += changeProfile.BeforeChangeProfileEvent; + AfterChangeProfile += changeProfile.AfterChangeProfileEvent; + } + else + { + BeforeChangeProfile -= changeProfile.BeforeChangeProfileEvent; + AfterChangeProfile -= changeProfile.AfterChangeProfileEvent; + } } - if (this is IChangeAccountSettingEvent) + if (this is IChangeAccountSettingEvent changeAccountSetting) { - IChangeAccountSettingEvent bind = (IChangeAccountSettingEvent)this; - BeforeChangeAccountSetting += bind.BeforeChangeAccountSettingEvent; - AfterChangeAccountSetting += bind.AfterChangeAccountSettingEvent; + if (isAdd) + { + BeforeChangeAccountSetting += changeAccountSetting.BeforeChangeAccountSettingEvent; + AfterChangeAccountSetting += changeAccountSetting.AfterChangeAccountSettingEvent; + } + else + { + BeforeChangeAccountSetting -= changeAccountSetting.BeforeChangeAccountSettingEvent; + AfterChangeAccountSetting -= changeAccountSetting.AfterChangeAccountSettingEvent; + } } - if (this is IOpenInventoryEvent) + if (this is IOpenInventoryEvent openInventory) { - IOpenInventoryEvent bind = (IOpenInventoryEvent)this; - BeforeOpenInventory += bind.BeforeOpenInventoryEvent; - AfterOpenInventory += bind.AfterOpenInventoryEvent; + if (isAdd) + { + BeforeOpenInventory += openInventory.BeforeOpenInventoryEvent; + AfterOpenInventory += openInventory.AfterOpenInventoryEvent; + } + else + { + BeforeOpenInventory -= openInventory.BeforeOpenInventoryEvent; + AfterOpenInventory -= openInventory.AfterOpenInventoryEvent; + } } - if (this is ISignInEvent) + if (this is ISignInEvent signIn) { - ISignInEvent bind = (ISignInEvent)this; - BeforeSignIn += bind.BeforeSignInEvent; - AfterSignIn += bind.AfterSignInEvent; + if (isAdd) + { + BeforeSignIn += signIn.BeforeSignInEvent; + AfterSignIn += signIn.AfterSignInEvent; + } + else + { + BeforeSignIn -= signIn.BeforeSignInEvent; + AfterSignIn -= signIn.AfterSignInEvent; + } } - if (this is IOpenStoreEvent) + if (this is IOpenStoreEvent openStore) { - IOpenStoreEvent bind = (IOpenStoreEvent)this; - BeforeOpenStore += bind.BeforeOpenStoreEvent; - AfterOpenStore += bind.AfterOpenStoreEvent; + if (isAdd) + { + BeforeOpenStore += openStore.BeforeOpenStoreEvent; + AfterOpenStore += openStore.AfterOpenStoreEvent; + } + else + { + BeforeOpenStore -= openStore.BeforeOpenStoreEvent; + AfterOpenStore -= openStore.AfterOpenStoreEvent; + } } - if (this is IBuyItemEvent) + if (this is IBuyItemEvent buyItem) { - IBuyItemEvent bind = (IBuyItemEvent)this; - BeforeBuyItem += bind.BeforeBuyItemEvent; - AfterBuyItem += bind.AfterBuyItemEvent; + if (isAdd) + { + BeforeBuyItem += buyItem.BeforeBuyItemEvent; + AfterBuyItem += buyItem.AfterBuyItemEvent; + } + else + { + BeforeBuyItem -= buyItem.BeforeBuyItemEvent; + AfterBuyItem -= buyItem.AfterBuyItemEvent; + } } - if (this is IShowRankingEvent) + if (this is IShowRankingEvent showRanking) { - IShowRankingEvent bind = (IShowRankingEvent)this; - BeforeShowRanking += bind.BeforeShowRankingEvent; - AfterShowRanking += bind.AfterShowRankingEvent; + if (isAdd) + { + BeforeShowRanking += showRanking.BeforeShowRankingEvent; + AfterShowRanking += showRanking.AfterShowRankingEvent; + } + else + { + BeforeShowRanking -= showRanking.BeforeShowRankingEvent; + AfterShowRanking -= showRanking.AfterShowRankingEvent; + } } - if (this is IUseItemEvent) + if (this is IUseItemEvent useItem) { - IUseItemEvent bind = (IUseItemEvent)this; - BeforeUseItem += bind.BeforeUseItemEvent; - AfterUseItem += bind.AfterUseItemEvent; + if (isAdd) + { + BeforeUseItem += useItem.BeforeUseItemEvent; + AfterUseItem += useItem.AfterUseItemEvent; + } + else + { + BeforeUseItem -= useItem.BeforeUseItemEvent; + AfterUseItem -= useItem.AfterUseItemEvent; + } } - if (this is IEndGameEvent) + if (this is IEndGameEvent endGame) { - IEndGameEvent bind = (IEndGameEvent)this; - BeforeEndGame += bind.BeforeEndGameEvent; - AfterEndGame += bind.AfterEndGameEvent; + if (isAdd) + { + BeforeEndGame += endGame.BeforeEndGameEvent; + AfterEndGame += endGame.AfterEndGameEvent; + } + else + { + BeforeEndGame -= endGame.BeforeEndGameEvent; + AfterEndGame -= endGame.AfterEndGameEvent; + } } } diff --git a/Library/Common/Addon/SkillModule.cs b/Library/Common/Addon/SkillModule.cs index 416bd2c..2de5d9d 100644 --- a/Library/Common/Addon/SkillModule.cs +++ b/Library/Common/Addon/SkillModule.cs @@ -59,6 +59,16 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon return _isLoaded; } + /// + /// 卸载模组 + /// + /// + public void UnLoad(params object[] objs) + { + Factory.OpenFactory.UnRegisterFactory(SkillFactory()); + Factory.OpenFactory.UnRegisterFactory(EffectFactory()); + } + /// /// 注册工厂 /// diff --git a/Library/Common/Addon/WebAPIPlugin.cs b/Library/Common/Addon/WebAPIPlugin.cs index 5da0bff..95fdb98 100644 --- a/Library/Common/Addon/WebAPIPlugin.cs +++ b/Library/Common/Addon/WebAPIPlugin.cs @@ -6,7 +6,7 @@ using Milimoe.FunGame.Core.Library.Common.Event; namespace Milimoe.FunGame.Core.Library.Common.Addon { - public abstract class WebAPIPlugin : IAddon, IAddonController + public abstract class WebAPIPlugin : IPlugin { /// /// 插件名称 @@ -28,10 +28,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon /// public abstract string Author { get; } + /// + /// 记录该插件的加载器 + /// + public WebAPIPluginLoader? PluginLoader { get; set; } = null; + /// /// 包含了一些常用方法的控制器 /// - public ServerAddonController Controller + public ServerAddonController Controller { get => _controller ?? throw new NotImplementedException(); internal set => _controller = value; @@ -40,16 +45,16 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon /// /// base控制器 /// - BaseAddonController IAddonController.Controller + BaseAddonController IAddonController.Controller { get => Controller; - set => _controller = (ServerAddonController?)value; + set => _controller = (ServerAddonController?)value; } /// /// 控制器内部变量 /// - private ServerAddonController? _controller; + private ServerAddonController? _controller; /// /// 加载标记 @@ -76,6 +81,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon return _isLoaded; } + /// + /// 卸载模组 + /// + /// + public void UnLoad(params object[] objs) + { + BindEvent(false); + } + /// /// 接收服务器控制台的输入 /// @@ -111,153 +125,300 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon /// /// 绑定事件。在后触发 /// - private void BindEvent() + private void BindEvent(bool isAdd = true) { - if (this is IConnectEvent) + if (this is IConnectEvent connect) { - IConnectEvent bind = (IConnectEvent)this; - BeforeConnect += bind.BeforeConnectEvent; - AfterConnect += bind.AfterConnectEvent; + if (isAdd) + { + BeforeConnect += connect.BeforeConnectEvent; + AfterConnect += connect.AfterConnectEvent; + } + else + { + BeforeConnect -= connect.BeforeConnectEvent; + AfterConnect -= connect.AfterConnectEvent; + } } - if (this is IDisconnectEvent) + if (this is IDisconnectEvent disconnect) { - IDisconnectEvent bind = (IDisconnectEvent)this; - BeforeDisconnect += bind.BeforeDisconnectEvent; - AfterDisconnect += bind.AfterDisconnectEvent; + if (isAdd) + { + BeforeDisconnect += disconnect.BeforeDisconnectEvent; + AfterDisconnect += disconnect.AfterDisconnectEvent; + } + else + { + BeforeDisconnect -= disconnect.BeforeDisconnectEvent; + AfterDisconnect -= disconnect.AfterDisconnectEvent; + } } - if (this is ILoginEvent) + if (this is ILoginEvent login) { - ILoginEvent bind = (ILoginEvent)this; - BeforeLogin += bind.BeforeLoginEvent; - AfterLogin += bind.AfterLoginEvent; + if (isAdd) + { + BeforeLogin += login.BeforeLoginEvent; + AfterLogin += login.AfterLoginEvent; + } + else + { + BeforeLogin -= login.BeforeLoginEvent; + AfterLogin -= login.AfterLoginEvent; + } } - if (this is ILogoutEvent) + if (this is ILogoutEvent logout) { - ILogoutEvent bind = (ILogoutEvent)this; - BeforeLogout += bind.BeforeLogoutEvent; - AfterLogout += bind.AfterLogoutEvent; + if (isAdd) + { + BeforeLogout += logout.BeforeLogoutEvent; + AfterLogout += logout.AfterLogoutEvent; + } + else + { + BeforeLogout -= logout.BeforeLogoutEvent; + AfterLogout -= logout.AfterLogoutEvent; + } } - if (this is IRegEvent) + if (this is IRegEvent reg) { - IRegEvent bind = (IRegEvent)this; - BeforeReg += bind.BeforeRegEvent; - AfterReg += bind.AfterRegEvent; + if (isAdd) + { + BeforeReg += reg.BeforeRegEvent; + AfterReg += reg.AfterRegEvent; + } + else + { + BeforeReg -= reg.BeforeRegEvent; + AfterReg -= reg.AfterRegEvent; + } } - if (this is IIntoRoomEvent) + if (this is IIntoRoomEvent intoRoom) { - IIntoRoomEvent bind = (IIntoRoomEvent)this; - BeforeIntoRoom += bind.BeforeIntoRoomEvent; - AfterIntoRoom += bind.AfterIntoRoomEvent; + if (isAdd) + { + BeforeIntoRoom += intoRoom.BeforeIntoRoomEvent; + AfterIntoRoom += intoRoom.AfterIntoRoomEvent; + } + else + { + BeforeIntoRoom -= intoRoom.BeforeIntoRoomEvent; + AfterIntoRoom -= intoRoom.AfterIntoRoomEvent; + } } - if (this is ISendTalkEvent) + if (this is ISendTalkEvent sendTalk) { - ISendTalkEvent bind = (ISendTalkEvent)this; - BeforeSendTalk += bind.BeforeSendTalkEvent; - AfterSendTalk += bind.AfterSendTalkEvent; + if (isAdd) + { + BeforeSendTalk += sendTalk.BeforeSendTalkEvent; + AfterSendTalk += sendTalk.AfterSendTalkEvent; + } + else + { + BeforeSendTalk -= sendTalk.BeforeSendTalkEvent; + AfterSendTalk -= sendTalk.AfterSendTalkEvent; + } } - if (this is ICreateRoomEvent) + if (this is ICreateRoomEvent createRoom) { - ICreateRoomEvent bind = (ICreateRoomEvent)this; - BeforeCreateRoom += bind.BeforeCreateRoomEvent; - AfterCreateRoom += bind.AfterCreateRoomEvent; + if (isAdd) + { + BeforeCreateRoom += createRoom.BeforeCreateRoomEvent; + AfterCreateRoom += createRoom.AfterCreateRoomEvent; + } + else + { + BeforeCreateRoom -= createRoom.BeforeCreateRoomEvent; + AfterCreateRoom -= createRoom.AfterCreateRoomEvent; + } } - if (this is IQuitRoomEvent) + if (this is IQuitRoomEvent quitRoom) { - IQuitRoomEvent bind = (IQuitRoomEvent)this; - BeforeQuitRoom += bind.BeforeQuitRoomEvent; - AfterQuitRoom += bind.AfterQuitRoomEvent; + if (isAdd) + { + BeforeQuitRoom += quitRoom.BeforeQuitRoomEvent; + AfterQuitRoom += quitRoom.AfterQuitRoomEvent; + } + else + { + BeforeQuitRoom -= quitRoom.BeforeQuitRoomEvent; + AfterQuitRoom -= quitRoom.AfterQuitRoomEvent; + } } - if (this is IChangeRoomSettingEvent) + if (this is IChangeRoomSettingEvent changeRoomSetting) { - IChangeRoomSettingEvent bind = (IChangeRoomSettingEvent)this; - BeforeChangeRoomSetting += bind.BeforeChangeRoomSettingEvent; - AfterChangeRoomSetting += bind.AfterChangeRoomSettingEvent; + if (isAdd) + { + BeforeChangeRoomSetting += changeRoomSetting.BeforeChangeRoomSettingEvent; + AfterChangeRoomSetting += changeRoomSetting.AfterChangeRoomSettingEvent; + } + else + { + BeforeChangeRoomSetting -= changeRoomSetting.BeforeChangeRoomSettingEvent; + AfterChangeRoomSetting -= changeRoomSetting.AfterChangeRoomSettingEvent; + } } - if (this is IStartMatchEvent) + if (this is IStartMatchEvent startMatch) { - IStartMatchEvent bind = (IStartMatchEvent)this; - BeforeStartMatch += bind.BeforeStartMatchEvent; - AfterStartMatch += bind.AfterStartMatchEvent; + if (isAdd) + { + BeforeStartMatch += startMatch.BeforeStartMatchEvent; + AfterStartMatch += startMatch.AfterStartMatchEvent; + } + else + { + BeforeStartMatch -= startMatch.BeforeStartMatchEvent; + AfterStartMatch -= startMatch.AfterStartMatchEvent; + } } - if (this is IStartGameEvent) + if (this is IStartGameEvent startGame) { - IStartGameEvent bind = (IStartGameEvent)this; - BeforeStartGame += bind.BeforeStartGameEvent; - AfterStartGame += bind.AfterStartGameEvent; + if (isAdd) + { + BeforeStartGame += startGame.BeforeStartGameEvent; + AfterStartGame += startGame.AfterStartGameEvent; + } + else + { + BeforeStartGame -= startGame.BeforeStartGameEvent; + AfterStartGame -= startGame.AfterStartGameEvent; + } } - if (this is IChangeProfileEvent) + if (this is IChangeProfileEvent changeProfile) { - IChangeProfileEvent bind = (IChangeProfileEvent)this; - BeforeChangeProfile += bind.BeforeChangeProfileEvent; - AfterChangeProfile += bind.AfterChangeProfileEvent; + if (isAdd) + { + BeforeChangeProfile += changeProfile.BeforeChangeProfileEvent; + AfterChangeProfile += changeProfile.AfterChangeProfileEvent; + } + else + { + BeforeChangeProfile -= changeProfile.BeforeChangeProfileEvent; + AfterChangeProfile -= changeProfile.AfterChangeProfileEvent; + } } - if (this is IChangeAccountSettingEvent) + if (this is IChangeAccountSettingEvent changeAccountSetting) { - IChangeAccountSettingEvent bind = (IChangeAccountSettingEvent)this; - BeforeChangeAccountSetting += bind.BeforeChangeAccountSettingEvent; - AfterChangeAccountSetting += bind.AfterChangeAccountSettingEvent; + if (isAdd) + { + BeforeChangeAccountSetting += changeAccountSetting.BeforeChangeAccountSettingEvent; + AfterChangeAccountSetting += changeAccountSetting.AfterChangeAccountSettingEvent; + } + else + { + BeforeChangeAccountSetting -= changeAccountSetting.BeforeChangeAccountSettingEvent; + AfterChangeAccountSetting -= changeAccountSetting.AfterChangeAccountSettingEvent; + } } - if (this is IOpenInventoryEvent) + if (this is IOpenInventoryEvent openInventory) { - IOpenInventoryEvent bind = (IOpenInventoryEvent)this; - BeforeOpenInventory += bind.BeforeOpenInventoryEvent; - AfterOpenInventory += bind.AfterOpenInventoryEvent; + if (isAdd) + { + BeforeOpenInventory += openInventory.BeforeOpenInventoryEvent; + AfterOpenInventory += openInventory.AfterOpenInventoryEvent; + } + else + { + BeforeOpenInventory -= openInventory.BeforeOpenInventoryEvent; + AfterOpenInventory -= openInventory.AfterOpenInventoryEvent; + } } - if (this is ISignInEvent) + if (this is ISignInEvent signIn) { - ISignInEvent bind = (ISignInEvent)this; - BeforeSignIn += bind.BeforeSignInEvent; - AfterSignIn += bind.AfterSignInEvent; + if (isAdd) + { + BeforeSignIn += signIn.BeforeSignInEvent; + AfterSignIn += signIn.AfterSignInEvent; + } + else + { + BeforeSignIn -= signIn.BeforeSignInEvent; + AfterSignIn -= signIn.AfterSignInEvent; + } } - if (this is IOpenStoreEvent) + if (this is IOpenStoreEvent openStore) { - IOpenStoreEvent bind = (IOpenStoreEvent)this; - BeforeOpenStore += bind.BeforeOpenStoreEvent; - AfterOpenStore += bind.AfterOpenStoreEvent; + if (isAdd) + { + BeforeOpenStore += openStore.BeforeOpenStoreEvent; + AfterOpenStore += openStore.AfterOpenStoreEvent; + } + else + { + BeforeOpenStore -= openStore.BeforeOpenStoreEvent; + AfterOpenStore -= openStore.AfterOpenStoreEvent; + } } - if (this is IBuyItemEvent) + if (this is IBuyItemEvent buyItem) { - IBuyItemEvent bind = (IBuyItemEvent)this; - BeforeBuyItem += bind.BeforeBuyItemEvent; - AfterBuyItem += bind.AfterBuyItemEvent; + if (isAdd) + { + BeforeBuyItem += buyItem.BeforeBuyItemEvent; + AfterBuyItem += buyItem.AfterBuyItemEvent; + } + else + { + BeforeBuyItem -= buyItem.BeforeBuyItemEvent; + AfterBuyItem -= buyItem.AfterBuyItemEvent; + } } - if (this is IShowRankingEvent) + if (this is IShowRankingEvent showRanking) { - IShowRankingEvent bind = (IShowRankingEvent)this; - BeforeShowRanking += bind.BeforeShowRankingEvent; - AfterShowRanking += bind.AfterShowRankingEvent; + if (isAdd) + { + BeforeShowRanking += showRanking.BeforeShowRankingEvent; + AfterShowRanking += showRanking.AfterShowRankingEvent; + } + else + { + BeforeShowRanking -= showRanking.BeforeShowRankingEvent; + AfterShowRanking -= showRanking.AfterShowRankingEvent; + } } - if (this is IUseItemEvent) + if (this is IUseItemEvent useItem) { - IUseItemEvent bind = (IUseItemEvent)this; - BeforeUseItem += bind.BeforeUseItemEvent; - AfterUseItem += bind.AfterUseItemEvent; + if (isAdd) + { + BeforeUseItem += useItem.BeforeUseItemEvent; + AfterUseItem += useItem.AfterUseItemEvent; + } + else + { + BeforeUseItem -= useItem.BeforeUseItemEvent; + AfterUseItem -= useItem.AfterUseItemEvent; + } } - if (this is IEndGameEvent) + if (this is IEndGameEvent endGame) { - IEndGameEvent bind = (IEndGameEvent)this; - BeforeEndGame += bind.BeforeEndGameEvent; - AfterEndGame += bind.AfterEndGameEvent; + if (isAdd) + { + BeforeEndGame += endGame.BeforeEndGameEvent; + AfterEndGame += endGame.AfterEndGameEvent; + } + else + { + BeforeEndGame -= endGame.BeforeEndGameEvent; + AfterEndGame -= endGame.AfterEndGameEvent; + } } } diff --git a/Library/Common/Architecture/SyncAwaiter.cs b/Library/Common/Architecture/SyncAwaiter.cs new file mode 100644 index 0000000..7c1eb29 --- /dev/null +++ b/Library/Common/Architecture/SyncAwaiter.cs @@ -0,0 +1,45 @@ +namespace Milimoe.FunGame.Core.Library.Common.Architecture +{ + /// + /// 该类的工具方法允许在同步方法中安全等待异步任务完成 + /// + public class SyncAwaiter + { + /// + /// 在同步方法中安全等待一个 Task 完成并获取结果 + /// 内部使用 ManualResetEventSlim,避免死锁 + /// + public static T WaitResult(Task task) + { + if (task.IsCompleted) + return task.Result; + + ManualResetEventSlim mres = new(false); + + // 当 task 完成时,设置事件信号 + task.ContinueWith(_ => + { + mres.Set(); + }, TaskScheduler.Default); + + // 阻塞当前线程直到 task 完成 + // 注意:这会阻塞调用线程! + mres.Wait(); + + // 现在可以安全取 Result(不会抛死锁) + return task.Result; + } + + /// + /// 无返回值版本 + /// + public static void Wait(Task task) + { + if (task.IsCompleted) return; + + ManualResetEventSlim mres = new(false); + task.ContinueWith(_ => mres.Set(), TaskScheduler.Default); + mres.Wait(); + } + } +} diff --git a/Library/Exception/Exception.cs b/Library/Exception/Exception.cs index d2e9518..714ae13 100644 --- a/Library/Exception/Exception.cs +++ b/Library/Exception/Exception.cs @@ -184,4 +184,19 @@ { public override string Message => "试图构造一个不支持的类的实例 (#10037)"; } + + public class AddonInvalidException(string fullPath = "") : Exception + { + public override string Message => $"加载项 {(fullPath != "" ? $"'{fullPath}'" : "")} 无效 (#10038)"; + } + + public class AddonLoadException(string fullPath = "", Exception? inner = null) : Exception("", inner) + { + public override string Message => $"加载项 {(fullPath != "" ? $"'{fullPath}'" : "")} 加载失败 (#10039)"; + } + + public class AddonUnloadException(string fullPath = "", Exception? inner = null) : Exception("", inner) + { + public override string Message => $"卸载加载项 {(fullPath != "" ? $"'{fullPath}' 的" : "")} 旧上下文失败 (#10040)"; + } } diff --git a/Model/DamageCalculationOptions.cs b/Model/DamageCalculationOptions.cs index d0aa5ce..9e38a6a 100644 --- a/Model/DamageCalculationOptions.cs +++ b/Model/DamageCalculationOptions.cs @@ -11,7 +11,7 @@ namespace Milimoe.FunGame.Core.Model /// 伤害来源 /// public Character Character { get; set; } = character; - + /// /// 完整计算伤害 /// diff --git a/Model/GamingQueue.cs b/Model/GamingQueue.cs index ec49496..a53ee69 100644 --- a/Model/GamingQueue.cs +++ b/Model/GamingQueue.cs @@ -2809,7 +2809,7 @@ namespace Milimoe.FunGame.Core.Model } if (healBonus != 0) { - totalHealBonus[effect]= healBonus; + totalHealBonus[effect] = healBonus; healStrings.Add($"{(healBonus > 0 ? " + " : " - ")}{Math.Abs(healBonus):0.##}({effect.Name})"); } } @@ -2842,7 +2842,7 @@ namespace Milimoe.FunGame.Core.Model } } - if (heal <= 0) + if (heal <= 0 || heal.ToString("0.##") == "0") { return; } diff --git a/Model/PrefabricatedEntity/CourageCommandSkill.cs b/Model/PrefabricatedEntity/CourageCommandSkill.cs index f934f78..c138fa0 100644 --- a/Model/PrefabricatedEntity/CourageCommandSkill.cs +++ b/Model/PrefabricatedEntity/CourageCommandSkill.cs @@ -7,6 +7,6 @@ namespace Milimoe.FunGame.Core.Model.PrefabricatedEntity /// public class CourageCommandSkill(long id, string name, Dictionary args, Character? character = null) : OpenSkill(id, name, args, character) { - + } } diff --git a/README.md b/README.md index 961e702..d09ba52 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ ## 项目简介 -`FunGame` 是一套基于 `C#.NET` 设计的回合制游戏服务器端开发框架,旨在简化多人回合制在线游戏的开发流程。 +`FunGame` 是一套基于 `C#.NET` 设计的回合制战斗系统解决方案,旨在打造充满策略趣味的战棋回合制游戏。 -配套解决方案:[FunGameServer](https://github.com/project-redbud/FunGame-Server)(基于 `ASP.NET Core Web API` 的跨平台高性能服务器控制台) +配套服务器解决方案:[FunGameServer](https://github.com/project-redbud/FunGame-Server)(基于 `ASP.NET Core Web API` 的跨平台高性能服务器控制台) 本仓库 `FunGame.Core` 项目是 `FunGame` 框架的核心模块,包含了框架的基础组件。 本项目不局限于服务器端开发,在支持 `.NET` 编程的客户端项目中也能使用。 diff --git a/Service/AddonManager.cs b/Service/AddonManager.cs index 85bda5f..2295efb 100644 --- a/Service/AddonManager.cs +++ b/Service/AddonManager.cs @@ -28,7 +28,7 @@ namespace Milimoe.FunGame.Core.Service { if (!Directory.Exists(ReflectionSet.PluginFolderPath)) return plugins; - string[] dlls = Directory.GetFiles(ReflectionSet.PluginFolderPath, "*.dll"); + string[] dlls = Directory.GetFiles(ReflectionSet.PluginFolderPath, "*.dll", SearchOption.AllDirectories); foreach (string dll in dlls) { @@ -81,7 +81,7 @@ namespace Milimoe.FunGame.Core.Service { if (!Directory.Exists(ReflectionSet.PluginFolderPath)) return plugins; - string[] dlls = Directory.GetFiles(ReflectionSet.PluginFolderPath, "*.dll"); + string[] dlls = Directory.GetFiles(ReflectionSet.PluginFolderPath, "*.dll", SearchOption.AllDirectories); foreach (string dll in dlls) { @@ -134,7 +134,7 @@ namespace Milimoe.FunGame.Core.Service { if (!Directory.Exists(ReflectionSet.PluginFolderPath)) return plugins; - string[] dlls = Directory.GetFiles(ReflectionSet.PluginFolderPath, "*.dll"); + string[] dlls = Directory.GetFiles(ReflectionSet.PluginFolderPath, "*.dll", SearchOption.AllDirectories); foreach (string dll in dlls) { @@ -190,7 +190,7 @@ namespace Milimoe.FunGame.Core.Service { if (!Directory.Exists(ReflectionSet.GameModuleFolderPath)) return modules; - string[] dlls = Directory.GetFiles(ReflectionSet.GameModuleFolderPath, "*.dll"); + string[] dlls = Directory.GetFiles(ReflectionSet.GameModuleFolderPath, "*.dll", SearchOption.AllDirectories); foreach (string dll in dlls) { @@ -264,7 +264,7 @@ namespace Milimoe.FunGame.Core.Service { if (!Directory.Exists(ReflectionSet.GameModuleFolderPath)) return servers; - string[] dlls = Directory.GetFiles(ReflectionSet.GameModuleFolderPath, "*.dll"); + string[] dlls = Directory.GetFiles(ReflectionSet.GameModuleFolderPath, "*.dll", SearchOption.AllDirectories); foreach (string dll in dlls) { @@ -334,7 +334,7 @@ namespace Milimoe.FunGame.Core.Service { if (!Directory.Exists(ReflectionSet.GameMapFolderPath)) return maps; - string[] dlls = Directory.GetFiles(ReflectionSet.GameMapFolderPath, "*.dll"); + string[] dlls = Directory.GetFiles(ReflectionSet.GameMapFolderPath, "*.dll", SearchOption.AllDirectories); foreach (string dll in dlls) { diff --git a/Service/HotLoadAddonManager.cs b/Service/HotLoadAddonManager.cs new file mode 100644 index 0000000..e77b601 --- /dev/null +++ b/Service/HotLoadAddonManager.cs @@ -0,0 +1,566 @@ +using System.Collections.Concurrent; +using System.Reflection; +using System.Runtime.Loader; +using Milimoe.FunGame.Core.Api.Utility; +using Milimoe.FunGame.Core.Interface.Addons; +using Milimoe.FunGame.Core.Interface.Base.Addons; +using Milimoe.FunGame.Core.Library.Common.Addon; +using Milimoe.FunGame.Core.Library.Constant; +using Milimoe.FunGame.Core.Library.Exception; + +namespace Milimoe.FunGame.Core.Service +{ + /// + /// 支持热更新、可卸载、可重载插件/模组 + /// 支持插件跨 DLL 引用模组,通过全局访问最新实例 + /// + internal static class HotLoadAddonManager + { + /// + /// 已加载的插件DLL名称对应的路径 + /// + internal static Dictionary PluginFilePaths { get; } = []; + + /// + /// 已加载的模组DLL名称对应的路径 + /// + internal static Dictionary ModuleFilePaths { get; } = []; + + /// + /// 已加载的插件 + /// + internal static Dictionary Plugins { get; } = []; + + /// + /// 已加载的模组 + /// + internal static Dictionary Modules { get; } = []; + + /// + /// key = 文件路径(全小写),value = 当前加载上下文 + 程序集 + 根实例 + /// + private static readonly ConcurrentDictionary _loadedDLLs = new(StringComparer.OrdinalIgnoreCase); + + /// + /// 即将清除的上下文 + /// + private static readonly List> _contextsToClean = []; + + /// + /// 尝试加载或重新加载某个 DLL,返回是否成功加载/更新 + /// + /// DLL 完整路径 + /// 新加载的实例列表 + /// 是否为插件 + /// 是否为模组 + /// 是否成功(无变化不成功) + private static bool TryLoadOrReload(string fullPath, out List newInstances, bool isPlugin, bool isModule) + { + newInstances = []; + + if (!File.Exists(fullPath)) return false; + + string key = fullPath.ToLowerInvariant(); + DateTime currentWriteTime = File.GetLastWriteTimeUtc(fullPath); + + // 文件无变化 → 返回现有实例 + if (_loadedDLLs.TryGetValue(key, out DLLAddonEntry? entry) && entry.LastWriteTimeUtc == currentWriteTime) + { + foreach (AddonSubEntry sub in entry.Addons) + { + if (sub.Weak.TryGetTarget(out IAddon? target) && target != null) + newInstances.Add(target); + } + return false; + } + + // 需要卸载旧 DLL + if (_loadedDLLs.TryRemove(key, out DLLAddonEntry? oldEntry)) + { + try + { + foreach (AddonSubEntry sub in oldEntry.Addons) + { + if (sub.Instance is IHotReloadAware aware) + { + aware.OnBeforeUnload(); + } + sub.Instance.UnLoad(); + Plugins.Remove(sub.Name); + Modules.Remove(sub.Name); + } + oldEntry.Context.Unload(); + GC.Collect(2, GCCollectionMode.Forced, true); + } + catch (Exception e) + { + throw new AddonUnloadException(fullPath, e); + } + finally + { + lock (_contextsToClean) + { + _contextsToClean.Add(new WeakReference(oldEntry.Context)); + } + } + } + + // 加载新 DLL + try + { + string dllName = Path.GetFileNameWithoutExtension(fullPath); + AddonLoadContext ctx = new(dllName, fullPath); + + using FileStream fs = new(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read); + Assembly assembly = ctx.LoadFromStream(fs); + string filename = assembly.GetName().Name?.Trim() ?? ""; + + Type[] addonTypes = [.. assembly.GetTypes().Where(t => !t.IsAbstract && typeof(IAddon).IsAssignableFrom(t) && (typeof(Plugin).IsAssignableFrom(t) || typeof(ServerPlugin).IsAssignableFrom(t) || + typeof(WebAPIPlugin).IsAssignableFrom(t) || typeof(GameModule).IsAssignableFrom(t) || typeof(GameModuleServer).IsAssignableFrom(t) || typeof(CharacterModule).IsAssignableFrom(t) || + typeof(SkillModule).IsAssignableFrom(t) || typeof(ItemModule).IsAssignableFrom(t) || typeof(GameMap).IsAssignableFrom(t)))]; + + DLLAddonEntry newEntry = new() + { + Context = ctx, + Assembly = assembly, + LastWriteTimeUtc = currentWriteTime + }; + + foreach (Type addonType in addonTypes) + { + try + { + if (Activator.CreateInstance(addonType) is not IAddon instance) continue; + + // 为了安全起见,未实现此接口的不允许使用热更新模式加载 + if (instance is not IHotReloadAware aware) continue; + + string addonName = instance.Name?.Trim() ?? addonType.Name; + AddonSubEntry sub = new() + { + Instance = instance, + Weak = new WeakReference(instance), + Name = addonName + }; + + newEntry.Addons.Add(sub); + newInstances.Add(instance); + + if (isPlugin) Plugins[addonName] = instance; + if (isModule) Modules[addonName] = instance; + } + catch (Exception e) + { + TXTHelper.AppendErrorLog(e.GetErrorInfo()); + } + } + + if (newInstances.Count > 0) + { + _loadedDLLs[key] = newEntry; + if (isPlugin) PluginFilePaths[filename] = key; + if (isModule) ModuleFilePaths[filename] = key; + } + return true; + } + catch (Exception e) + { + throw new AddonLoadException(fullPath, e); + } + } + + /// + /// 从 plugins 目录加载所有插件 + /// + /// 插件字典 + /// 委托字典 + /// 其他参数 + /// 被更新的插件列表 + internal static List LoadPlugins(Dictionary plugins, Dictionary delegates, params object[] otherobjs) + { + List updated = []; + if (!Directory.Exists(ReflectionSet.PluginFolderPath)) return updated; + + string[] dlls = Directory.GetFiles(ReflectionSet.PluginFolderPath, "*.dll", SearchOption.AllDirectories); + + foreach (string dll in dlls) + { + bool loaded = TryLoadOrReload(dll, out List instances, true, false); + foreach (IAddon instance in instances) + { + string name = instance.Name.Trim(); + if (instance is Plugin pluginInstance) + { + // 热更新无变化的文件时,不会再触发 Load 方法 + if ((loaded || !plugins.ContainsKey(name)) && pluginInstance.Load(otherobjs)) + { + pluginInstance.Controller = new(pluginInstance, delegates); + updated.Add(pluginInstance); + } + plugins[name] = pluginInstance; + } + } + } + + return updated; + } + + /// + /// 从 plugins 目录加载所有 Server 插件 + /// + /// 插件字典 + /// 委托字典 + /// 其他参数 + /// 被更新的插件列表 + internal static List LoadServerPlugins(Dictionary plugins, Dictionary delegates, params object[] otherobjs) + { + List updated = []; + if (!Directory.Exists(ReflectionSet.PluginFolderPath)) return updated; + + string[] dlls = Directory.GetFiles(ReflectionSet.PluginFolderPath, "*.dll", SearchOption.AllDirectories); + + foreach (string dll in dlls) + { + bool loaded = TryLoadOrReload(dll, out List instances, true, false); + foreach (IAddon instance in instances) + { + string name = instance.Name.Trim(); + if (instance is ServerPlugin pluginInstance) + { + // 热更新无变化的文件时,不会再触发 Load 方法 + if ((loaded || !plugins.ContainsKey(name)) && pluginInstance.Load(otherobjs)) + { + pluginInstance.Controller = new(pluginInstance, delegates); + updated.Add(pluginInstance); + } + plugins[name] = pluginInstance; + } + } + } + + return updated; + } + + /// + /// 从 plugins 目录加载所有 WebAPI 插件 + /// + /// 插件字典 + /// 委托字典 + /// 其他参数 + /// 被更新的插件列表 + internal static List LoadWebAPIPlugins(Dictionary plugins, Dictionary delegates, params object[] otherobjs) + { + List updated = []; + if (!Directory.Exists(ReflectionSet.PluginFolderPath)) return updated; + + string[] dlls = Directory.GetFiles(ReflectionSet.PluginFolderPath, "*.dll", SearchOption.AllDirectories); + + foreach (string dll in dlls) + { + bool loaded = TryLoadOrReload(dll, out List instances, true, false); + foreach (IAddon instance in instances) + { + string name = instance.Name.Trim(); + if (instance is WebAPIPlugin pluginInstance) + { + // 热更新无变化的文件时,不会再触发 Load 方法 + if ((loaded || !plugins.ContainsKey(name)) && pluginInstance.Load(otherobjs)) + { + pluginInstance.Controller = new(pluginInstance, delegates); + updated.Add(pluginInstance); + } + plugins[name] = pluginInstance; + } + } + } + + return updated; + } + + /// + /// 从 modules 目录加载所有模组 + /// + /// 模组字典 + /// 角色字典 + /// 技能字典 + /// 物品字典 + /// 委托字典 + /// 其他参数 + /// 被更新的模组列表 + internal static List LoadGameModules(Dictionary modules, Dictionary characters, Dictionary skills, Dictionary items, Dictionary delegates, params object[] otherobjs) + { + List updated = []; + if (!Directory.Exists(ReflectionSet.GameModuleFolderPath)) return updated; + + string[] dlls = Directory.GetFiles(ReflectionSet.GameModuleFolderPath, "*.dll", SearchOption.AllDirectories); + + foreach (string dll in dlls) + { + bool loaded = TryLoadOrReload(dll, out List instances, false, true); + foreach (IAddon instance in instances) + { + string name = instance.Name.Trim(); + if (instance is GameModule moduleInstance) + { + // 热更新无变化的文件时,不会再触发 Load 方法 + if ((loaded || !modules.ContainsKey(name)) && instance.Load(otherobjs)) + { + moduleInstance.Controller = new(moduleInstance, delegates); + } + modules[name] = moduleInstance; + } + else if (instance is CharacterModule charInstance) + { + if (loaded || !characters.ContainsKey(name)) + { + instance.Load(otherobjs); + } + characters[name] = charInstance; + } + else if (instance is SkillModule skillInstance) + { + if (loaded || !skills.ContainsKey(name)) + { + instance.Load(otherobjs); + } + skills[name] = skillInstance; + } + else if (instance is ItemModule itemInstance) + { + if (loaded || !items.ContainsKey(name)) + { + instance.Load(otherobjs); + } + items[name] = itemInstance; + } + } + } + + return updated; + } + + /// + /// 从 modules 目录加载所有适用于服务器的模组 + /// + /// 服务器模组字典 + /// 角色字典 + /// 技能字典 + /// 物品字典 + /// 委托字典 + /// 其他参数 + /// 被更新的服务器模组列表 + internal static List LoadGameModulesForServer(Dictionary servers, Dictionary characters, Dictionary skills, Dictionary items, Dictionary delegates, params object[] otherobjs) + { + List updated = []; + if (!Directory.Exists(ReflectionSet.GameModuleFolderPath)) return updated; + + string[] dlls = Directory.GetFiles(ReflectionSet.GameModuleFolderPath, "*.dll", SearchOption.AllDirectories); + + foreach (string dll in dlls) + { + bool loaded = TryLoadOrReload(dll, out List instances, false, true); + foreach (IAddon instance in instances) + { + string name = instance.Name.Trim(); + if (instance is GameModuleServer serversInstance) + { + // 热更新无变化的文件时,不会再触发 Load 方法 + if ((loaded || !servers.ContainsKey(name)) && instance.Load(otherobjs)) + { + serversInstance.Controller = new(serversInstance, delegates); + } + servers[name] = serversInstance; + } + else if (instance is CharacterModule charInstance) + { + if (loaded || !characters.ContainsKey(name)) + { + instance.Load(otherobjs); + } + characters[name] = charInstance; + } + else if (instance is SkillModule skillInstance) + { + if (loaded || !skills.ContainsKey(name)) + { + instance.Load(otherobjs); + } + skills[name] = skillInstance; + } + else if (instance is ItemModule itemInstance) + { + if (loaded || !items.ContainsKey(name)) + { + instance.Load(otherobjs); + } + items[name] = itemInstance; + } + } + } + + return updated; + } + + /// + /// 从 maps 目录加载所有地图 + /// + /// 地图字典 + /// 其他参数 + /// 被更新的地图列表 + internal static List LoadGameMaps(Dictionary maps, params object[] objs) + { + List updated = []; + if (!Directory.Exists(ReflectionSet.GameMapFolderPath)) return updated; + + string[] dlls = Directory.GetFiles(ReflectionSet.GameMapFolderPath, "*.dll", SearchOption.AllDirectories); + + foreach (string dll in dlls) + { + bool loaded = TryLoadOrReload(dll, out List instances, false, true); + foreach (IAddon instance in instances) + { + string name = instance.Name.Trim(); + if (instance is GameMap mapInstance) + { + // 热更新无变化的文件时,不会再触发 Load 方法 + if ((loaded || !maps.ContainsKey(name)) && mapInstance.Load(objs)) + { + updated.Add(mapInstance); + } + maps[name] = mapInstance; + } + } + } + + return updated; + } + + /// + /// 在任务计划中定期执行 + /// + internal static void CleanUnusedContexts() + { + lock (_contextsToClean) + { + for (int i = _contextsToClean.Count - 1; i >= 0; i--) + { + if (!_contextsToClean[i].TryGetTarget(out AssemblyLoadContext? ctx) || ctx.IsCollectible == false) + { + _contextsToClean.RemoveAt(i); + continue; + } + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + if (!ctx.IsCollectible) + { + _contextsToClean.RemoveAt(i); + } + } + } + } + + /// + /// 热更新 DLL + /// + /// DLL 完整路径 + /// 是否成功热更新 + internal static bool HotReload(string filePath) + { + return TryLoadOrReload(filePath, out _, isPlugin: Directory.GetParent(filePath)?.FullName == ReflectionSet.PluginFolderPath, isModule: Directory.GetParent(filePath)?.FullName == ReflectionSet.GameModuleFolderPath); + } + + /// + /// 尝试获取当前最新的实例 + /// + /// 预期类型 + /// 插件/模组名称 + /// 最新的实例 + /// 是否找到 + internal static bool TryGetLiveInstance(string addonName, out T? instance) where T : class, IAddon + { + instance = null; + + // 所有的 Plugin 都继承自 IPlugin -> IAddon,除此之外都是 IAddon,所以先从 Plugin 里找 + if (typeof(T).IsSubclassOf(typeof(IPlugin))) + { + if (Plugins.FirstOrDefault(kv => kv.Key == addonName).Value is T plugin) + { + instance = plugin; + } + } + else if (Modules.FirstOrDefault(kv => kv.Key == addonName).Value is T module) + { + instance = module; + } + + if (instance != null) + { + return true; + } + + return false; + } + + /// + /// 每个加载项都拥有独立的可收集 AssemblyLoadContext + /// + private class AddonLoadContext(string addonName, string filePath) : AssemblyLoadContext($"{addonName}_{Guid.NewGuid():N}", isCollectible: true) + { + public string AddonName { get; } = addonName; + public string FilePath { get; } = filePath; + public DateTime LastWriteTimeUtc { get; } = File.GetLastWriteTimeUtc(filePath); + public AssemblyDependencyResolver Resolver { get; } = new(filePath); + + /// + /// 读取加载项的依赖项 + /// + protected override Assembly? Load(AssemblyName assemblyName) + { + string? assemblyPath = Resolver.ResolveAssemblyToPath(assemblyName); + + if (assemblyPath != null) + { + return LoadFromAssemblyPath(assemblyPath); + } + + return null; + } + + /// + /// 读取 native dll 依赖项 + /// + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + string? nativePath = Resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + if (nativePath != null) + { + return LoadUnmanagedDllFromPath(nativePath); + } + return base.LoadUnmanagedDll(unmanagedDllName); + } + } + + /// + /// 记录 DLL 信息 + /// + private class DLLAddonEntry + { + public required AddonLoadContext Context { get; set; } + public required Assembly Assembly { get; set; } + public required DateTime LastWriteTimeUtc { get; set; } + public List Addons { get; } = []; + } + + /// + /// 记录加载项信息 + /// + private class AddonSubEntry + { + public required IAddon Instance { get; set; } + public required WeakReference Weak { get; set; } + public required string Name { get; set; } + } + } +}