添加加载项热更新功能 (#148)

* 添加加载项热更新功能

* 添加加载项卸载的内部实现,完善示例代码
This commit is contained in:
milimoe 2026-02-06 09:32:28 +08:00 committed by GitHub
parent c4e29b1f4f
commit 915de4bc36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 3096 additions and 409 deletions

View File

@ -68,6 +68,43 @@ namespace Milimoe.FunGame.Core.Api.Utility
} }
} }
/// <summary>
/// 移除工厂方法
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="d"></param>
public void UnRegisterFactory<T>(EntityFactoryDelegate<T> d)
{
if (typeof(T) == typeof(Character) && d is EntityFactoryDelegate<Character> character)
{
CharacterFactories.Remove(character);
}
if (typeof(T) == typeof(Inventory) && d is EntityFactoryDelegate<Inventory> inventory)
{
InventoryFactories.Remove(inventory);
}
if (typeof(T) == typeof(Skill) && d is EntityFactoryDelegate<Skill> skill)
{
SkillFactories.Remove(skill);
}
if (typeof(T) == typeof(Effect) && d is EntityFactoryDelegate<Effect> effect)
{
EffectFactories.Remove(effect);
}
if (typeof(T) == typeof(Item) && d is EntityFactoryDelegate<Item> item)
{
ItemFactories.Remove(item);
}
if (typeof(T) == typeof(Room) && d is EntityFactoryDelegate<Room> room)
{
RoomFactories.Remove(room);
}
if (typeof(T) == typeof(User) && d is EntityFactoryDelegate<User> user)
{
UserFactories.Remove(user);
}
}
/// <summary> /// <summary>
/// 构造一个实体实例 /// 构造一个实体实例
/// </summary> /// </summary>

View File

@ -39,9 +39,17 @@ namespace Milimoe.FunGame.Core.Api.Utility
/// <summary> /// <summary>
/// 已加载的模组DLL名称对应的路径 /// 已加载的模组DLL名称对应的路径
/// </summary> /// </summary>
public static Dictionary<string, string> ModuleFilePaths => new(AddonManager.ModuleFilePaths); public Dictionary<string, string> ModuleFilePaths => IsHotLoadMode ? new(HotLoadAddonManager.ModuleFilePaths) : new(AddonManager.ModuleFilePaths);
private GameModuleLoader() { } /// <summary>
/// 使用可热更新的加载项模式
/// </summary>
public bool IsHotLoadMode { get; } = false;
private GameModuleLoader(bool hotMode = false)
{
IsHotLoadMode = hotMode;
}
/// <summary> /// <summary>
/// 传入 <see cref="FunGameInfo.FunGame"/> 类型来创建指定端的模组读取器 /// 传入 <see cref="FunGameInfo.FunGame"/> 类型来创建指定端的模组读取器
@ -61,11 +69,13 @@ namespace Milimoe.FunGame.Core.Api.Utility
AddonManager.LoadGameMaps(loader.Maps, otherobjs); AddonManager.LoadGameMaps(loader.Maps, otherobjs);
foreach (GameMap map in loader.Maps.Values.ToList()) foreach (GameMap map in loader.Maps.Values.ToList())
{ {
map.ModuleLoader = loader;
map.AfterLoad(loader, otherobjs); map.AfterLoad(loader, otherobjs);
} }
AddonManager.LoadGameModules(loader.Modules, loader.Characters, loader.Skills, loader.Items, delegates, otherobjs); AddonManager.LoadGameModules(loader.Modules, loader.Characters, loader.Skills, loader.Items, delegates, otherobjs);
foreach (GameModule module in loader.Modules.Values.ToList()) foreach (GameModule module in loader.Modules.Values.ToList())
{ {
module.ModuleLoader = loader;
// 读取模组的依赖集合 // 读取模组的依赖集合
module.GameModuleDepend.GetDependencies(loader); module.GameModuleDepend.GetDependencies(loader);
// 如果模组加载后需要执行代码请重写AfterLoad方法 // 如果模组加载后需要执行代码请重写AfterLoad方法
@ -77,11 +87,13 @@ namespace Milimoe.FunGame.Core.Api.Utility
AddonManager.LoadGameMaps(loader.Maps, otherobjs); AddonManager.LoadGameMaps(loader.Maps, otherobjs);
foreach (GameMap map in loader.Maps.Values.ToList()) foreach (GameMap map in loader.Maps.Values.ToList())
{ {
map.ModuleLoader = loader;
map.AfterLoad(loader, otherobjs); map.AfterLoad(loader, otherobjs);
} }
AddonManager.LoadGameModulesForServer(loader.ModuleServers, loader.Characters, loader.Skills, loader.Items, delegates, otherobjs); AddonManager.LoadGameModulesForServer(loader.ModuleServers, loader.Characters, loader.Skills, loader.Items, delegates, otherobjs);
foreach (GameModuleServer server in loader.ModuleServers.Values.ToList()) foreach (GameModuleServer server in loader.ModuleServers.Values.ToList())
{ {
server.ModuleLoader = loader;
server.GameModuleDepend.GetDependencies(loader); server.GameModuleDepend.GetDependencies(loader);
server.AfterLoad(loader, otherobjs); server.AfterLoad(loader, otherobjs);
} }
@ -89,6 +101,101 @@ namespace Milimoe.FunGame.Core.Api.Utility
return loader; return loader;
} }
/// <summary>
/// 传入 <see cref="FunGameInfo.FunGame"/> 类型来创建指定端的模组读取器 [ 可热更新模式 ]
/// <para>runtime = <see cref="FunGameInfo.FunGame.FunGame_Desktop"/> 时,仅读取 <seealso cref="Modules"/></para>
/// <para>runtime = <see cref="FunGameInfo.FunGame.FunGame_Server"/> 时,仅读取 <seealso cref="ModuleServers"/></para>
/// <seealso cref="Maps"/> 都会读取
/// </summary>
/// <param name="runtime">传入 <see cref="FunGameInfo.FunGame"/> 类型来创建指定端的模组读取器</param>
/// <param name="delegates">用于构建 <see cref="Controller.AddonController{T}"/></param>
/// <param name="otherobjs">其他需要传入给插件初始化的对象</param>
/// <returns></returns>
public static GameModuleLoader LoadGameModulesByHotLoadMode(FunGameInfo.FunGame runtime, Dictionary<string, object> delegates, params object[] otherobjs)
{
GameModuleLoader loader = new(true);
if (runtime == FunGameInfo.FunGame.FunGame_Desktop)
{
List<GameMap> updated = HotLoadAddonManager.LoadGameMaps(loader.Maps, otherobjs);
foreach (GameMap map in updated)
{
map.ModuleLoader = loader;
map.AfterLoad(loader, otherobjs);
}
List<GameModule> 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<GameMap> updated = HotLoadAddonManager.LoadGameMaps(loader.Maps, otherobjs);
foreach (GameMap map in updated)
{
map.ModuleLoader = loader;
map.AfterLoad(loader, otherobjs);
}
List<GameModuleServer> 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;
}
/// <summary>
/// 热更新
/// </summary>
/// <param name="runtime"></param>
/// <param name="delegates"></param>
/// <param name="otherobjs"></param>
public void HotReload(FunGameInfo.FunGame runtime, Dictionary<string, object> delegates, params object[] otherobjs)
{
if (!IsHotLoadMode) return;
if (runtime == FunGameInfo.FunGame.FunGame_Desktop)
{
List<GameMap> updated = HotLoadAddonManager.LoadGameMaps(Maps, otherobjs);
foreach (GameMap map in updated)
{
map.ModuleLoader = this;
map.AfterLoad(this, otherobjs);
}
List<GameModule> 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<GameMap> updated = HotLoadAddonManager.LoadGameMaps(Maps, otherobjs);
foreach (GameMap map in updated)
{
map.ModuleLoader = this;
map.AfterLoad(this, otherobjs);
}
List<GameModuleServer> 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);
}
}
}
/// <summary> /// <summary>
/// 获取对应名称的模组实例 /// 获取对应名称的模组实例
/// <para>如果需要取得服务器模组的实例,请调用 <see cref="GetServerMode"/></para> /// <para>如果需要取得服务器模组的实例,请调用 <see cref="GetServerMode"/></para>

View File

@ -6,8 +6,10 @@ using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Milimoe.FunGame.Core.Interface.Addons;
using Milimoe.FunGame.Core.Library.Common.Architecture; using Milimoe.FunGame.Core.Library.Common.Architecture;
using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Library.Constant;
using Milimoe.FunGame.Core.Service;
// 通用工具类,客户端和服务器端都可以直接调用的工具方法都可以写在这里 // 通用工具类,客户端和服务器端都可以直接调用的工具方法都可以写在这里
namespace Milimoe.FunGame.Core.Api.Utility namespace Milimoe.FunGame.Core.Api.Utility
@ -850,4 +852,27 @@ namespace Milimoe.FunGame.Core.Api.Utility
} }
#endregion #endregion
#region
public class HotLoadAddonUtility
{
/// <summary>
/// 热更新 DLL
/// </summary>
/// <param name="filePath">DLL 完整路径</param>
/// <returns>是否成功热更新</returns>
public static bool HotReload(string filePath) => HotLoadAddonManager.HotReload(filePath);
/// <summary>
/// 尝试获取当前最新的实例
/// </summary>
/// <typeparam name="T">预期类型</typeparam>
/// <param name="addonName">插件/模组名称</param>
/// <param name="instance">最新的实例</param>
/// <returns>是否找到</returns>
public static bool TryGetLiveInstance<T>(string addonName, out T? instance) where T : class, IAddon => HotLoadAddonManager.TryGetLiveInstance(addonName, out instance);
}
#endregion
} }

View File

@ -15,11 +15,16 @@ namespace Milimoe.FunGame.Core.Api.Utility
/// <summary> /// <summary>
/// 已加载的插件DLL名称对应的路径 /// 已加载的插件DLL名称对应的路径
/// </summary> /// </summary>
public static Dictionary<string, string> PluginFilePaths => new(AddonManager.PluginFilePaths); public Dictionary<string, string> PluginFilePaths => IsHotLoadMode ? new(HotLoadAddonManager.PluginFilePaths) : new(AddonManager.PluginFilePaths);
private PluginLoader() /// <summary>
/// 使用可热更新的加载项模式
/// </summary>
public bool IsHotLoadMode { get; } = false;
private PluginLoader(bool hotMode = false)
{ {
IsHotLoadMode = hotMode;
} }
/// <summary> /// <summary>
@ -34,12 +39,48 @@ namespace Milimoe.FunGame.Core.Api.Utility
AddonManager.LoadPlugins(loader.Plugins, delegates, otherobjs); AddonManager.LoadPlugins(loader.Plugins, delegates, otherobjs);
foreach (Plugin plugin in loader.Plugins.Values.ToList()) foreach (Plugin plugin in loader.Plugins.Values.ToList())
{ {
plugin.PluginLoader = loader;
// 如果插件加载后需要执行代码请重写AfterLoad方法 // 如果插件加载后需要执行代码请重写AfterLoad方法
plugin.AfterLoad(loader, otherobjs); plugin.AfterLoad(loader, otherobjs);
} }
return loader; return loader;
} }
/// <summary>
/// 构建一个插件读取器并读取插件 [ 可热更新模式 ]
/// </summary>
/// <param name="delegates">用于构建 <see cref="Controller.AddonController{T}"/></param>
/// <param name="otherobjs">其他需要传入给插件初始化的对象</param>
/// <returns></returns>
public static PluginLoader LoadPluginsByHotLoadMode(Dictionary<string, object> delegates, params object[] otherobjs)
{
PluginLoader loader = new();
List<Plugin> updated = HotLoadAddonManager.LoadPlugins(loader.Plugins, delegates, otherobjs);
foreach (Plugin plugin in updated)
{
plugin.PluginLoader = loader;
// 如果插件加载后需要执行代码请重写AfterLoad方法
plugin.AfterLoad(loader, otherobjs);
}
return loader;
}
/// <summary>
/// 热更新
/// </summary>
/// <param name="delegates"></param>
/// <param name="otherobjs"></param>
public void HotReload(Dictionary<string, object> delegates, params object[] otherobjs)
{
if (!IsHotLoadMode) return;
List<Plugin> updated = HotLoadAddonManager.LoadPlugins(Plugins, delegates, otherobjs);
foreach (Plugin plugin in updated)
{
plugin.PluginLoader = this;
plugin.AfterLoad(this, otherobjs);
}
}
public Plugin this[string name] public Plugin this[string name]
{ {
get get

View File

@ -15,11 +15,16 @@ namespace Milimoe.FunGame.Core.Api.Utility
/// <summary> /// <summary>
/// 已加载的插件DLL名称对应的路径 /// 已加载的插件DLL名称对应的路径
/// </summary> /// </summary>
public static Dictionary<string, string> PluginFilePaths => new(AddonManager.PluginFilePaths); public Dictionary<string, string> PluginFilePaths => IsHotLoadMode ? new(HotLoadAddonManager.PluginFilePaths) : new(AddonManager.PluginFilePaths);
private ServerPluginLoader() /// <summary>
/// 使用可热更新的加载项模式
/// </summary>
public bool IsHotLoadMode { get; } = false;
private ServerPluginLoader(bool hotMode = false)
{ {
IsHotLoadMode = hotMode;
} }
/// <summary> /// <summary>
@ -34,12 +39,48 @@ namespace Milimoe.FunGame.Core.Api.Utility
AddonManager.LoadServerPlugins(loader.Plugins, delegates, otherobjs); AddonManager.LoadServerPlugins(loader.Plugins, delegates, otherobjs);
foreach (ServerPlugin plugin in loader.Plugins.Values.ToList()) foreach (ServerPlugin plugin in loader.Plugins.Values.ToList())
{ {
plugin.PluginLoader = loader;
// 如果插件加载后需要执行代码请重写AfterLoad方法 // 如果插件加载后需要执行代码请重写AfterLoad方法
plugin.AfterLoad(loader, otherobjs); plugin.AfterLoad(loader, otherobjs);
} }
return loader; return loader;
} }
/// <summary>
/// 构建一个插件读取器并读取插件 [ 可热更新模式 ]
/// </summary>
/// <param name="delegates">用于构建 <see cref="Controller.BaseAddonController{T}"/></param>
/// <param name="otherobjs">其他需要传入给插件初始化的对象</param>
/// <returns></returns>
public static ServerPluginLoader LoadPluginsByHotLoadMode(Dictionary<string, object> delegates, params object[] otherobjs)
{
ServerPluginLoader loader = new(true);
List<ServerPlugin> updated = HotLoadAddonManager.LoadServerPlugins(loader.Plugins, delegates, otherobjs);
foreach (ServerPlugin plugin in updated)
{
plugin.PluginLoader = loader;
// 如果插件加载后需要执行代码请重写AfterLoad方法
plugin.AfterLoad(loader, otherobjs);
}
return loader;
}
/// <summary>
/// 热更新
/// </summary>
/// <param name="delegates"></param>
/// <param name="otherobjs"></param>
public void HotReload(Dictionary<string, object> delegates, params object[] otherobjs)
{
if (!IsHotLoadMode) return;
List<ServerPlugin> updated = HotLoadAddonManager.LoadServerPlugins(Plugins, delegates, otherobjs);
foreach (ServerPlugin plugin in updated)
{
plugin.PluginLoader = this;
plugin.AfterLoad(this, otherobjs);
}
}
public ServerPlugin this[string name] public ServerPlugin this[string name]
{ {
get get

View File

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

View File

@ -1,6 +1,7 @@
using Milimoe.FunGame.Core.Interface.Base; using Milimoe.FunGame.Core.Interface.Base;
using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Library.Constant;
using Milimoe.FunGame.Core.Model; using Milimoe.FunGame.Core.Model;
using Milimoe.FunGame.Core.Service;
namespace Milimoe.FunGame.Core.Api.Utility namespace Milimoe.FunGame.Core.Api.Utility
{ {
@ -154,6 +155,14 @@ namespace Milimoe.FunGame.Core.Api.Utility
return msg.Trim(); return msg.Trim();
} }
/// <summary>
/// 开启循环检查是否有未清除的加载项上下文
/// </summary>
public static void StartCleanUnusedAddonContexts(Action<Exception>? error = null)
{
Shared.AddRecurringTask("CleanUnusedContexts", TimeSpan.FromMinutes(2), HotLoadAddonManager.CleanUnusedContexts, true, error);
}
/// <summary> /// <summary>
/// 执行任务 /// 执行任务
/// </summary> /// </summary>

View File

@ -81,6 +81,7 @@ namespace Milimoe.FunGame.Core.Api.Utility
* Console * Console
*/ */
WriteINI("Console", "LogLevel", "INFO"); WriteINI("Console", "LogLevel", "INFO");
WriteINI("Console", "UseHotLoadAddons", "false");
/** /**
* Server * Server
*/ */

View File

@ -15,11 +15,16 @@ namespace Milimoe.FunGame.Core.Api.Utility
/// <summary> /// <summary>
/// 已加载的插件DLL名称对应的路径 /// 已加载的插件DLL名称对应的路径
/// </summary> /// </summary>
public static Dictionary<string, string> PluginFilePaths => new(AddonManager.PluginFilePaths); public Dictionary<string, string> PluginFilePaths => IsHotLoadMode ? new(HotLoadAddonManager.PluginFilePaths) : new(AddonManager.PluginFilePaths);
private WebAPIPluginLoader() /// <summary>
/// 使用可热更新的加载项模式
/// </summary>
public bool IsHotLoadMode { get; } = false;
private WebAPIPluginLoader(bool hotMode = false)
{ {
IsHotLoadMode = hotMode;
} }
/// <summary> /// <summary>
@ -34,12 +39,48 @@ namespace Milimoe.FunGame.Core.Api.Utility
AddonManager.LoadWebAPIPlugins(loader.Plugins, delegates, otherobjs); AddonManager.LoadWebAPIPlugins(loader.Plugins, delegates, otherobjs);
foreach (WebAPIPlugin plugin in loader.Plugins.Values.ToList()) foreach (WebAPIPlugin plugin in loader.Plugins.Values.ToList())
{ {
plugin.PluginLoader = loader;
// 如果插件加载后需要执行代码请重写AfterLoad方法 // 如果插件加载后需要执行代码请重写AfterLoad方法
plugin.AfterLoad(loader, otherobjs); plugin.AfterLoad(loader, otherobjs);
} }
return loader; return loader;
} }
/// <summary>
/// 构建一个插件读取器并读取插件 [ 可热更新模式 ]
/// </summary>
/// <param name="delegates">用于构建 <see cref="Controller.BaseAddonController{T}"/></param>
/// <param name="otherobjs">其他需要传入给插件初始化的对象</param>
/// <returns></returns>
public static WebAPIPluginLoader LoadPluginsByHotLoadMode(Dictionary<string, object> delegates, params object[] otherobjs)
{
WebAPIPluginLoader loader = new(true);
List<WebAPIPlugin> updated = HotLoadAddonManager.LoadWebAPIPlugins(loader.Plugins, delegates, otherobjs);
foreach (WebAPIPlugin plugin in updated)
{
plugin.PluginLoader = loader;
// 如果插件加载后需要执行代码请重写AfterLoad方法
plugin.AfterLoad(loader, otherobjs);
}
return loader;
}
/// <summary>
/// 热更新
/// </summary>
/// <param name="delegates"></param>
/// <param name="otherobjs"></param>
public void HotReload(Dictionary<string, object> delegates, params object[] otherobjs)
{
if (!IsHotLoadMode) return;
List<WebAPIPlugin> updated = HotLoadAddonManager.LoadWebAPIPlugins(Plugins, delegates, otherobjs);
foreach (WebAPIPlugin plugin in updated)
{
plugin.PluginLoader = this;
plugin.AfterLoad(this, otherobjs);
}
}
public WebAPIPlugin this[string name] public WebAPIPlugin this[string name]
{ {
get get

View File

@ -707,7 +707,7 @@ namespace Milimoe.FunGame.Core.Entity
/// 力量豁免 /// 力量豁免
/// </summary> /// </summary>
public double STRExemption => STR * GameplayEquilibriumConstant.STRtoExemptionRateMultiplier; public double STRExemption => STR * GameplayEquilibriumConstant.STRtoExemptionRateMultiplier;
/// <summary> /// <summary>
/// 敏捷豁免 /// 敏捷豁免
/// </summary> /// </summary>
@ -1531,7 +1531,7 @@ namespace Milimoe.FunGame.Core.Entity
builder.AppendLine($"等级:{Level} / {GameplayEquilibriumConstant.MaxLevel}(突破进度:{LevelBreak + 1} / {GameplayEquilibriumConstant.LevelBreakList.Count}"); 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($"经验值:{EXP:0.##}{(Level != GameplayEquilibriumConstant.MaxLevel && GameplayEquilibriumConstant.EXPUpperLimit.TryGetValue(Level, out double need) ? " / " + need : "")}");
} }
builder.AppendLine(GetSimpleAttributeInfo(showGrowth, showBasicOnly).Trim()); builder.AppendLine(GetSimpleAttributeInfo(showGrowth, showBasicOnly).Trim());
if (showMapRelated) if (showMapRelated)

View File

@ -142,7 +142,7 @@ namespace Milimoe.FunGame.Core.Entity
/// 无视免疫类型 /// 无视免疫类型
/// </summary> /// </summary>
public virtual ImmuneType IgnoreImmune { get; set; } = ImmuneType.None; public virtual ImmuneType IgnoreImmune { get; set; } = ImmuneType.None;
/// <summary> /// <summary>
/// 豁免性的具体说明 /// 豁免性的具体说明
/// </summary> /// </summary>

View File

@ -474,7 +474,7 @@ namespace Milimoe.FunGame.Core.Entity
/// <param name="response"></param> /// <param name="response"></param>
public virtual void ResolveInquiryBeforeTargetSelection(Character character, DecisionPoints dp, InquiryOptions options, InquiryResponse response) public virtual void ResolveInquiryBeforeTargetSelection(Character character, DecisionPoints dp, InquiryOptions options, InquiryResponse response)
{ {
} }
/// <summary> /// <summary>

View File

@ -8,5 +8,6 @@
public string Author { get; } public string Author { get; }
public bool Load(params object[] objs); public bool Load(params object[] objs);
public void UnLoad(params object[] objs);
} }
} }

View File

@ -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 interface IGameMap : IAddon
{ {
public GameModuleLoader? ModuleLoader { get; }
} }
} }

View File

@ -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 namespace Milimoe.FunGame.Core.Interface.Addons
{ {
@ -6,6 +7,7 @@ namespace Milimoe.FunGame.Core.Interface.Addons
IGamingRandomEventHandler, IGamingRoundEventHandler, IGamingLevelUpEventHandler, IGamingMoveEventHandler, IGamingAttackEventHandler, IGamingSkillEventHandler, IGamingItemEventHandler, IGamingMagicEventHandler, IGamingRandomEventHandler, IGamingRoundEventHandler, IGamingLevelUpEventHandler, IGamingMoveEventHandler, IGamingAttackEventHandler, IGamingSkillEventHandler, IGamingItemEventHandler, IGamingMagicEventHandler,
IGamingBuyEventHandler, IGamingSuperSkillEventHandler, IGamingPauseEventHandler, IGamingUnpauseEventHandler, IGamingSurrenderEventHandler, IGamingUpdateInfoEventHandler, IGamingPunishEventHandler, IGameModuleDepend IGamingBuyEventHandler, IGamingSuperSkillEventHandler, IGamingPauseEventHandler, IGamingUnpauseEventHandler, IGamingSurrenderEventHandler, IGamingUpdateInfoEventHandler, IGamingPunishEventHandler, IGameModuleDepend
{ {
public GameModuleLoader? ModuleLoader { get; }
public bool HideMain { get; } public bool HideMain { get; }
public void StartGame(Gaming instance, params object[] args); public void StartGame(Gaming instance, params object[] args);
public void StartUI(params object[] args); public void StartUI(params object[] args);

View File

@ -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.Common.Addon;
using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Library.Constant;
@ -6,6 +7,8 @@ namespace Milimoe.FunGame.Core.Interface.Addons
{ {
public interface IGameModuleServer : IAddon, IAddonController<IGameModuleServer>, IGameModuleDepend public interface IGameModuleServer : IAddon, IAddonController<IGameModuleServer>, IGameModuleDepend
{ {
public GameModuleLoader? ModuleLoader { get; }
public bool StartServer(GamingObject obj, params object[] args); public bool StartServer(GamingObject obj, params object[] args);
public Task<Dictionary<string, object>> GamingMessageHandler(IServerModel model, GamingType type, Dictionary<string, object> data); public Task<Dictionary<string, object>> GamingMessageHandler(IServerModel model, GamingType type, Dictionary<string, object> data);

View File

@ -0,0 +1,13 @@
namespace Milimoe.FunGame.Core.Interface.Base.Addons
{
/// <summary>
/// 实现此接口的插件/模组才能被热更新模式加载
/// </summary>
public interface IHotReloadAware
{
/// <summary>
/// 在卸载前调用,自行做一些清理,否则卸载不安全
/// </summary>
public void OnBeforeUnload();
}
}

View File

@ -51,17 +51,26 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
// 模组加载后,不允许再次加载此模组 // 模组加载后,不允许再次加载此模组
_isLoaded = true; _isLoaded = true;
// 注册工厂 // 注册工厂
Factory.OpenFactory.RegisterFactory(EntityFactory()); Factory.OpenFactory.RegisterFactory(CharacterFactory());
// 如果加载后需要执行代码请重写AfterLoad方法 // 如果加载后需要执行代码请重写AfterLoad方法
AfterLoad(); AfterLoad();
} }
return _isLoaded; return _isLoaded;
} }
/// <summary>
/// 卸载模组
/// </summary>
/// <param name="objs"></param>
public void UnLoad(params object[] objs)
{
Factory.OpenFactory.UnRegisterFactory(CharacterFactory());
}
/// <summary> /// <summary>
/// 注册工厂 /// 注册工厂
/// </summary> /// </summary>
protected virtual Factory.EntityFactoryDelegate<Character> EntityFactory() protected virtual Factory.EntityFactoryDelegate<Character> CharacterFactory()
{ {
return (id, name, args) => return (id, name, args) =>
{ {

View File

@ -1,9 +1,12 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text;
using Milimoe.FunGame.Core.Api.Transmittal; using Milimoe.FunGame.Core.Api.Transmittal;
using Milimoe.FunGame.Core.Api.Utility; using Milimoe.FunGame.Core.Api.Utility;
using Milimoe.FunGame.Core.Entity; using Milimoe.FunGame.Core.Entity;
using Milimoe.FunGame.Core.Interface; using Milimoe.FunGame.Core.Interface;
using Milimoe.FunGame.Core.Interface.Base; 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.Common.Event;
using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Library.Constant;
using Milimoe.FunGame.Core.Model; using Milimoe.FunGame.Core.Model;
@ -30,7 +33,8 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example
} }
/// <summary> /// <summary>
/// 模组:必须继承基类:<see cref="GameModule"/><para/> /// GameModule 是用于客户端的模组。每个模组都有一个对应的服务器模组,可以简单理解为“一种游戏模式”<para/>
/// 必须继承基类:<see cref="GameModule"/><para/>
/// 继承事件接口并实现其方法来使模组生效。例如继承:<seealso cref="IGamingUpdateInfoEvent"/><para/> /// 继承事件接口并实现其方法来使模组生效。例如继承:<seealso cref="IGamingUpdateInfoEvent"/><para/>
/// </summary> /// </summary>
public class ExampleGameModule : GameModule, IGamingUpdateInfoEvent public class ExampleGameModule : GameModule, IGamingUpdateInfoEvent
@ -78,8 +82,7 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example
public override void StartUI(params object[] args) public override void StartUI(params object[] args)
{ {
// 如果你是一个WPF或者Winform项目可以在这里启动你的界面 /// 如果模组不依附 <see cref="Gaming"/> 类启动或者没有UI则不需要重写此方法
// 如果没有,则不需要重写此方法
} }
public void GamingUpdateInfoEvent(object sender, GamingEventArgs e, Dictionary<string, object> data) public void GamingUpdateInfoEvent(object sender, GamingEventArgs e, Dictionary<string, object> data)
@ -112,7 +115,7 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example
/// 模组服务器:必须继承基类:<see cref="GameModuleServer"/><para/> /// 模组服务器:必须继承基类:<see cref="GameModuleServer"/><para/>
/// 使用switch块分类处理 <see cref="GamingType"/>。 /// 使用switch块分类处理 <see cref="GamingType"/>。
/// </summary> /// </summary>
public class ExampleGameModuleServer : GameModuleServer public class ExampleGameModuleServer : GameModuleServer, IHotReloadAware
{ {
/// <summary> /// <summary>
/// 注意:服务器模组的名称必须和模组名称相同。除非你指定了 <see cref="GameModule.IsConnectToOtherServerModule"/> 和 <see cref="GameModule.AssociatedServerModuleName"/> /// 注意:服务器模组的名称必须和模组名称相同。除非你指定了 <see cref="GameModule.IsConnectToOtherServerModule"/> 和 <see cref="GameModule.AssociatedServerModuleName"/>
@ -137,6 +140,8 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example
{ {
public GamingObject GamingObject { get; } = obj; public GamingObject GamingObject { get; } = obj;
public List<User> ConnectedUser { get; } = []; public List<User> ConnectedUser { get; } = [];
public List<Character> CharactersForPick { get; } = [];
public Dictionary<string, Character> UserCharacters { get; } = [];
public Dictionary<string, Dictionary<string, object>> UserData { get; } = []; public Dictionary<string, Dictionary<string, object>> UserData { get; } = [];
} }
@ -184,48 +189,24 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example
await SendGamingMessage(obj.All.Values, GamingType.UpdateInfo, data); await SendGamingMessage(obj.All.Values, GamingType.UpdateInfo, data);
// 新建一个线程等待所有玩家确认如果超时则取消游戏30秒 // 新建一个线程等待所有玩家确认如果超时则取消游戏30秒
CancellationTokenSource cts = new(); // 每200ms确认一次不需要太频繁
CancellationToken ct = cts.Token; await WaitForUsers(30, async () =>
Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(30), ct);
Task completionTask = Task.Run(async () =>
{ {
while (!ct.IsCancellationRequested) if (worker.ConnectedUser.Count == obj.Users.Count)
{ {
if (worker.ConnectedUser.Count == obj.Users.Count) Controller.WriteLine("所有玩家都已经连接。");
{ return true;
Controller.WriteLine("所有玩家都已经连接。");
return;
}
// 每200ms确认一次不需要太频繁
await Task.Delay(200);
} }
}, ct); return false;
}, 200, async () =>
// 等待完成或超时
Task completedTask = await Task.WhenAny(completionTask, timeoutTask);
if (completedTask == timeoutTask)
{ {
Controller.WriteLine("等待玩家连接超时,放弃该局游戏!", LogLevel.Warning); Controller.WriteLine("等待玩家连接超时,放弃该局游戏!", LogLevel.Warning);
cts.Cancel(); await CancelGame(obj, worker, "由于等待超时,游戏已取消!");
}, async () =>
// 通知已连接的玩家
Dictionary<string, object> timeoutData = new()
{
{ "msg", "由于等待超时,游戏已取消!" }
};
// 结束
SendEndGame(obj);
worker.ConnectedUser.Clear();
Workers.Remove(obj.Room.Roomid, out _);
}
else
{ {
cts.Cancel(); // 所有玩家都连接完毕了,可以建立一个回合制游戏了
} await StartGame(obj, worker);
});
cts.Dispose();
} }
public override async Task<Dictionary<string, object>> GamingMessageHandler(IServerModel model, GamingType type, Dictionary<string, object> data) public override async Task<Dictionary<string, object>> GamingMessageHandler(IServerModel model, GamingType type, Dictionary<string, object> data)
@ -239,17 +220,45 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example
switch (type) switch (type)
{ {
case GamingType.Connect: case GamingType.Connect:
// 编写处理“连接”命令的逻辑
// 如果需要处理客户端传递的参数获取与客户端约定好的参数key对应的值
string un = NetworkUtility.JsonDeserializeFromDictionary<string>(data, "username") ?? "";
Guid token = NetworkUtility.JsonDeserializeFromDictionary<Guid>(data, "connect_token");
if (un == username && worker.UserData.TryGetValue(username, out Dictionary<string, object>? 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<string>(data, "username") ?? "";
Guid token = NetworkUtility.JsonDeserializeFromDictionary<Guid>(data, "connect_token");
if (un == username && worker.UserData.TryGetValue(username, out Dictionary<string, object>? 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<long>(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<string>(data, "event") ?? "";
if (e.Equals("SelectSkillTargets", StringComparison.CurrentCultureIgnoreCase))
{
long caster = NetworkUtility.JsonDeserializeFromDictionary<long>(data, "caster");
long[] targets = NetworkUtility.JsonDeserializeFromDictionary<long[]>(data, "targets") ?? [];
// 接收客户端传来的目标序号并记录
if (worker.UserData.TryGetValue(username, out Dictionary<string, object>? value) && value != null)
{
value.Add("SkillTargets", targets);
}
}
break;
} }
else Controller.WriteLine(username + " 确认连接失败!", LogLevel.Warning);
break;
default: default:
await Task.Delay(1); await Task.Delay(1);
break; break;
@ -258,9 +267,307 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example
return result; 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<Task<bool>> waitSomething, int delay, Func<Task> onTimeout, Func<Task> 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<User, Character> characters = [];
List<Character> characterPickeds = [];
Dictionary<string, object> data = [];
// 首先,让玩家们选择角色
// 需要一个待选的角色池
// 这些角色可以从工厂中获取,比如:
Character character1 = Factory.OpenFactory.GetInstance<Character>(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<Character> 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<Skill>(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) =>
{
/// 如果你的逻辑都写在 <see cref="ModuleServerWorker"/> 里就不用这么麻烦每次都传 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<Character, double> 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<Character> Queue_SelectSkillTargetsEvent(ModuleServerWorker worker, Character caster, Skill skill, List<Character> enemys, List<Character> teammates, List<Grid> castRange)
{
// 这是一个需要与客户端交互的事件,其他的选择事件与之做法相同
// SyncAwaiter是一个允许同步方法安全等待异步任务完成的工具类
return SyncAwaiter.WaitResult(RequestClientSelectSkillTargets(worker, caster, skill, enemys, teammates, castRange));
}
private async Task<List<Character>> RequestClientSelectSkillTargets(ModuleServerWorker worker, Character caster, Skill skill, List<Character> enemys, List<Character> teammates, List<Grid> castRange)
{
List<Character> selectTargets = [];
Dictionary<string, object> 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<string, object>? 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<string, object>? 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<string, object> 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<string, object> data = [];
data.Add("showmessage", true);
data.Add("msg", str);
await SendGamingMessage(obj.All.Values, GamingType.UpdateInfo, data);
}
protected HashSet<IServerModel> _clientModels = []; protected HashSet<IServerModel> _clientModels = [];
/// <summary> /// <summary>
/// 匿名服务器允许客户端不经过FunGameServer的登录验证就能建立一个游戏模组连接<para/>
/// 匿名服务器示例 /// 匿名服务器示例
/// </summary> /// </summary>
/// <param name="model"></param> /// <param name="model"></param>
@ -268,7 +575,7 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example
/// <returns></returns> /// <returns></returns>
public override bool StartAnonymousServer(IServerModel model, Dictionary<string, object> data) public override bool StartAnonymousServer(IServerModel model, Dictionary<string, object> data)
{ {
// 可以做验证处理 // 可以做验证处理(这只是个演示,具体实现只需要双方约定,收发什么都无所谓)
string access_token = NetworkUtility.JsonDeserializeFromDictionary<string>(data, "access_token") ?? ""; string access_token = NetworkUtility.JsonDeserializeFromDictionary<string>(data, "access_token") ?? "";
if (access_token == "approval_access_token") if (access_token == "approval_access_token")
{ {
@ -304,6 +611,24 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example
return result; return result;
} }
/// <summary>
/// 热更新示例:必须实现 <see cref="IHotReloadAware"/> 接口才会被热更新模式加载这个模组<para/>
/// 如果想要实现端运行的所有模组都能热更新,那么这些模组都必须实现了这个接口(包括 <see cref="GameModule"/><see cref="GameMap"/><see cref="CharacterModule"/> 等等……)
/// </summary>
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);
}
}
} }
/// <summary> /// <summary>
@ -385,6 +710,22 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example
return dict; return dict;
} }
} }
protected override Factory.EntityFactoryDelegate<Character> CharacterFactory()
{
// 上面示例用 Characters 是预定义的
// 这里的工厂模式则是根据传进来的参数定制生成角色,只要重写这个方法就能注册工厂了
return (id, name, args) =>
{
return null;
};
}
public static Character CreateCharacter(long id, string name, Dictionary<string, object> args)
{
// 注册工厂后,后续创建角色只需要这样调用
return Factory.OpenFactory.GetInstance<Character>(id, name, args);
}
} }
/// <summary> /// <summary>
@ -405,14 +746,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example
get get
{ {
Dictionary<string, Skill> dict = []; Dictionary<string, Skill> dict = [];
// 技能应该在GameModule中新建类继承Skill实现再自行构造。 /// 技能应该在新建类继承Skill实现再自行构造并加入此列表。
/// 技能的实现示例参见:<see cref="ExampleSkill"/>
return dict; return dict;
} }
} }
protected override Factory.EntityFactoryDelegate<Skill> SkillFactory() protected override Factory.EntityFactoryDelegate<Skill> SkillFactory()
{ {
// 注册一个工厂根据id和name返回一个你继承实现了的类对象。 // 注册一个工厂根据id和name返回一个你继承实现了的类对象。所有的工厂使用方法参考 Character都是一样的
return (id, name, args) => return (id, name, args) =>
{ {
return null; return null;
@ -423,6 +765,18 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example
{ {
return (id, name, args) => 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);
/// 如 <see cref="ExampleOpenItemByJson"/> 中所说,特效需要在工厂中注册,方便重用
if (id == 1001)
{
return new ExampleOpenEffectExATK2(skill, args);
}
return null; return null;
}; };
} }
@ -446,7 +800,8 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example
get get
{ {
Dictionary<string, Item> dict = []; Dictionary<string, Item> dict = [];
// 物品应该在GameModule中新建类继承Item实现再自行构造。 /// 物品应该新建类继承Item实现再自行构造并加入此列表。
/// 物品的实现示例参见:<see cref="ExampleItem"/>
return dict; return dict;
} }
} }

View File

@ -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<string, object> values = new()
{
{ "exatk", }
};
Effects.Add(new ExampleOpenEffectExATK2(this, values, character));
}
public override IEnumerable<Effect> 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<string, object> 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结构参见<see cref="JsonConverter.ItemConverter"/>和<see cref="JsonConverter.SkillConverter"/>
/// 属性和值解释:
/// Active 的值是一个技能对象Passives 则是一个技能对象的数组
/// 这里的技能是是动态创建的Id可以随便填一个没有经过编码的
/// SkillType = 3 代表枚举 <see cref="SkillType.Passive"/>
/// Effects 中传入一个特效对象的数组其JSON结构参见<see cref="JsonConverter.EffectConverter"/>
/// 通常,框架要求所有的特效都要有一个编码的类并经过工厂注册,这样才能正常的动态构建对象
/// 没有在转换器上出现的属性,都会进入 <see cref="Effect.Values"/> 字典,特效可以自行解析,就像上面的 ExATK2 一样
/// 如果1001这个特效已经过工厂注册那么它的工作流程如下
Skill skill = new OpenSkill(2001, "木杖", []);
Effect effect = Factory.OpenFactory.GetInstance<Effect>(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<Item>(json) ?? Factory.GetItem();
/// 如果你有一个JSON文件专门用来定义这些动态物品可以这样加载
/// 此方法使用 <see cref="EntityModuleConfig{T}"/> 配置文件读取器
Dictionary<string, Item> exItems = Factory.GetGameModuleInstances<Item>("module_name", "file_name");
if (exItems.Count > 0)
{
item = exItems.Values.First();
}
/// 不止物品,角色和技能都支持动态创建,在工厂中注册较为关键,既能享受动态数值,也能享受编码的具体逻辑
return item;
}
}
}

View File

@ -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
{
/// <summary>
/// 非指向性技能示例1迷踪步<para/>
/// 立即将角色传送到范围内的任意一个未被角色占据的地点<para/>
/// 类型:战技
/// </summary>
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));
}
}
/// <summary>
/// 注意:特效包含于技能之中,多个特效组合成一个技能
/// </summary>
/// <param name="skill"></param>
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<Character> targets, List<Grid> grids, Dictionary<string, object> others)
{
// 只有开启了地图模式才有效
if (GamingQueue?.Map is GameMap map && grids.Count > 0)
{
map.CharacterMove(caster, map.GetCharacterCurrentGrid(caster), grids[0]);
}
}
}
/// <summary>
/// 主动技能特效示例:基于攻击力的伤害(带基础伤害)
/// </summary>
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<Character> targets, List<Grid> grids, Dictionary<string, object> 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);
//}
}
}
/// <summary>
/// 非指向性技能示例2钻石星尘<para/>
/// 对半径为 2 格的圆形区域造成魔法伤害<para/>
/// 类型:魔法
/// </summary>
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));
}
}
/// <summary>
/// 指向性技能示例:全力一击<para/>
/// 对目标造成物理伤害并打断施法<para/>
/// 类型:战技
/// </summary>
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<Character> targets, List<Grid> grids, Dictionary<string, object> others)
{
foreach (Character target in targets)
{
// 这是另一种豁免检定方式,在技能实现时,自行调用 CheckExemption只对该特效有效
if (!CheckExemption(caster, target, this))
{
InterruptCasting(target, caster);
}
}
}
}
/// <summary>
/// 被动技能示例:心灵之弦
/// </summary>
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));
}
/// <summary>
/// 特别注意:被动技能必须重写此方法,否则它不会自动添加到角色身上
/// </summary>
/// <returns></returns>
public override IEnumerable<Effect> 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}" : "");
/// <summary>
/// 被动技能的冷却时间可以借用技能的冷却时间(<see cref="Skill.CD"/> 等属性)实现,也可以内部实现,看喜好
/// </summary>
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<Effect, double> 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;
}
}
/// <summary>
/// 爆发技示例:千羽瞬华<para/>
/// 给自己加属性,并联动其他技能
/// </summary>
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<Effect, double> 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<Character> targets, List<Grid> grids, Dictionary<string, object> others)
{
ActualATKBonus = 0;
ActualPhysicalPenetrationBonus = 0;
ActualEvadeRateBonus = 0;
// 不叠加的效果通常只刷新持续时间
RemainDuration = Duration;
// 通常thisEffect本身在整局战斗中都是唯一的需要只需要判断 this 就行
if (!caster.Effects.Contains(this))
{
// 加也是加 this
caster.Effects.Add(this);
OnEffectGained(caster);
}
// 施加状态记录到回合日志中
RecordCharacterApplyEffects(caster, EffectType.DamageBoost, EffectType.PenetrationBoost);
}
}
}

View File

@ -98,6 +98,11 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
} }
} }
/// <summary>
/// 记录该模组的加载器
/// </summary>
public GameModuleLoader? ModuleLoader { get; set; } = null;
/// <summary> /// <summary>
/// 加载标记 /// 加载标记
/// </summary> /// </summary>
@ -136,6 +141,22 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
return _isLoaded; return _isLoaded;
} }
/// <summary>
/// 卸载模组
/// </summary>
/// <param name="objs"></param>
public void UnLoad(params object[] objs)
{
foreach (Grid grid in Grids.Values)
{
grid.Characters.Clear();
grid.Effects.Clear();
}
Characters.Clear();
Grids.Clear();
GridsByCoordinate.Clear();
}
/// <summary> /// <summary>
/// 地图完全加载后需要做的事 /// 地图完全加载后需要做的事
/// </summary> /// </summary>

View File

@ -69,6 +69,11 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
set => _associatedServerModuleName = value; set => _associatedServerModuleName = value;
} }
/// <summary>
/// 记录该模组的加载器
/// </summary>
public GameModuleLoader? ModuleLoader { get; set; } = null;
/// <summary> /// <summary>
/// 包含了一些常用方法的控制器 /// 包含了一些常用方法的控制器
/// </summary> /// </summary>
@ -138,6 +143,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
return _isLoaded; return _isLoaded;
} }
/// <summary>
/// 卸载模组
/// </summary>
/// <param name="objs"></param>
public void UnLoad(params object[] objs)
{
BindEvent(false);
}
/// <summary> /// <summary>
/// 模组完全加载后需要做的事 /// 模组完全加载后需要做的事
/// </summary> /// </summary>
@ -160,8 +174,14 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
/// </summary> /// </summary>
private void Init(params object[] objs) private void Init(params object[] objs)
{ {
if (objs.Length > 0) _session = (Session)objs[0]; if (objs.Length > 0 && objs[0] is Session session)
if (objs.Length > 1) _config = (FunGameConfig)objs[1]; {
_session = session;
}
if (objs.Length > 1 && objs[1] is FunGameConfig config)
{
_config = config;
}
} }
/// <summary> /// <summary>
@ -182,126 +202,246 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
/// <summary> /// <summary>
/// 绑定事件。在<see cref="BeforeLoad"/>后触发 /// 绑定事件。在<see cref="BeforeLoad"/>后触发
/// </summary> /// </summary>
private void BindEvent() private void BindEvent(bool isAdd = true)
{ {
if (this is IGamingConnectEvent) if (this is IGamingConnectEvent connect)
{ {
IGamingConnectEvent bind = (IGamingConnectEvent)this; if (isAdd)
GamingConnect += bind.GamingConnectEvent; {
GamingConnect += connect.GamingConnectEvent;
}
else
{
GamingConnect -= connect.GamingConnectEvent;
}
} }
if (this is IGamingDisconnectEvent) if (this is IGamingDisconnectEvent disconnect)
{ {
IGamingDisconnectEvent bind = (IGamingDisconnectEvent)this; if (isAdd)
GamingDisconnect += bind.GamingDisconnectEvent; {
GamingDisconnect += disconnect.GamingDisconnectEvent;
}
else
{
GamingDisconnect -= disconnect.GamingDisconnectEvent;
}
} }
if (this is IGamingReconnectEvent) if (this is IGamingReconnectEvent reconnect)
{ {
IGamingReconnectEvent bind = (IGamingReconnectEvent)this; if (isAdd)
GamingReconnect += bind.GamingReconnectEvent; {
GamingReconnect += reconnect.GamingReconnectEvent;
}
else
{
GamingReconnect -= reconnect.GamingReconnectEvent;
}
} }
if (this is IGamingBanCharacterEvent) if (this is IGamingBanCharacterEvent ban)
{ {
IGamingBanCharacterEvent bind = (IGamingBanCharacterEvent)this; if (isAdd)
GamingBanCharacter += bind.GamingBanCharacterEvent; {
GamingBanCharacter += ban.GamingBanCharacterEvent;
}
else
{
GamingBanCharacter -= ban.GamingBanCharacterEvent;
}
} }
if (this is IGamingPickCharacterEvent) if (this is IGamingPickCharacterEvent pick)
{ {
IGamingPickCharacterEvent bind = (IGamingPickCharacterEvent)this; if (isAdd)
GamingPickCharacter += bind.GamingPickCharacterEvent; {
GamingPickCharacter += pick.GamingPickCharacterEvent;
}
else
{
GamingPickCharacter -= pick.GamingPickCharacterEvent;
}
} }
if (this is IGamingRandomEvent) if (this is IGamingRandomEvent random)
{ {
IGamingRandomEvent bind = (IGamingRandomEvent)this; if (isAdd)
GamingRandom += bind.GamingRandomEvent; {
GamingRandom += random.GamingRandomEvent;
}
else
{
GamingRandom -= random.GamingRandomEvent;
}
} }
if (this is IGamingRoundEvent) if (this is IGamingRoundEvent round)
{ {
IGamingRoundEvent bind = (IGamingRoundEvent)this; if (isAdd)
GamingRound += bind.GamingRoundEvent; {
GamingRound += round.GamingRoundEvent;
}
else
{
GamingRound -= round.GamingRoundEvent;
}
} }
if (this is IGamingLevelUpEvent) if (this is IGamingLevelUpEvent levelUp)
{ {
IGamingLevelUpEvent bind = (IGamingLevelUpEvent)this; if (isAdd)
GamingLevelUp += bind.GamingLevelUpEvent; {
GamingLevelUp += levelUp.GamingLevelUpEvent;
}
else
{
GamingLevelUp -= levelUp.GamingLevelUpEvent;
}
} }
if (this is IGamingMoveEvent) if (this is IGamingMoveEvent move)
{ {
IGamingMoveEvent bind = (IGamingMoveEvent)this; if (isAdd)
GamingMove += bind.GamingMoveEvent; {
GamingMove += move.GamingMoveEvent;
}
else
{
GamingMove -= move.GamingMoveEvent;
}
} }
if (this is IGamingAttackEvent) if (this is IGamingAttackEvent attack)
{ {
IGamingAttackEvent bind = (IGamingAttackEvent)this; if (isAdd)
GamingAttack += bind.GamingAttackEvent; {
GamingAttack += attack.GamingAttackEvent;
}
else
{
GamingAttack -= attack.GamingAttackEvent;
}
} }
if (this is IGamingSkillEvent) if (this is IGamingSkillEvent skill)
{ {
IGamingSkillEvent bind = (IGamingSkillEvent)this; if (isAdd)
GamingSkill += bind.GamingSkillEvent; {
GamingSkill += skill.GamingSkillEvent;
}
else
{
GamingSkill -= skill.GamingSkillEvent;
}
} }
if (this is IGamingItemEvent) if (this is IGamingItemEvent item)
{ {
IGamingItemEvent bind = (IGamingItemEvent)this; if (isAdd)
GamingItem += bind.GamingItemEvent; {
GamingItem += item.GamingItemEvent;
}
else
{
GamingItem -= item.GamingItemEvent;
}
} }
if (this is IGamingMagicEvent) if (this is IGamingMagicEvent magic)
{ {
IGamingMagicEvent bind = (IGamingMagicEvent)this; if (isAdd)
GamingMagic += bind.GamingMagicEvent; {
GamingMagic += magic.GamingMagicEvent;
}
else
{
GamingMagic -= magic.GamingMagicEvent;
}
} }
if (this is IGamingBuyEvent) if (this is IGamingBuyEvent buy)
{ {
IGamingBuyEvent bind = (IGamingBuyEvent)this; if (isAdd)
GamingBuy += bind.GamingBuyEvent; {
GamingBuy += buy.GamingBuyEvent;
}
else
{
GamingBuy -= buy.GamingBuyEvent;
}
} }
if (this is IGamingSuperSkillEvent) if (this is IGamingSuperSkillEvent super)
{ {
IGamingSuperSkillEvent bind = (IGamingSuperSkillEvent)this; if (isAdd)
GamingSuperSkill += bind.GamingSuperSkillEvent; {
GamingSuperSkill += super.GamingSuperSkillEvent;
}
else
{
GamingSuperSkill -= super.GamingSuperSkillEvent;
}
} }
if (this is IGamingPauseEvent) if (this is IGamingPauseEvent pause)
{ {
IGamingPauseEvent bind = (IGamingPauseEvent)this; if (isAdd)
GamingPause += bind.GamingPauseEvent; {
GamingPause += pause.GamingPauseEvent;
}
else
{
GamingPause -= pause.GamingPauseEvent;
}
} }
if (this is IGamingUnpauseEvent) if (this is IGamingUnpauseEvent unpause)
{ {
IGamingUnpauseEvent bind = (IGamingUnpauseEvent)this; if (isAdd)
GamingUnpause += bind.GamingUnpauseEvent; {
GamingUnpause += unpause.GamingUnpauseEvent;
}
else
{
GamingUnpause -= unpause.GamingUnpauseEvent;
}
} }
if (this is IGamingSurrenderEvent) if (this is IGamingSurrenderEvent surrender)
{ {
IGamingSurrenderEvent bind = (IGamingSurrenderEvent)this; if (isAdd)
GamingSurrender += bind.GamingSurrenderEvent; {
GamingSurrender += surrender.GamingSurrenderEvent;
}
else
{
GamingSurrender -= surrender.GamingSurrenderEvent;
}
} }
if (this is IGamingUpdateInfoEvent) if (this is IGamingUpdateInfoEvent update)
{ {
IGamingUpdateInfoEvent bind = (IGamingUpdateInfoEvent)this; if (isAdd)
GamingUpdateInfo += bind.GamingUpdateInfoEvent; {
GamingUpdateInfo += update.GamingUpdateInfoEvent;
}
else
{
GamingUpdateInfo -= update.GamingUpdateInfoEvent;
}
} }
if (this is IGamingPunishEvent) if (this is IGamingPunishEvent punish)
{ {
IGamingPunishEvent bind = (IGamingPunishEvent)this; if (isAdd)
GamingPunish += bind.GamingPunishEvent; {
GamingPunish += punish.GamingPunishEvent;
}
else
{
GamingPunish -= punish.GamingPunishEvent;
}
} }
} }

View File

@ -45,6 +45,11 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
/// </summary> /// </summary>
public virtual bool IsAnonymous { get; set; } = false; public virtual bool IsAnonymous { get; set; } = false;
/// <summary>
/// 记录该模组的加载器
/// </summary>
public GameModuleLoader? ModuleLoader { get; set; } = null;
/// <summary> /// <summary>
/// 包含了一些常用方法的控制器 /// 包含了一些常用方法的控制器
/// </summary> /// </summary>
@ -147,6 +152,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
return _isLoaded; return _isLoaded;
} }
/// <summary>
/// 卸载模组
/// </summary>
/// <param name="objs"></param>
public void UnLoad(params object[] objs)
{
}
/// <summary> /// <summary>
/// 模组完全加载后需要做的事 /// 模组完全加载后需要做的事
/// </summary> /// </summary>

View File

@ -58,6 +58,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
return _isLoaded; return _isLoaded;
} }
/// <summary>
/// 卸载模组
/// </summary>
/// <param name="objs"></param>
public void UnLoad(params object[] objs)
{
Factory.OpenFactory.UnRegisterFactory(ItemFactory());
}
/// <summary> /// <summary>
/// 注册工厂 /// 注册工厂
/// </summary> /// </summary>

View File

@ -29,6 +29,11 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
/// </summary> /// </summary>
public abstract string Author { get; } public abstract string Author { get; }
/// <summary>
/// 记录该插件的加载器
/// </summary>
public PluginLoader? PluginLoader { get; set; } = null;
/// <summary> /// <summary>
/// 包含了一些常用方法的控制器 /// 包含了一些常用方法的控制器
/// </summary> /// </summary>
@ -79,6 +84,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
return _isLoaded; return _isLoaded;
} }
/// <summary>
/// 卸载模组
/// </summary>
/// <param name="objs"></param>
public void UnLoad(params object[] objs)
{
BindEvent(false);
}
/// <summary> /// <summary>
/// 插件完全加载后需要做的事 /// 插件完全加载后需要做的事
/// </summary> /// </summary>
@ -118,153 +132,300 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
/// <summary> /// <summary>
/// 绑定事件。在<see cref="BeforeLoad"/>后触发 /// 绑定事件。在<see cref="BeforeLoad"/>后触发
/// </summary> /// </summary>
private void BindEvent() private void BindEvent(bool isAdd = true)
{ {
if (this is IConnectEvent) if (this is IConnectEvent connect)
{ {
IConnectEvent bind = (IConnectEvent)this; if (isAdd)
BeforeConnect += bind.BeforeConnectEvent; {
AfterConnect += bind.AfterConnectEvent; 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; if (isAdd)
BeforeDisconnect += bind.BeforeDisconnectEvent; {
AfterDisconnect += bind.AfterDisconnectEvent; 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; if (isAdd)
BeforeLogin += bind.BeforeLoginEvent; {
AfterLogin += bind.AfterLoginEvent; 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; if (isAdd)
BeforeLogout += bind.BeforeLogoutEvent; {
AfterLogout += bind.AfterLogoutEvent; 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; if (isAdd)
BeforeReg += bind.BeforeRegEvent; {
AfterReg += bind.AfterRegEvent; 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; if (isAdd)
BeforeIntoRoom += bind.BeforeIntoRoomEvent; {
AfterIntoRoom += bind.AfterIntoRoomEvent; 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; if (isAdd)
BeforeSendTalk += bind.BeforeSendTalkEvent; {
AfterSendTalk += bind.AfterSendTalkEvent; 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; if (isAdd)
BeforeCreateRoom += bind.BeforeCreateRoomEvent; {
AfterCreateRoom += bind.AfterCreateRoomEvent; 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; if (isAdd)
BeforeQuitRoom += bind.BeforeQuitRoomEvent; {
AfterQuitRoom += bind.AfterQuitRoomEvent; 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; if (isAdd)
BeforeChangeRoomSetting += bind.BeforeChangeRoomSettingEvent; {
AfterChangeRoomSetting += bind.AfterChangeRoomSettingEvent; 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; if (isAdd)
BeforeStartMatch += bind.BeforeStartMatchEvent; {
AfterStartMatch += bind.AfterStartMatchEvent; 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; if (isAdd)
BeforeStartGame += bind.BeforeStartGameEvent; {
AfterStartGame += bind.AfterStartGameEvent; 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; if (isAdd)
BeforeChangeProfile += bind.BeforeChangeProfileEvent; {
AfterChangeProfile += bind.AfterChangeProfileEvent; 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; if (isAdd)
BeforeChangeAccountSetting += bind.BeforeChangeAccountSettingEvent; {
AfterChangeAccountSetting += bind.AfterChangeAccountSettingEvent; 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; if (isAdd)
BeforeOpenInventory += bind.BeforeOpenInventoryEvent; {
AfterOpenInventory += bind.AfterOpenInventoryEvent; 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; if (isAdd)
BeforeSignIn += bind.BeforeSignInEvent; {
AfterSignIn += bind.AfterSignInEvent; 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; if (isAdd)
BeforeOpenStore += bind.BeforeOpenStoreEvent; {
AfterOpenStore += bind.AfterOpenStoreEvent; 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; if (isAdd)
BeforeBuyItem += bind.BeforeBuyItemEvent; {
AfterBuyItem += bind.AfterBuyItemEvent; 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; if (isAdd)
BeforeShowRanking += bind.BeforeShowRankingEvent; {
AfterShowRanking += bind.AfterShowRankingEvent; 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; if (isAdd)
BeforeUseItem += bind.BeforeUseItemEvent; {
AfterUseItem += bind.AfterUseItemEvent; 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; if (isAdd)
BeforeEndGame += bind.BeforeEndGameEvent; {
AfterEndGame += bind.AfterEndGameEvent; BeforeEndGame += endGame.BeforeEndGameEvent;
AfterEndGame += endGame.AfterEndGameEvent;
}
else
{
BeforeEndGame -= endGame.BeforeEndGameEvent;
AfterEndGame -= endGame.AfterEndGameEvent;
}
} }
} }

View File

@ -28,6 +28,11 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
/// </summary> /// </summary>
public abstract string Author { get; } public abstract string Author { get; }
/// <summary>
/// 记录该插件的加载器
/// </summary>
public ServerPluginLoader? PluginLoader { get; set; } = null;
/// <summary> /// <summary>
/// 包含了一些常用方法的控制器 /// 包含了一些常用方法的控制器
/// </summary> /// </summary>
@ -76,6 +81,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
return _isLoaded; return _isLoaded;
} }
/// <summary>
/// 卸载模组
/// </summary>
/// <param name="objs"></param>
public void UnLoad(params object[] objs)
{
BindEvent(false);
}
/// <summary> /// <summary>
/// 接收服务器控制台的输入 /// 接收服务器控制台的输入
/// </summary> /// </summary>
@ -102,153 +116,300 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
/// <summary> /// <summary>
/// 绑定事件。在<see cref="BeforeLoad"/>后触发 /// 绑定事件。在<see cref="BeforeLoad"/>后触发
/// </summary> /// </summary>
private void BindEvent() private void BindEvent(bool isAdd = true)
{ {
if (this is IConnectEvent) if (this is IConnectEvent connect)
{ {
IConnectEvent bind = (IConnectEvent)this; if (isAdd)
BeforeConnect += bind.BeforeConnectEvent; {
AfterConnect += bind.AfterConnectEvent; 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; if (isAdd)
BeforeDisconnect += bind.BeforeDisconnectEvent; {
AfterDisconnect += bind.AfterDisconnectEvent; 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; if (isAdd)
BeforeLogin += bind.BeforeLoginEvent; {
AfterLogin += bind.AfterLoginEvent; 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; if (isAdd)
BeforeLogout += bind.BeforeLogoutEvent; {
AfterLogout += bind.AfterLogoutEvent; 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; if (isAdd)
BeforeReg += bind.BeforeRegEvent; {
AfterReg += bind.AfterRegEvent; 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; if (isAdd)
BeforeIntoRoom += bind.BeforeIntoRoomEvent; {
AfterIntoRoom += bind.AfterIntoRoomEvent; 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; if (isAdd)
BeforeSendTalk += bind.BeforeSendTalkEvent; {
AfterSendTalk += bind.AfterSendTalkEvent; 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; if (isAdd)
BeforeCreateRoom += bind.BeforeCreateRoomEvent; {
AfterCreateRoom += bind.AfterCreateRoomEvent; 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; if (isAdd)
BeforeQuitRoom += bind.BeforeQuitRoomEvent; {
AfterQuitRoom += bind.AfterQuitRoomEvent; 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; if (isAdd)
BeforeChangeRoomSetting += bind.BeforeChangeRoomSettingEvent; {
AfterChangeRoomSetting += bind.AfterChangeRoomSettingEvent; 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; if (isAdd)
BeforeStartMatch += bind.BeforeStartMatchEvent; {
AfterStartMatch += bind.AfterStartMatchEvent; 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; if (isAdd)
BeforeStartGame += bind.BeforeStartGameEvent; {
AfterStartGame += bind.AfterStartGameEvent; 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; if (isAdd)
BeforeChangeProfile += bind.BeforeChangeProfileEvent; {
AfterChangeProfile += bind.AfterChangeProfileEvent; 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; if (isAdd)
BeforeChangeAccountSetting += bind.BeforeChangeAccountSettingEvent; {
AfterChangeAccountSetting += bind.AfterChangeAccountSettingEvent; 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; if (isAdd)
BeforeOpenInventory += bind.BeforeOpenInventoryEvent; {
AfterOpenInventory += bind.AfterOpenInventoryEvent; 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; if (isAdd)
BeforeSignIn += bind.BeforeSignInEvent; {
AfterSignIn += bind.AfterSignInEvent; 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; if (isAdd)
BeforeOpenStore += bind.BeforeOpenStoreEvent; {
AfterOpenStore += bind.AfterOpenStoreEvent; 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; if (isAdd)
BeforeBuyItem += bind.BeforeBuyItemEvent; {
AfterBuyItem += bind.AfterBuyItemEvent; 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; if (isAdd)
BeforeShowRanking += bind.BeforeShowRankingEvent; {
AfterShowRanking += bind.AfterShowRankingEvent; 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; if (isAdd)
BeforeUseItem += bind.BeforeUseItemEvent; {
AfterUseItem += bind.AfterUseItemEvent; 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; if (isAdd)
BeforeEndGame += bind.BeforeEndGameEvent; {
AfterEndGame += bind.AfterEndGameEvent; BeforeEndGame += endGame.BeforeEndGameEvent;
AfterEndGame += endGame.AfterEndGameEvent;
}
else
{
BeforeEndGame -= endGame.BeforeEndGameEvent;
AfterEndGame -= endGame.AfterEndGameEvent;
}
} }
} }

View File

@ -59,6 +59,16 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
return _isLoaded; return _isLoaded;
} }
/// <summary>
/// 卸载模组
/// </summary>
/// <param name="objs"></param>
public void UnLoad(params object[] objs)
{
Factory.OpenFactory.UnRegisterFactory(SkillFactory());
Factory.OpenFactory.UnRegisterFactory(EffectFactory());
}
/// <summary> /// <summary>
/// 注册工厂 /// 注册工厂
/// </summary> /// </summary>

View File

@ -6,7 +6,7 @@ using Milimoe.FunGame.Core.Library.Common.Event;
namespace Milimoe.FunGame.Core.Library.Common.Addon namespace Milimoe.FunGame.Core.Library.Common.Addon
{ {
public abstract class WebAPIPlugin : IAddon, IAddonController<IAddon> public abstract class WebAPIPlugin : IPlugin
{ {
/// <summary> /// <summary>
/// 插件名称 /// 插件名称
@ -28,10 +28,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
/// </summary> /// </summary>
public abstract string Author { get; } public abstract string Author { get; }
/// <summary>
/// 记录该插件的加载器
/// </summary>
public WebAPIPluginLoader? PluginLoader { get; set; } = null;
/// <summary> /// <summary>
/// 包含了一些常用方法的控制器 /// 包含了一些常用方法的控制器
/// </summary> /// </summary>
public ServerAddonController<IAddon> Controller public ServerAddonController<IPlugin> Controller
{ {
get => _controller ?? throw new NotImplementedException(); get => _controller ?? throw new NotImplementedException();
internal set => _controller = value; internal set => _controller = value;
@ -40,16 +45,16 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
/// <summary> /// <summary>
/// base控制器 /// base控制器
/// </summary> /// </summary>
BaseAddonController<IAddon> IAddonController<IAddon>.Controller BaseAddonController<IPlugin> IAddonController<IPlugin>.Controller
{ {
get => Controller; get => Controller;
set => _controller = (ServerAddonController<IAddon>?)value; set => _controller = (ServerAddonController<IPlugin>?)value;
} }
/// <summary> /// <summary>
/// 控制器内部变量 /// 控制器内部变量
/// </summary> /// </summary>
private ServerAddonController<IAddon>? _controller; private ServerAddonController<IPlugin>? _controller;
/// <summary> /// <summary>
/// 加载标记 /// 加载标记
@ -76,6 +81,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
return _isLoaded; return _isLoaded;
} }
/// <summary>
/// 卸载模组
/// </summary>
/// <param name="objs"></param>
public void UnLoad(params object[] objs)
{
BindEvent(false);
}
/// <summary> /// <summary>
/// 接收服务器控制台的输入 /// 接收服务器控制台的输入
/// </summary> /// </summary>
@ -111,153 +125,300 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
/// <summary> /// <summary>
/// 绑定事件。在<see cref="BeforeLoad"/>后触发 /// 绑定事件。在<see cref="BeforeLoad"/>后触发
/// </summary> /// </summary>
private void BindEvent() private void BindEvent(bool isAdd = true)
{ {
if (this is IConnectEvent) if (this is IConnectEvent connect)
{ {
IConnectEvent bind = (IConnectEvent)this; if (isAdd)
BeforeConnect += bind.BeforeConnectEvent; {
AfterConnect += bind.AfterConnectEvent; 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; if (isAdd)
BeforeDisconnect += bind.BeforeDisconnectEvent; {
AfterDisconnect += bind.AfterDisconnectEvent; 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; if (isAdd)
BeforeLogin += bind.BeforeLoginEvent; {
AfterLogin += bind.AfterLoginEvent; 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; if (isAdd)
BeforeLogout += bind.BeforeLogoutEvent; {
AfterLogout += bind.AfterLogoutEvent; 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; if (isAdd)
BeforeReg += bind.BeforeRegEvent; {
AfterReg += bind.AfterRegEvent; 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; if (isAdd)
BeforeIntoRoom += bind.BeforeIntoRoomEvent; {
AfterIntoRoom += bind.AfterIntoRoomEvent; 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; if (isAdd)
BeforeSendTalk += bind.BeforeSendTalkEvent; {
AfterSendTalk += bind.AfterSendTalkEvent; 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; if (isAdd)
BeforeCreateRoom += bind.BeforeCreateRoomEvent; {
AfterCreateRoom += bind.AfterCreateRoomEvent; 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; if (isAdd)
BeforeQuitRoom += bind.BeforeQuitRoomEvent; {
AfterQuitRoom += bind.AfterQuitRoomEvent; 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; if (isAdd)
BeforeChangeRoomSetting += bind.BeforeChangeRoomSettingEvent; {
AfterChangeRoomSetting += bind.AfterChangeRoomSettingEvent; 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; if (isAdd)
BeforeStartMatch += bind.BeforeStartMatchEvent; {
AfterStartMatch += bind.AfterStartMatchEvent; 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; if (isAdd)
BeforeStartGame += bind.BeforeStartGameEvent; {
AfterStartGame += bind.AfterStartGameEvent; 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; if (isAdd)
BeforeChangeProfile += bind.BeforeChangeProfileEvent; {
AfterChangeProfile += bind.AfterChangeProfileEvent; 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; if (isAdd)
BeforeChangeAccountSetting += bind.BeforeChangeAccountSettingEvent; {
AfterChangeAccountSetting += bind.AfterChangeAccountSettingEvent; 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; if (isAdd)
BeforeOpenInventory += bind.BeforeOpenInventoryEvent; {
AfterOpenInventory += bind.AfterOpenInventoryEvent; 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; if (isAdd)
BeforeSignIn += bind.BeforeSignInEvent; {
AfterSignIn += bind.AfterSignInEvent; 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; if (isAdd)
BeforeOpenStore += bind.BeforeOpenStoreEvent; {
AfterOpenStore += bind.AfterOpenStoreEvent; 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; if (isAdd)
BeforeBuyItem += bind.BeforeBuyItemEvent; {
AfterBuyItem += bind.AfterBuyItemEvent; 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; if (isAdd)
BeforeShowRanking += bind.BeforeShowRankingEvent; {
AfterShowRanking += bind.AfterShowRankingEvent; 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; if (isAdd)
BeforeUseItem += bind.BeforeUseItemEvent; {
AfterUseItem += bind.AfterUseItemEvent; 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; if (isAdd)
BeforeEndGame += bind.BeforeEndGameEvent; {
AfterEndGame += bind.AfterEndGameEvent; BeforeEndGame += endGame.BeforeEndGameEvent;
AfterEndGame += endGame.AfterEndGameEvent;
}
else
{
BeforeEndGame -= endGame.BeforeEndGameEvent;
AfterEndGame -= endGame.AfterEndGameEvent;
}
} }
} }

View File

@ -0,0 +1,45 @@
namespace Milimoe.FunGame.Core.Library.Common.Architecture
{
/// <summary>
/// 该类的工具方法允许在同步方法中安全等待异步任务完成
/// </summary>
public class SyncAwaiter
{
/// <summary>
/// 在同步方法中安全等待一个 Task 完成并获取结果
/// 内部使用 ManualResetEventSlim避免死锁
/// </summary>
public static T WaitResult<T>(Task<T> 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;
}
/// <summary>
/// 无返回值版本
/// </summary>
public static void Wait(Task task)
{
if (task.IsCompleted) return;
ManualResetEventSlim mres = new(false);
task.ContinueWith(_ => mres.Set(), TaskScheduler.Default);
mres.Wait();
}
}
}

View File

@ -184,4 +184,19 @@
{ {
public override string Message => "试图构造一个不支持的类的实例 (#10037)"; 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)";
}
} }

View File

@ -11,7 +11,7 @@ namespace Milimoe.FunGame.Core.Model
/// 伤害来源 /// 伤害来源
/// </summary> /// </summary>
public Character Character { get; set; } = character; public Character Character { get; set; } = character;
/// <summary> /// <summary>
/// 完整计算伤害 /// 完整计算伤害
/// </summary> /// </summary>

View File

@ -2809,7 +2809,7 @@ namespace Milimoe.FunGame.Core.Model
} }
if (healBonus != 0) if (healBonus != 0)
{ {
totalHealBonus[effect]= healBonus; totalHealBonus[effect] = healBonus;
healStrings.Add($"{(healBonus > 0 ? " + " : " - ")}{Math.Abs(healBonus):0.##}{effect.Name}"); 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; return;
} }

View File

@ -7,6 +7,6 @@ namespace Milimoe.FunGame.Core.Model.PrefabricatedEntity
/// </summary> /// </summary>
public class CourageCommandSkill(long id, string name, Dictionary<string, object> args, Character? character = null) : OpenSkill(id, name, args, character) public class CourageCommandSkill(long id, string name, Dictionary<string, object> args, Character? character = null) : OpenSkill(id, name, args, character)
{ {
} }
} }

View File

@ -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` 框架的核心模块,包含了框架的基础组件。 本仓库 `FunGame.Core` 项目是 `FunGame` 框架的核心模块,包含了框架的基础组件。
本项目不局限于服务器端开发,在支持 `.NET` 编程的客户端项目中也能使用。 本项目不局限于服务器端开发,在支持 `.NET` 编程的客户端项目中也能使用。

View File

@ -28,7 +28,7 @@ namespace Milimoe.FunGame.Core.Service
{ {
if (!Directory.Exists(ReflectionSet.PluginFolderPath)) return plugins; 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) foreach (string dll in dlls)
{ {
@ -81,7 +81,7 @@ namespace Milimoe.FunGame.Core.Service
{ {
if (!Directory.Exists(ReflectionSet.PluginFolderPath)) return plugins; 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) foreach (string dll in dlls)
{ {
@ -134,7 +134,7 @@ namespace Milimoe.FunGame.Core.Service
{ {
if (!Directory.Exists(ReflectionSet.PluginFolderPath)) return plugins; 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) foreach (string dll in dlls)
{ {
@ -190,7 +190,7 @@ namespace Milimoe.FunGame.Core.Service
{ {
if (!Directory.Exists(ReflectionSet.GameModuleFolderPath)) return modules; 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) foreach (string dll in dlls)
{ {
@ -264,7 +264,7 @@ namespace Milimoe.FunGame.Core.Service
{ {
if (!Directory.Exists(ReflectionSet.GameModuleFolderPath)) return servers; 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) foreach (string dll in dlls)
{ {
@ -334,7 +334,7 @@ namespace Milimoe.FunGame.Core.Service
{ {
if (!Directory.Exists(ReflectionSet.GameMapFolderPath)) return maps; 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) foreach (string dll in dlls)
{ {

View File

@ -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
{
/// <summary>
/// 支持热更新、可卸载、可重载插件/模组
/// 支持插件跨 DLL 引用模组,通过全局访问最新实例
/// </summary>
internal static class HotLoadAddonManager
{
/// <summary>
/// 已加载的插件DLL名称对应的路径
/// </summary>
internal static Dictionary<string, string> PluginFilePaths { get; } = [];
/// <summary>
/// 已加载的模组DLL名称对应的路径
/// </summary>
internal static Dictionary<string, string> ModuleFilePaths { get; } = [];
/// <summary>
/// 已加载的插件
/// </summary>
internal static Dictionary<string, IAddon> Plugins { get; } = [];
/// <summary>
/// 已加载的模组
/// </summary>
internal static Dictionary<string, IAddon> Modules { get; } = [];
/// <summary>
/// key = 文件路径全小写value = 当前加载上下文 + 程序集 + 根实例
/// </summary>
private static readonly ConcurrentDictionary<string, DLLAddonEntry> _loadedDLLs = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// 即将清除的上下文
/// </summary>
private static readonly List<WeakReference<AssemblyLoadContext>> _contextsToClean = [];
/// <summary>
/// 尝试加载或重新加载某个 DLL返回是否成功加载/更新
/// </summary>
/// <param name="fullPath">DLL 完整路径</param>
/// <param name="newInstances">新加载的实例列表</param>
/// <param name="isPlugin">是否为插件</param>
/// <param name="isModule">是否为模组</param>
/// <returns>是否成功(无变化不成功)</returns>
private static bool TryLoadOrReload(string fullPath, out List<IAddon> 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<AssemblyLoadContext>(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<IAddon>(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);
}
}
/// <summary>
/// 从 plugins 目录加载所有插件
/// </summary>
/// <param name="plugins">插件字典</param>
/// <param name="delegates">委托字典</param>
/// <param name="otherobjs">其他参数</param>
/// <returns>被更新的插件列表</returns>
internal static List<Plugin> LoadPlugins(Dictionary<string, Plugin> plugins, Dictionary<string, object> delegates, params object[] otherobjs)
{
List<Plugin> 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<IAddon> 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;
}
/// <summary>
/// 从 plugins 目录加载所有 Server 插件
/// </summary>
/// <param name="plugins">插件字典</param>
/// <param name="delegates">委托字典</param>
/// <param name="otherobjs">其他参数</param>
/// <returns>被更新的插件列表</returns>
internal static List<ServerPlugin> LoadServerPlugins(Dictionary<string, ServerPlugin> plugins, Dictionary<string, object> delegates, params object[] otherobjs)
{
List<ServerPlugin> 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<IAddon> 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;
}
/// <summary>
/// 从 plugins 目录加载所有 WebAPI 插件
/// </summary>
/// <param name="plugins">插件字典</param>
/// <param name="delegates">委托字典</param>
/// <param name="otherobjs">其他参数</param>
/// <returns>被更新的插件列表</returns>
internal static List<WebAPIPlugin> LoadWebAPIPlugins(Dictionary<string, WebAPIPlugin> plugins, Dictionary<string, object> delegates, params object[] otherobjs)
{
List<WebAPIPlugin> 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<IAddon> 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;
}
/// <summary>
/// 从 modules 目录加载所有模组
/// </summary>
/// <param name="modules">模组字典</param>
/// <param name="characters">角色字典</param>
/// <param name="skills">技能字典</param>
/// <param name="items">物品字典</param>
/// <param name="delegates">委托字典</param>
/// <param name="otherobjs">其他参数</param>
/// <returns>被更新的模组列表</returns>
internal static List<GameModule> LoadGameModules(Dictionary<string, GameModule> modules, Dictionary<string, CharacterModule> characters, Dictionary<string, SkillModule> skills, Dictionary<string, ItemModule> items, Dictionary<string, object> delegates, params object[] otherobjs)
{
List<GameModule> 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<IAddon> 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;
}
/// <summary>
/// 从 modules 目录加载所有适用于服务器的模组
/// </summary>
/// <param name="servers">服务器模组字典</param>
/// <param name="characters">角色字典</param>
/// <param name="skills">技能字典</param>
/// <param name="items">物品字典</param>
/// <param name="delegates">委托字典</param>
/// <param name="otherobjs">其他参数</param>
/// <returns>被更新的服务器模组列表</returns>
internal static List<GameModuleServer> LoadGameModulesForServer(Dictionary<string, GameModuleServer> servers, Dictionary<string, CharacterModule> characters, Dictionary<string, SkillModule> skills, Dictionary<string, ItemModule> items, Dictionary<string, object> delegates, params object[] otherobjs)
{
List<GameModuleServer> 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<IAddon> 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;
}
/// <summary>
/// 从 maps 目录加载所有地图
/// </summary>
/// <param name="maps">地图字典</param>
/// <param name="objs">其他参数</param>
/// <returns>被更新的地图列表</returns>
internal static List<GameMap> LoadGameMaps(Dictionary<string, GameMap> maps, params object[] objs)
{
List<GameMap> 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<IAddon> 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;
}
/// <summary>
/// 在任务计划中定期执行
/// </summary>
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);
}
}
}
}
/// <summary>
/// 热更新 DLL
/// </summary>
/// <param name="filePath">DLL 完整路径</param>
/// <returns>是否成功热更新</returns>
internal static bool HotReload(string filePath)
{
return TryLoadOrReload(filePath, out _, isPlugin: Directory.GetParent(filePath)?.FullName == ReflectionSet.PluginFolderPath, isModule: Directory.GetParent(filePath)?.FullName == ReflectionSet.GameModuleFolderPath);
}
/// <summary>
/// 尝试获取当前最新的实例
/// </summary>
/// <typeparam name="T">预期类型</typeparam>
/// <param name="addonName">插件/模组名称</param>
/// <param name="instance">最新的实例</param>
/// <returns>是否找到</returns>
internal static bool TryGetLiveInstance<T>(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;
}
/// <summary>
/// 每个加载项都拥有独立的可收集 AssemblyLoadContext
/// </summary>
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);
/// <summary>
/// 读取加载项的依赖项
/// </summary>
protected override Assembly? Load(AssemblyName assemblyName)
{
string? assemblyPath = Resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
/// <summary>
/// 读取 native dll 依赖项
/// </summary>
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string? nativePath = Resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (nativePath != null)
{
return LoadUnmanagedDllFromPath(nativePath);
}
return base.LoadUnmanagedDll(unmanagedDllName);
}
}
/// <summary>
/// 记录 DLL 信息
/// </summary>
private class DLLAddonEntry
{
public required AddonLoadContext Context { get; set; }
public required Assembly Assembly { get; set; }
public required DateTime LastWriteTimeUtc { get; set; }
public List<AddonSubEntry> Addons { get; } = [];
}
/// <summary>
/// 记录加载项信息
/// </summary>
private class AddonSubEntry
{
public required IAddon Instance { get; set; }
public required WeakReference<IAddon> Weak { get; set; }
public required string Name { get; set; }
}
}
}