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; }
+ }
+ }
+}