因为准备弃坑FF14了,打算在弃坑之前把直感和邮差最后完善一下。结果在@PrototypeSeiren大佬的超强技术力支持和拱火下决定把邮差彻底重构一下,移植为ACT插件。正好之前答应了呆萌大佬的直感移植一直还欠着,就当趁机熟悉一下ACT插件的写法了。
没想到,接连不断的踩坑噩梦就此开始了——
ACT主程序
Fody无法打包Nancy和GrayMagic
旧版本的独立程序版鲶鱼精邮差是通过Nancy处理来自Triggernometry的HTTP请求的。但是在直接移植后发现ACT加载插件时候会跳错“未能加载文件或程序集Nancy或它的一个依赖项,系统找不到指定的文件”。必须将Nancy的dll文件放在ACT的根目录下后启动ACT才能正常工作。
问了一下呆萌,得到回复是需要自己实现AssemblyResolver。于是删掉Fody,编译运行,果不其然问题依旧。
于是研究了一个小时Nancy的文档,发现由于Nancy的结构原因,需要你自己建立类继承NancyModule来处理访问请求。这导致在ACT加载我们自己的插件DLL并初始化之前,就已经需要来自Nancy的NancyModule类信息。而这个时候我们自己的AssemblyResolver还没能完成注册。Nancy的架构并不支持我们做到Lazyload——即当我们完成插件自身的初始化,用到Nancy时再手动加载Nancy的资源。因此这个问题在我的能力范围内可以算是无解。最后只好拿HTTPListener撸了个轮子凑合解决了问题。
万幸的是,由于GrayMagic是我们自己按需加载的,所以还是可以确保AssemblyResolver正常生效。但是紧跟着,下一个坑就来了——
无法获取DLL自身路径
由于插件本身将自身PostNamazu.dll和GrayMagic.dll放在了同一个目录下。为了加载GrayMagic,就必须要获取dll自身的路径来组合出GrayMagic.dll的路径。结果加载时候毫不意外的直接报错了
调试了一下发现,在插件中调用System.Reflection.Assembly.GetExecutingAssembly().Location
尝试取得插件路径时会获取到空字符串。
直接百度一下会发现几乎没人提到过这个问题。怪了,这问题这么冷门吗……
最后还是在官方文档上找到了解答。
如果主程序没有直接加载硬盘上的DLL文件,而是首先将DLL文件以
byte[]
形式读取至内存,然后通过Assembly.Load(Byte[])
的方式加载的话,那么对象的.Location
属性获取到的就会是空字符串。
绝了……问了下獭爹,发现獭爹实力避坑:
不过不愧是獭爹,2分钟后就给出了正确答案
由于在移植过程中遇到的各种问题,我已经本能的不对ACT抱有指望了,真是没想到在这里ACT居然已经留下了应对措施。通过pluginFile.DirectoryName成功的获取到了插件的自身路径,这个问题算是解决了。
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
var name = args.Name.Split(',')[0];
if (name != "GreyMagic") return null;
var selfPluginData = ActGlobals.oFormActMain.PluginGetSelfData(this);
var path = selfPluginData.pluginFile.DirectoryName;
return Assembly.LoadFile($@"{path}\{name}.dll");
}
Winform在高DPI下界面崩坏
在这次之前我已经有将近3年没碰winform了,所以一时间还没想起来这个问题…结果在最后调试时候忽然想起来了,改了一下ACT主程序的缩放模式,果不其然,界面瞬间崩坏
Winform的高DPI适配难度我可是深有体会。不过这里其实是有一个偷懒小技巧的。就是利用单行Textbox类控件自动适配大小的特性。例如图中的NumericUpDown控件。由于此类控件的高度会随着其显示内容而自动调整,因此其他控件只需要以这个NumericUpDown控件的Height属性为基准调整自身的高度、位置就行了。
最后的效果说不上多好,但是起码界面比例不会崩坏了,就这样吧
主线程无法Catch监听线程抛出的异常
严格来说其实是多线程的正常特性,而不是ACT的坑。子线程是独立于主线程之外的,主线程执行结束了子线程可能还在继续运行,而当子线程抛出异常时,主线程可能早就结束很久了,因此让主线程去catch子线程抛出的错误本身就不合理。正确的处理方法本来就应该是在子线程中自行处理自己内部抛出的异常。
但是ACT是一个winform程序,并且提供给我们和用户的交互窗口只有插件本身的面板窗口。因此我们在错误处理过程中,不可避免的需要对主界面上的控件进行操作,向Textbox中插入日志,修改开关按钮的Enabled状态,禁用不可用选项等。直接将这些操作写在最里层模块的Catch中毫无疑问非常不利于解耦。
为了解决这些问题,我们就必须要用委托的方式,让里层的子线程的Catch段使用外层中提供的委托来对界面控件进行操作。
_httpServer = new HttpServer((int) PluginUI.TextPort.Value);
_httpServer.ReceivedCommandRequest += DoTextCommand;
_httpServer.ReceivedWayMarksRequest += DoWaymarks;
_httpServer.OnException += OnException;
private void OnException(Exception ex)
{
string errorMessage = $"无法在{_httpServer.Port}端口启动监听\n{ex.Message}";
PluginUI.ButtonStart.Enabled = true;
PluginUI.ButtonStop.Enabled = false;
PluginUI.Log(errorMessage);
MessageBox.Show(errorMessage);
}
把这么基础的东西放在这感觉有点水,但是说实话WPF写得多了我一开始也完全忘记了Winform跨线程不能直接访问控件这回事。只能说绑定真是太好用了,让人沉迷。
解析插件
解析插件加载时不对应游戏进程
在ACT按照插件列表的顺序依次初始化所有插件的过程中,即使游戏本身已经启动,解析插件此时也并不会开始查找游戏进程,此时通过解析插件自身的接口DataRepository.GetCurrentFFXIVProcess()
获取到的游戏进程为空。这也就意味着即使你的插件在解析插件之后初始化,也不能在初始化的过程中直接获取并操作当前游戏进程,必须要在经过一段时间后方可进行此类操作。
不过解析插件自身有一个很方便的特性,那就是在第一次绑定一个游戏进程的时候也会触发ProcessChanged事件,因此可以在自己的插件中绑定此事件对游戏进行初始化操作。然而方便归方便,这里面其实还有2个坑
首先,ProcessChanged事件并不是仅仅在游戏进程发生了实际切换时才会触发,例如你当前有2个进程10000和20000。默认的Auto情况下,解析插件对应的进程是10000,此时你在解析插件的面板中从Auto切换至10000,ProcessChanged事件并不会触发。但是当你从10000切换回Auto时,虽然解析插件对应的游戏进程依然没有改变,但是ProcessChanged事件会被触发。因此在自己的插件中响应这个事件时,需要自己储存旧的游戏进程并和新的进程进行对比来判断是否需要进行操作。
其次,就是下面说到的——
ProcessChanged事件不举
在当前唯一的游戏进程退出后,重新启动新的游戏客户端时,解析插件的ProcessChanged事件不会触发,因此导致邮差无法及时响应并更新自身对应的游戏进程。
虽然事件没有触发,但是从ACT标题栏显示的信息可以确定此时解析插件已经正确的获取到了新的游戏进程信息,通过DataRepository.GetCurrentFFXIVProcess()
也可以正确获取到新的游戏进程。
听闻这是个早有的bug,獭爹在1月份就提了issue,但是直到8月份国际服更新5.3对应的解析插件时才被修复。但是经过实际测试,这个bug似乎并没有被完全修复,目前国际服最新版本的解析插件依然存在这个问题。
最后解决方法:抛弃响应解析插件的ProcessChanged事件,在邮差初始化时创建BackgroundWorker,手动持续获取解析插件当前的DataRepository.GetCurrentFFXIVProcess()
属性来判断游戏进程的切换。
内存泄漏
这就非常的离谱。测试时候偶然发现,每次一开ACT,游戏内存就直接起飞,搞的我以为是我自己的插件写出了问题。结果查了2个小时最后发现即使用纯净环境下的ACT,把其他插件都删干净只留一个解析插件。在解析插件启用后依然会瞬间导致游戏本身的内存爆炸。
在群里问了一圈发现这个问题好像每个人都有遇到,但是大多数人只会增加一百到几百兆的内存占用,几乎可以忽略,很难被发现。但是唯独我这里遇到的情况是一下吃掉了2个多G的内存,而且关闭解析插件与ACT都不会释放,只能重启游戏。这个谜一样的内存泄漏至今没找到解决办法,不过既然不影响插件工作,而且在其他电脑上影响都可忽略,这里就先无视了。
Triggernometry高级触发器
请求编码与内容不对应
Triggernometry提交的Post Json数据,通过.Request.ContentEncoding
获取到的编码为GB2132,但是实际内容使用的编码固定为UTF-8。
因此根据.Request.ContentEncoding
所创建的StreamReader在读取接收到的包含中文的指令时就会产生乱码。
解决方法:无视.Request.ContentEncoding
属性,固定使用UTF-8编码解析接收到的文本指令即可。
非异步触发器测试问题
这个在发布页面也有写,因为和前面踩的各种开发过程中的坑不同,这个坑是面向使用者在使用过程中的坑,所以需要使用者去特别注意。
在Triggernometry高级触发器中建立将指令发送给鲶鱼精邮差的非异步(即:没有勾选计划任务页的“异步执行,不会阻止执行其他操作”选项的)触发器时,点击主界面的Test Action将会造成ACT假死直至超时(持续数分钟)
这是由于在Triggernometry中测试非异步触发器时,触发器将会使用ACT的当前主进程进行触发器的测试,并且在获得反馈结果之前,将会冻结ACT主进程阻止后续操作。因此同为ACT插件的邮差也会被阻塞,无法接收到Triggernometry的触发指令并进行反馈,造成死锁。此状况将会一直持续直至Triggernometry的操作由于超时而被中断。
此情况仅会出现于手动点击Test Action进行触发器测试的场合。当触发器由游戏内日志行正常触发时,无论是同步还是异步执行的触发器都不会阻塞ACT的主线程,也不会发生上述的死锁现象。此外,对于异步触发器,即使通过在触发器页面手动点击Test Action进行触发器测试,Triggernometry也会在新建立的线程中执行触发器操作,而不会阻塞ACT主进程。因此同样不会造成死锁。
综上,此问题并不会影响鲶鱼精邮差与Triggernometry在游戏中的正常使用(无论是同步还是异步触发器操作)。但是在测试自己建立的触发器时,为了防止上述情况发生,需要尽量将与邮差进行交互的触发器操作设置为异步执行,或通过游戏内日志的触发方式对触发器进行测试。
最后想说的
最后的成品插件版邮差已经开源。里面记录着前面我踩过的每一个深坑。只能说ACT毕竟也是个将近10年前的老玩具了,很多当时可能看起来很先进的开发理念放到现在显得非常不人性化,不过好在大多数都能克服。不过你要让我再来一次我是铁定拒绝的。
感谢夏影大佬在ACT插件上的贡献~
同时也在这里打扰一下问个问题,邮差在开启后,日志中仅有一行开始监听,连Getting offsets,和找到进程两行日志都无法显示,这是什么原因呢
啊,其实你最好是在github上提issue,在这里我不一定会常看…
如果你用的是咖啡的ACT的话,需要更新一下最新版本的邮差。咖啡ACT不知道哪一部分的改动会导致获取不到GrayMagic的dll路径,所以我从1.3.0.0版本之后干脆就把GrayMagic也一起打包了以避免这个问题。