前言

虽然标题写的是FFXIV ACT插件开发入门,但是这篇博客主要写的是我作为一个插件开发新手开发自己的插件的心路历程。本文的目的是给各位新手提供一些参考资料,以及记录一下常见的问题和解决方案。希望这篇文章能够给你一些启示,也欢迎各位补充说明。

本文章假设您有一定的C#语言基础,以及一些基础的WinForm开发基础,并使用Visual Studio作为自己的开发工具。

ACT 插件入门

我们通常看到的ACT插件的成品都是一个DLL(以及附带的一些文件)。我们可以直接在VS中新建一个类库项目,这样它的编译出来就会是一个DLL了。

正如ACT开发提示中所述,所有的ACT插件都需要实现 Advanced_Combat_Tracker.IActPluginV1 接口。为了让我们的类实现这个接口,我们必须先在项目中引用ACT本体。在项目依赖项中添加引用,选择下载好的ACT可执行文件,即可在代码中引用它。

using Advanced_Combat_Tracker;

class MyActPlugin : IActPluginV1 {
    // 初始化插件的方法
    void InitPlugin(TabPage pluginScreenSpace, Label pluginStatusText) {
        // 初始化插件逻辑
        // pluginScreenSpace 是插件页面的Tab,需要你自行往内部填充控件
        page.Text = "MyPlugin"; // 设置Tab的名称
        page.Controls.Add(new PluginControl()); // 填充控件,PluginControl继承的是UserControl,是WinForm的界面
        // pluginStatusText 是插件设置页面的状态指示标签,可以用于指示插件工作状态
        pluginStatusText.Text = "Plugin Inited."
    }
    // 反初始化插件的方法
    void DeInitPlugin() {
        // 处理一些收尾工作,如释放资源,保存数据等
    }
}

编译后,即可使用ACT载入编译后的插件了。启用后应该会在插件内看到一个名为MyPlugin的页面。

调用ACT内置的功能

ACT在ActGlobals类下提供了一系列静态变量,用于用户访问ACT内部的各种数据。你需要参考ACT的API文档,了解这些功能的用途。

ActGlobals.oFormActMain是ACT主窗口的实例,它包括了多个常用的API,包括:
ActPlugins: 所有的Act插件。如果你要查找另一个插件,尝试遍历这个变量。
AppDataFolder: 数据存放路径。如果你想要存放数据,请写入此目录中。
ParseRawLogLine: 将一条LogLine交给Act处理。可以用于写入日志,交给其他插件处理。
PlaySound*: 播放指定的声音
TTS: 调用TTS读出指定的文本

注意事项

和WinForm中一样,你需要注意线程安全问题。

你的代码不一定运行在UI线程上,这意味着任何对于UI的修改都是不安全并且会报错的。如果你想要修改UI(的某些项的值),你需要保证修改的代码正确的运行在UI线程上。

以下示例代码可以安全的修改UI:

ActGlobals.oFormActMain.Invoke(new Action(() => { lblStatus.Text = "Hello world" }));

FFXIV 插件集成

既然标题写了是FFXIV ACT插件开发,我们就需要涉及到与FFXIV解析插件的交互。

ravahn 在解析插件的仓库内提供了用于开发的未打包的DLL,请下载带有SDK字样的压缩包,并解压备用。而国服解析插件并未提供相应的SDK,若要使用则需要自行解压,或使用国际服版本插件替代。

如果尝试使用国际服SDK,则需要注意SDK的对应版本。国服插件的DLL与国际服SDK中提供的DLL版本可能不同,如果混用可能出现无法加载(提示强签名等)的问题。若出现此情况,请尝试使用其他版本SDK的DLL。

在本文写作时最新的5.58版本中,如果使用对应国际服5.58对应的2.2.x.x版本的SDK,其中的FFXIV_ACT_Plugin.Common.dll 版本为2.2.0.5,会在使用国服解析插件时加载出错。
若需要在国服中使用,则需要适应对应国际服5.55对应的2.0.x.x版本的SDK,其中的FFXIV_ACT_Plugin.Common.dll 版本为2.0.4.10。

大部分需要用到的功能都存在于FFXIV_ACT_Plugin.Common.dll中,而这也是解析插件给外部插件提供的接口。你应当只引用这一个DLL,其他的DLL应当用于参考而不是实际使用。在项目依赖项中添加此DLL的引用即可。

实例查找

我们需要首先查找到FFXIV解析插件的实例,才能正确的调用它相关的API。

在上面我们提到了,ACT提供了一个变量,里面存放着所有插件的信息。我们直接遍历这个变量,判断是否为我们需要的插件即可。

抹茶等插件使用的方法是判断插件的文件名,这虽然会有一些问题但是它算是一个能用的方案。你也可以尝试使用反射的方法判断类型是否为指定的类型。

这是查找插件的示例代码,推荐在初始化插件的时候查找:

var plugins = ActGlobals.oFormActMain.ActPlugins;
foreach (var item in plugins)
{
    if (item.pluginFile.Name.ToUpper().Contains("FFXIV_ACT_PLUGIN"))
    {
        var instance = item.pluginObj as FFXIV_ACT_Plugin.FFXIV_ACT_Plugin;
    }
}

使用插件

插件的API文档中写了一些接口的描述,请参考文档了解如何使用。

  • IDataSubscription: 数据监听,用于注册对应的数据事件。
    • NetworkReceived: 网络接收事件,可以用于解析网络包。
    • NetworkSent: 网络发送事件,可以用于解析网络包。
    • 其他事件请参考文档
  • IDataRepository: 数据仓库,获取游戏相关的数据。
    • GetServerTimestamp: 获取最后一次通信时的服务器时间

对于大部分的插件来说,解析插件提供的那些信息是完全不够的。这种使用我们就需要注册NetworkReceived事件,手动解析网络数据包。

FFXIV 网络数据包入门

在这里我们给出三个概念:
– 数据包(Packet): 是一个网络数据包。包括了许多个分包(Segment),并可能对他们做了压缩。
– 分包 (Segment): 是FFXIV网络通信中的一条指令或一段数据,是最小单元。
– 远程调用(IPC): 是分包的一种子类型,是远程调用指令。其Segment Type为3。大部分的FFXIV网络包都属于这个类型。

Sapphire是三方开发者开发的FFXIV服务器,包含了许多有用的信息。这三种数据包的详细信息请参考Sapphire的包格式定义文件。

NetworkReceived和NetworkSent事件给我们提供了分包数据,我们只需要解析它的message参数中提供的数据即可。下面给出了包头可用的数据。

 0               4                 8              12              16
 +---------------+-----------------+---------------+-------+-------+
 | size          |  source_actor   | target_actor  | type  |  pad  | Segment Header
 +-------+-------+------+----------+---------------+-------+-------+
 | 14 00 | type  |  pad | serverId |   timestamp   |      pad1     | IPC Header
 +-------+-------+------+----------+---------------+---------------+
 |                                                                 |
 :                         IPC Data                                :
 |                                                                 |
 +-----------------------------------------------------------------+

这两个包头中,比较有用的数据有:
– Segment Size: 包长度,包括包头数据的长度。
– Segment Type: 包类型。用于判断此包是否为IPC包
– IPC Type: IPC 类型。又被称为Opcode
– IPC serverId: 服务器ID
– IPC timestamp: 服务器时间戳

Opcode用于识别IPC包类型,在解析过程中扮演着非常重要的作用。然而由于引入的混淆机制,各个包的Opcode在每个版本都会被随机打乱。故我们在新版本就需要重新识别各个包,获得他们的Opcode。

附加:如何得到我想要的网络数据

最好的方法是自己抓包分析。我们推荐使用Wireshark搭配FFXIV解析插件抓包并分析。

假设我想要知道我的雇员信息,就可以抓包分析发送的数据。由于雇员信息已经是一个已知数据格式的信息了,我们可以直接参考Sapphire的包定义解析内部数据。此时我们只要知道这个包对应的Opcode就行。

但如果是一个未知格式的数据,你就需要自行辨认数据格式。此时FFXIVMon提供的功能可能能够帮助到你。

悬浮窗

现在大多数人使用的应该都是NGLD OverlayPlugin。这个插件有丰富的扩展系统,方便第三方开发者开发自己的悬浮窗。

悬浮窗编写

参考资料中的悬浮窗编写文档写的非常详细,建议阅读。

悬浮窗主要使用两个API:
– addOverlayListener: 添加对指定数据源的监听。
– callOverlayHandler: 调用指定的处理程序。

一般来说,悬浮窗的主要工作流程就是添加对数据的监听,处理数据后将其显示出来。最简单的悬浮窗代码应该是这样的:

<script type="text/javascript" src="https://act.diemoe.net/overlays/common/common.min.js"></script>
<script>
    // 添加数据处理
    addOverlayListener('CombatData', (data) => {
        console.log(`经历战斗: ${data.title} | ${data.duration} | 团伤: ${data.ENCDPS}`);
    });
    // 注册完毕,启动悬浮窗事件
    startOverlayEvents();
</script>

在你的页面路径后面添加?OVERLAY_WS=ws://127.0.0.1:10501/ws并访问,打开浏览器的控制台即可看到上面打印输出的信息。

悬浮窗扩展

如果要为自己的悬浮窗提供一些数据,一个比较通用的方法是调用ACT的ParseRawLogLine方法,将自己的数据写入到日志中。这样,其他插件就可以通过日志行来得到对应的数据了,悬浮窗也能通过监听LogLine数据来得到需要的数据。

这个方案在只有一些简单数据的时候是非常好用的,但是一旦需要传递比较多数据且有较多交互操作时,这一方案就不太够用了。这时候,就需要扩展悬浮窗插件,增加所需的数据源和对应处理程序了。

为了调用悬浮窗的对应API,你需要下载悬浮窗的插件并添加引用。将插件的libs文件夹下的OverlayPlugin.Common.dllOverlayPlugin.Core.dll加入引用即可。

与ACT插件一样,悬浮窗的插件同样也是通过实现指定的接口来编写的。悬浮窗会自动查找所有实现了RainbowMage.OverlayPlugin.IOverlayAddonV2接口的类,并尝试调用其Init方法。我们就只需要在Init方法中调用相关API注册事件。

using RainbowMage.OverlayPlugin;
public class AddonExample : IOverlayAddonV2
{
    public void Init()
    {
        var container = Registry.GetContainer();
        var registry = container.Resolve<Registry>();

        // 注册事件源
        registry.StartEventSource(new AddonExampleEventSource(container));

        // 注册悬浮窗
        registry.RegisterOverlay<AddonExampleOverlay>();

        // 或者注册悬浮窗预设
        registry.RegisterOverlayPreset2(new AddonExampleOverlayPresent());
    }
}

Registry是悬浮窗插件的注册器,它有以下的几个方法可供使用:

  • StartEventSource: 注册一个事件源。事件源提供了悬浮窗的数据源以及调用的处理,我们需要的大部分功能都在这里。
  • RegisterOverlay: 注册一个自定义悬浮窗。自定义悬浮窗提供了高级的方法更新页面文档,可以精细的控制悬浮窗页面。如果没有特殊需要,不需要使用此方法。
  • RegisterOverlayPreset2: 注册一个悬浮窗预设。悬浮窗预设是在新建悬浮窗的预设中添加一项,其可以自定义名称、页面地址、大小等信息。可以使用此方法为用户添加一项自己插件的预设。
    • IOverlayPreset 是预设的接口,其中
    • Type是悬浮窗类型,一般为MiniParse
    • Supports表示悬浮窗支持的功能。modern 表示是 OverlayPlugin 兼容的悬浮窗,actws 表示是 ACTWS 插件兼容的悬浮窗。

想要自定义数据源,就需要注册一个事件源。事件源需要继承EventSourceBase抽象类。

public class AddonExampleEventSource : EventSourceBase
{
    public AddonExampleEventSource(TinyIoCContainer container) : base(container)
    {
        // 设置事件源名称,必须是唯一的
        Name = "AddonExampleES";

        // 注册数据源名称。此数据源提供给悬浮窗监听
        RegisterEventTypes(new List<string>()
        {
            "onAddonExampleEmbeddedTimerFiredEvent",
        });

        // 注册事件处理程序,提供给悬浮窗调用
        RegisterEventHandler("addonExampleCurrentTime", (msg) => {
            var ret = new JObject();
            ret["time"] = DateTimeOffset.UtcNow.ToString();
            return ret;
        });
    }

    public override Control CreateConfigControl()
    {
        // 创建配置页面,与ACT插件页面的UserControl类似。
        // 如果不想显示页面,返回null
        return null;
    }

    public override void LoadConfig(IPluginConfig config)
    {
        // 载入配置
    }

    public override void SaveConfig(IPluginConfig config)
    {
        // 保存配置
    }

    public void FireEvent()
    {
        // 将数据发送给悬浮窗
        DispatchEvent(JObject.FromObject(new
        {
            type = "onAddonExampleEmbeddedTimerFiredEvent",
            message = "EmbeddedTimer fired!"
        }));
    }
}

程序集引用问题

如果需要调用某个接口,就需要引用它的程序集。然而,直接引入一个程序集会造成一些问题,例如版本接口不匹配的问题,或者是依赖地狱问题。

很多时候我们仅仅只是想要访问某个特定的接口,而不想要关心它的实现。在不引入程序集的情况下,我们还可以使用反射的方式解决这一引用问题。

我写了一个 PluginCommon 的库,用于解决插件开发中的一些常见的问题。它
– 提供了一个FFXIV解析插件的代理类,在不引入程序集的情况下可以直接访问
– 提供了一个EventSourceBase抽象类,在不引入OverlayPlugin.Core的情况下可以实现悬浮窗事件
– 提供了FFXIV网络包的解析器

希望对你有帮助。

参考资料

分类: 未分类

0 条评论

发表回复

Avatar placeholder

您的电子邮箱地址不会被公开。