Skip to content

Add ipc attribute system - #2882

Open
Jaksuhn wants to merge 6 commits into
goatcorp:masterfrom
Jaksuhn:ezipc
Open

Jaksuhn wants to merge 6 commits into
goatcorp:masterfrom
Jaksuhn:ezipc

Conversation

@Jaksuhn

@Jaksuhn Jaksuhn commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Goal of this was to emulate EzIpc from ECommons, but in a better way and native to dalamud so one wouldn't need that dependency. The way that (ec)/this works is providing convenience for ipc handling by offloading most of it onto the lib/dalamud.

This is what a lot of my ipc files look(ed) like normally

class SomePluginIpc
{
    private readonly ICallGateSubscriber<int, bool> _getThing;
    public SomePluginIpc(IDalamudPluginInterface pi) {
        _getThing = pi.GetIpcSubscriber<int, bool>("SomePlugin.GetThing");
    }

    internal bool TryGetThing(int id) {
        if (!_getThing.HasFunction)
            return false;
        return _getThing.InvokeFunc(id);
    }
}

It's a bit verbose (IMO) with the initialisation and due to a c# limitation, having a helper function is almost required since <T1, T2> can't be named on the subscriber. It's also helpful when you want to safely invoke the ipc. This system would reduce the same thing to the following:

class SomePluginIpc {
    [Ipc] public IpcFunc<int, bool> GetThing { get; private set; } = null!;
    internal bool TryGetThing(int id, out bool result) => GetThing.TryInvoke(id, out result);
    // TryInvoke just checks HasFunction/HasAction before calling and outs the result
    // can also just not have the helper function at all ofc, then it's just a one liner
}

class Plugin(IDalamudPluginInterface pi) {
    pi.CreateIpcSubscribers<SomePluginIpc>("SomePlugin");
}

and basically the same for providers

class MyIpcProvider {
    [Ipc] public bool GetThing(int id) => this.Thing != null;
}

class Plugin(IDalamudPluginInterface pi) {
    var providers = pi.CreateIpcProviders<MyIpcProvider>();
}

The names are all reflected unless you want to pass/override them manually. The instances are also all stored in the interface and are auto disposed on plugin unload so one only has to register it unless they need to dispose sometime before the plugin lifecycle ends. This of course means you'd have to follow the same names as the plugin you're subscribing to to utilise the reflection, but if you don't, you can override it, like:

[IpcPrefix("State")]
internal sealed class SomePluginIpc
{
    // SomePlugin.State.Thing1
    [Ipc] public IpcFunc<bool> Thing1 { get; private set; } = null!;

    // SomePlugin.Extra.Thing2
    [Ipc("Extra.Thing2")] public IpcAction Thing2 { get; private set; } = null!;

    // SomePlugin.Extra.Thing3
    [Ipc("Extra.%m")] public IpcFunc<int> Thing3 { get; private set; } = null!;

    // SomePlugin.MyPlugin.Thing4
    [Ipc("%p.Thing4")] public IpcAction Thing4 { get; private set; } = null!;

    // DifferentPrefix.Full.Name
    [Ipc("DifferentPrefix.Full.Name", applyPrefix: false)] public IpcFunc<string> NameNotReflected { get; private set; } = null!;

    // SomePlugin.Custom
    [Ipc("Custom")] public IpcFunc<int> Thing5 { get; private set; } = null!;
}

If you wanna disable the ipcs outside of plugin disposal, you can if you keep track of the IpcRegistrations. Otherwise, you don't need to and can let dalamud call it when it disposes of your plugin normally

private IpcRegistration<MyIpcProvider>? providers;
private IpcRegistration<SomePluginIpc>? subscribers;

class Plugin(IDalamudPluginInterface pi) {
    this.providers = pi.CreateIpcProviders<MyIpcProvider>();
    this.subscribers = pi.CreateIpcSubscribers<SomePluginIpc>("SomePlugin");
}

void DisableIpcs() {
    this.providers?.Dispose();
    this.subscribers?.Dispose();
}

AI disclosure: It generated the docs (I'm not particularly good at those), though I went back and edited some as needed and checked they sounded fine. I also used it for quick making the additional generics, e.g. made IpcAction, it made IpcAction<T1>, IpcAction<T1, T2> ... since that was just copying boilerplate.
Pending a "LGTM" review I can go back and hand write all the docs. I just kind of don't expect much out of this PR.

@Jaksuhn
Jaksuhn requested a review from a team as a code owner July 14, 2026 15:29
@KazWolfe

Copy link
Copy Markdown
Member

Not exactly a fan of the prefix behavior with [Ipc("Something")] becoming MyPlugin.Something versus MyPlugin.State.Something. I'd suggest making applyPrefix: false required for any prefix override, and then allow the user to control that.

I honestly also don't think we need the string substitution here, let's keep it simple and have IPC names be somewhat compile-time constant.

@Jaksuhn

Jaksuhn commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Done on both counts. Now it's required and will throw if not set when you override.

I was originally gonna argue for %m but yeah it's not really needed with IpcPrefix existing (that EC didn't have)

@KazWolfe

KazWolfe commented Aug 24, 2026 •

Copy link
Copy Markdown
Member

Done on both counts. Now it's required and will throw if not set when you override.

To clarify here, Bar and Nya.Baz are invalid?

[IpcPrefix("Commands")]
public class IpcCommands {
  [Ipc] public void DoFoo();         // Resolved: MyPlugin.Commands.DoFoo
  [Ipc("Bar")] public void DoBar();  // Resolved: MyPlugin.Commands.Bar
  
  [Ipc("Nya.Baz")]
  public void DoBaz();               // Resolved: MyPlugin.Commands.Nya.Baz

  [Ipc("Qux", applyPrefix: false)]   
  public void DoQux();               // Resolved: Qux

  [Ipc("MyPlugin.Corge", applyPrefix: false)]
  public void DoCorge();             // Resolved: MyPlugin.Corge
}

If so, I'd suggest making that still valid if possible since it brings in the principle of least surprise.

I was originally gonna argue for %m but yeah it's not really needed with IpcPrefix existing (that EC didn't have)

There's also the following form, which I think explains better:

public class Utilities {
  [Ipc($"{nameof(Utilities)}.Test", applyPrefix: false)]
  public void DoTest();              // Resolved: Utilities.Test

@Jaksuhn

Jaksuhn commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

To clarify here, Bar and Nya.Baz are invalid?

Yes since you don't set applyPrefix and that's what I thought you meant.

I'd suggest making that still valid if possible since it brings in the principle of least surprise.

Sure.

There's also the following form, which I think explains better:

True, I just like the brevity/lack of needing to repeating Utilities.X

@KazWolfe
KazWolfe enabled auto-merge (squash) August 24, 2026 18:11
@KazWolfe

Copy link
Copy Markdown
Member

Pushing to @goaaats for feature approval, I think this API surface looks good as-is.

@KazWolfe
KazWolfe disabled auto-merge August 24, 2026 18:11
@KazWolfe
KazWolfe requested a review from goaaats August 24, 2026 18:12

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants