哈哈没想到吧,就这篇文竟然还会有后续
ACT主程序
查找其他插件本体的方法
之前用的获取解析插件的方法是从獭爹那里偷来的
private FFXIV_ACT_Plugin.FFXIV_ACT_Plugin GetFFXIVPlugin()
{
FFXIV_ACT_Plugin.FFXIV_ACT_Plugin ffxivActPlugin = null;
foreach (var actPluginData in ActGlobals.oFormActMain.ActPlugins)
if (actPluginData.pluginFile.Name.ToUpper().Contains("FFXIV_ACT_Plugin".ToUpper()) &&
(actPluginData.lblPluginStatus.Text.ToUpper().Contains("FFXIV Plugin Started.".ToUpper()) || //国服旧版本
actPluginData.lblPluginStatus.Text.ToUpper().Contains("FFXIV_ACT_Plugin Started.".ToUpper()))) //国际服新版本
ffxivActPlugin = (FFXIV_ACT_Plugin.FFXIV_ACT_Plugin)actPluginData.pluginObj;
return ffxivActPlugin ?? throw new Exception("找不到FFXIV解析插件,请确保其加载顺序位于鲶鱼精邮差之前。");
}
这个方法本质上就是遍历ACT当前所有插件,然后判断pluginFile.Name。本来感觉这个方法应该不会有什么问题,结果没想到,经验主义害死人啊,这个属性返回的其实不是插件自己的插件名,而是DLL的文件名…
于是终于有一天遇到了非常离奇的找不到插件的bug。在痛苦排错一万年,杀毒软件和ACT背了一万口锅以后,终于发现,原来是因为对方为了在国服和国际服都能用,而自己给解析插件改了文件名为cn.dll和jp.dll…
当然这里有另一个不大不小的坑,为什么之前大佬们的写法会是判断插件的文件名,而不是直接判断插件名呢?难道大佬们不知道直接取得插件名才是更标准的做法吗?原因很简单但却让人落泪,ACT其实并没有给出任何直接取得插件实例对应的插件名的方法。虽然插件本身有个version属性,但是这个属性直接就是一个string,是一个包括了插件的名字、版本号等各种信息的格式化后的文本。要想单独提取字符串里的插件名还得单独对字符串进行处理,实在是过于不标准了。而且本着尽量少用正则的原则,最好的方法还是直接判断插件本身的type。
var plugin = ActGlobals.oFormActMain.ActPlugins.FirstOrDefault(x => x.pluginObj?.GetType().ToString() == "FFXIV_ACT_Plugin.FFXIV_ACT_Plugin")?.pluginObj;
return (FFXIV_ACT_Plugin.FFXIV_ACT_Plugin)plugin ?? throw new Exception("找不到FFXIV解析插件,请确保其加载顺序位于鲶鱼精邮差之前。");
同理,获取其他插件的方法最好也是直接GetType(),例如
//TriggerNometry
var plugin = ActGlobals.oFormActMain.ActPlugins.FirstOrDefault(x => x.pluginObj?.GetType().ToString() == "TriggernometryProxy.ProxyPlugin")?.pluginObj;
//OverlayPlugin
var plugin = ActGlobals.oFormActMain.ActPlugins.FirstOrDefault(x => x.pluginObj?.GetType().ToString() == "RainbowMage.OverlayPlugin.PluginLoader")?.pluginObj;
高DPI缩放
我不得不承认其实我是被ACT插件的样例给误导了。ACT的样例插件只有一个cs文件,直接让PluginSample类继承了UserControl, IActPluginV1
。而且很风骚地把InitializeComponent()
和其他部分全写在同一个文件里而不是单独放一个.Designer.cs。而且在ACT调用插件的初始化时直接pluginScreenSpace.Controls.Add(this);
。
哇,好孩子千万不要学。这直接高度集成化到了VS自己的Designer压根认不出来控件布局,搞得我一度以为这个UI只能自己盲写,不能快乐Winform拖拖拖。
鲁莽了,一开始确实是以为ACT规定你插件必须本体同时继承并实现UserControl
和IActPluginV1
两者。实际回头看看根本不是这样的,UserControl可以单独创建一个类,比如public partial class UIControl : UserControl
然后在插件初始化的时候
PluginUI = new UIControl();
pluginScreenSpace.Controls.Add(PluginUI);
而插件本体根本就只需要继承IActPluginV1就可以。这样UIControl就是个完全独立的UserControl
,自然可以Winform快乐拖拖拖,也可以直接作为整体缩放,完全不用再担心手搓布局遇到高DPI就崩坏的情况。
OverlayPlugin
其实原本是并不需要和悬浮窗插件打交道的,但是眼看着Triggernometry要步Hojoring的后尘,总得提前做好准备,所以从1.3.0.0版本开始,尝试着加入了悬浮窗插件的回调支持,但是不试不知道,一试就发现这玩意的坑可一点都不少。
OverlayPlugin的回调注册不像Triggernometry一样直接call一下它提供的方法就完了。它要求你必须写一个继承了IOverlayAddonV2的,独立的OverlayPlugin插件,显示在OverlayPlugin自己的插件列表中,就像Cactbot一样。
然后,这个插件才可以向OVerlayPlugin通过RegisterEventHandler()
方法注册回调。OverlayPlugin的回调只开放给自己的插件,并不随意开放给其他插件——我差点就想直接反射它的注册方法了,还好我怕麻烦(bushi
显然有了上面UI的教训,这次不会再鲶鱼精主程序同时继承UserControl, IActPluginV1,IOverlayAddonV2
这种蠢事了。但是事情也不是直接新建一个单独的类这么简单的——看过OverlayPlugin的代码后可以发现,OverlayPlugin的回调注册非常简单粗暴,就是一个String对一个方法的Dictionary<>,而引用了这个Dictionary<>的只有2个方法,1个是注册方法,里面会先对注册的key进行判断,如果同名方法已存在就直接throw,否则就写入Dictionary,另一个就是调用,根据key返回对应的方法。
于是问题来了,这里没有提供注销已有注册的方法,也没有清除全部注册的方法,而且因为注册时会先进行一次判重,如果重复直接throw,所以连覆盖旧的注册都是做不到的。换句话说,只要OverlayPlugin不重载,我们的回调注册就是一锤子买卖。
但是问题就在这了,如果我们在OverlayPlugin保持启动的情况下,在ACT的插件面板里卸载了鲶鱼精,然后又重写加载——这个时候,新的鲶鱼精和旧的显然不是同一个实例,原本注册的指向旧的鲶鱼精中对应方法的回调也就失去了目标。然而这时候,新的鲶鱼精又要怎么覆盖旧的注册呢?答案是,没有办法()
好,不愧是挖坑小王子。所以这里的唯一办法就是,单独建立一个独立的OverlayPlugin中介插件,它在鲶鱼精启动后就会注册到OverlayPlugin并保持开启,即使鲶鱼精本体被卸载,这个中介插件也依然保持启动。当新的鲶鱼精实例重新启动后,它会优先尝试寻找已有的中介插件,如果有的话,就更新那个中介插件中的回调指向。
最后想说的
啊,看看隔壁大喇嘛插件写起来多么快乐,这ACT真是太搞心态了。
不过好在其他插件(例如解析插件和高级触发器)都还在持续更新,基本都遵循了近些年普遍的开发规范,所以和其他插件打交道的过程要简单了很多。真希望呆萌和咖啡能给ACT增加一些方便的接口,不然这插件开发起来简直太让人头秃了。