Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NamePlateGui #1915

Merged
merged 9 commits into from
Jul 20, 2024
Merged

Add NamePlateGui #1915

merged 9 commits into from
Jul 20, 2024

Conversation

nebel
Copy link
Contributor

@nebel nebel commented Jul 10, 2024

image

Introduction

First of all, huge thanks to @Pilzinsel64 for his work on the SetPlayerNamePlate-based API which paved the way for this Nameplate API iteration. Unfortunately, changes to the base game caused a lot of problems for everyone and forced us to take a different approach. On the plus side, this new arraydata-based approach allows us to support all nameplate types, not just player nameplates. Thanks also to @aers for your support and for pushing me to create this, and to everyone else who helped and provided feedback.

At its core this API uses addon lifecycle hooks to run before the NamePlate addon's OnRequestedUpdate function and modify the backing data before plates are rendered. It provides two events which users can subscribe to:

  • OnNamePlateUpdate, which fires only when a nameplate has important updates (as determined by the base game) and passes to the consumer only those nameplates which are updated. This should be the preferred event to use in most cases.
  • OnDataUpdate, which fires when there are any nameplate data changes at all, and passes to the consumer a list of all active nameplates. This should be avoided unless you are making changes which would otherwise be overwritten by the game every frame.

Usage

Example usage for changing parts of a nameplate with OnNamePlateUpdate (used for the image at the top):

Service.NamePlateGui.OnNamePlateUpdate += (context, handlers) => {
    foreach (var handler in handlers) {
        if (handler.NamePlateKind == NamePlateKind.PlayerCharacter) {
            handler.FreeCompanyTagParts.TextWrap = (new SeString(new UIForegroundPayload(43)), new SeString(UIForegroundPayload.UIForegroundOff));
            handler.FreeCompanyTagParts.Text = "Hello";
            handler.TitleParts.TextWrap = (new SeString(new UIForegroundPayload(16)), new SeString(UIForegroundPayload.UIForegroundOff));
            handler.TitleParts.Text = "Plate";
            handler.IsPrefixTitle = true;
            handler.DisplayTitle = true;
            handler.NameParts.Text = "Anonymous Player";
            handler.NameParts.TextWrap = (new SeString(new UIForegroundPayload(37)), new SeString(UIForegroundPayload.UIForegroundOff));
        }
    }
};

The above example uses "Parts" fields, which can be used to allow multiple plugins to collaboratively build a field by its parts. If your plugin modifies or adds colours to one of these fields, consider using these "Parts" fields so that your plugin's modifications have a chance of working alongside those of another plugin. Simple getters/setters which don't use the parts system are also available.

Example usage for debugging nameplate kinds and updates with OnDataUpdate:

Service.NamePlateGui.OnDataUpdate += (context, handlers) =>
{
    foreach (var handler in handlers) {
        if (handler.IsUpdating || context.IsFullUpdate) {
            handler.MarkerIconId = 66181 + (int)handler.NamePlateKind;
        }
        else {
            handler.MarkerIconId = 66161 + (int)handler.NamePlateKind;
        }
    }
};

The above attaches an icon to each nameplate indicating its NamePlateKind, and flashes that icon whenever the plate is updated (i.e. when it would be considered "updated" for the purpose of OnNamePlateUpdate). This uses the data update event which runs every frame because marker icons are set and cleared each frame.

More detailed documentation on what changes are possible is visible on methods of INamePlateUpdateHandler.

Other than these events, the only other public method is RequestRedraw(), which requests the game to redraw all nameplates on the next frame. Plugins can call this method to force a redraw of all nameplates when settings change, the plugin is loaded, etc. even while using the more efficient OnNamePlateUpdate event, because calling this method will cause all plates to be considered updated on the next frame.

Notes

In an earlier design I had more events related to nameplate drawing, which are mainly needed by plugins which do additional changes to native UI nodes and so on. However, the needs of such plugins are much more complicated and hard to generalise, and in the end it felt like a lot of unnecessary complexity. Such plugins should probably use PostRequestedUpdate or PreDraw lifecycle hooks to carry out their additional custom node logic there.

One minor downside of the arraydata-based approach is that modifying arraydata will typically cause the NamePlate addon's OnRequestedUpdate function to fire every frame. However, this function will already fire every frame if the camera or any on-screen entity with a nameplate is moving, so this only affects situations where load is very low in the first place (e.g. idling in the inn). The service itself takes care not to perform unnecessary processing whenever possible, so I think the overall impact should be small even when subscribers exist, and virtually non-existent when no subscribers exist.

Any advice or feedback is welcome. I have ported the new version of Party Icons to use this service and things are working well for me, but I would especially appreciate feedback from developers of other plugins with existing nameplate functionality.

@nebel nebel requested a review from a team as a code owner July 10, 2024 08:37
@WesselKuipers
Copy link
Contributor

Nice work!

With this setup, is it possible to apply changes like colours to also include the quotes (like «») of the part without unsetting the quotes and setting it in the text part of it?

What about setting visibility of the entire nameplate? I see the "VisibilityFlags" part in NamePlateUpdateHandler.cs, but I'm not sure if this is the way to go about messing with these.

@nebel
Copy link
Contributor Author

nebel commented Jul 10, 2024

With this setup, is it possible to apply changes like colours to also include the quotes (like «») of the part without unsetting the quotes and setting it in the text part of it?

Right now this isn't supported as a distinct "part" property, but it can be done by putting color payloads in the quote characters, e.g. via

handler.TitleParts.LeftQuote = new SeString().Append(new UIForegroundPayload(16)).Append("《");
handler.TitleParts.RightQuote = new SeString().Append("》").Append(UIForegroundPayload.UIForegroundOff);

We could consider adding LeftQuoteWrap and RightQuoteWrap but I'm not sure how many plugins would actually use it to the point that it's an improvement over the above. Let me know what you think.

What about setting visibility of the entire nameplate? I see the "VisibilityFlags" part in NamePlateUpdateHandler.cs, but I'm not sure if this is the way to go about messing with these.

You can hide a nameplate entirely by setting the 0x1 bit on VisibilityFlags to zero. However this is set and reset by the game on every frame, so you'd need to use OnDataUpdate to do it this way. Alternatively you can use OnNamePlateUpdate and the Remove* methods to blank out particular parts of plates. A player nameplate with all blanked fields, for example, typically has its UI nodes hidden and has no collision from my testing (although I didn't test other nameplate types for this).

@Pilzinsel64
Copy link
Contributor

Pilzinsel64 commented Jul 10, 2024

Looks good!

I'm about to migrate Player Tags at the moment. Already spend over three hours for refactoring just now and finally got it compiled against this PR. But I didn't have time for testing in-game yet, need to do that later this day or maybe tomorrow. Will leave a comment here whenever I finished my tests with Player Tags.

but I'm not sure how many plugins would actually use it

I would use it for Player Tags as the user can decide to apply color to the whole nameplate. Leaving that parts uncolored might look weird and I would need even more complex code just for including the quotes. But we will see. I think this is ok for now. :)

@nebel
Copy link
Contributor Author

nebel commented Jul 11, 2024

After discussing with several developers of plugins which modify nameplates, it seems there was a desire to add a "wrap" part to title and free company tag fields outside the quotes in addition to inside the quotes, as some plugins want to color the entire part.

Based on that I added an OuterWrap property to TitleParts and FreeCompanyTagParts. It works like TextWrap but wraps the entire field instead of wrapping only the text inside the quotes.

A more extreme version of the initial example allows the following:

image

handler.TitleParts.OuterWrap = (new SeString(new UIForegroundPayload(522)), new SeString(UIForegroundPayload.UIForegroundOff));
handler.FreeCompanyTagParts.OuterWrap = (new SeString(new UIForegroundPayload(710)), new SeString(UIForegroundPayload.UIForegroundOff));

handler.TitleParts.LeftQuote = "[";
handler.TitleParts.RightQuote = "]";
handler.FreeCompanyTagParts.LeftQuote = " (";
handler.FreeCompanyTagParts.RightQuote = ")";

handler.FreeCompanyTagParts.TextWrap = (new SeString(new UIForegroundPayload(43)), new SeString(UIForegroundPayload.UIForegroundOff));
handler.FreeCompanyTagParts.Text = "Hello";
handler.TitleParts.TextWrap = (new SeString(new UIForegroundPayload(16)), new SeString(UIForegroundPayload.UIForegroundOff));
handler.TitleParts.Text = "Plate";
handler.IsPrefixTitle = true;
handler.DisplayTitle = true;
handler.NameParts.Text = "Anonymous Player";
handler.NameParts.TextWrap = (new SeString(new UIForegroundPayload(37)), new SeString(UIForegroundPayload.UIForegroundOff));

@Pilzinsel64
Copy link
Contributor

Finished migration and testing of Player Tags. Works fine on my end.
grafik

@WesselKuipers
Copy link
Contributor

I'm trying to figure out how to hide nameplates programmatically and it seems like if you call all the removeX() functions it does work, but there isn't one for the icons so you end up getting this:
image

@kalilistic
Copy link
Contributor

Finished migration and testing for PlayerTrack. No issues so far for my use.

@WesselKuipers
Copy link
Contributor

Otherwise I fully migrated FC Name Color as well and everything works pretty much perfectly!

@nebel
Copy link
Contributor Author

nebel commented Jul 13, 2024

I'm trying to figure out how to hide nameplates programmatically and it seems like if you call all the removeX() functions it does work, but there isn't one for the icons so you end up getting this:

You should be able to hide that icon by setting handler.NameIconId to -1.

Pilzinsel64 added a commit to Pilzinsel64/Pilz.Dalamud that referenced this pull request Jul 17, 2024
@nebel nebel force-pushed the array-nameplate-api branch from d383a03 to c0968a6 Compare July 18, 2024 07:24
Copy link
Member

@goaaats goaaats left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Some notes, nothing major though - I would appreciate if you could add a self-test for this to Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps that does some basic nameplate modifications. Ideally you would test if they reflect in the "rendered data" but if that is super involved then don't bother.

/// <summary>
/// An empty null-terminated string pointer allocated in unmanaged memory, used to tag removed fields.
/// </summary>
internal static readonly nint EmptyStringPointer = CreateEmptyStringPointer();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will leak, is that intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's intentional. This will live for the scope of Dalamud and I think it's worth a byte basically. If we freed this we could potentially end up with bad data in the string array.

/// <summary>
/// Gets the flags for this nameplate according to the nameplate info object.
/// </summary>
int Flags { get; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you know what these mean? Does it make sense to expose the raw bitset like this as an int, without describing the actual value? (same for the other flags exposed like this)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a little conflicted on this. The issue is that I believe they aren't fully understood, so I found it hard to have a "proper" enum here. But I also think it's possible to do some things like maybe find the title number from here for example (maybe? I kind of forget myself) and maybe it could be improved over time as we learn more if we let people poke in these flag fields. Other flags can do useful things but it's often the case that I can only explain maybe one or two flags and others are unknown.

We could hide them and just assume people who need them will cast the address to a CS struct though. But it becomes a bit harder to use then. So not sure what is best here.

/// <inheritdoc/>
public void RequestRedraw()
{
this.parentService.RequestRedraw();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it matter if multiple plugins call this very frequently/multiple times in the same frame? I assume some game code reads that flag set in the number array and re-renders the nameplates?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. The game reads the flag we set which tells it to do a full update, and we poke the array data to cause an update on the next frame. Basically it's not a problem to call every frame if you're dragging a slider around in a settings window or something, and multiple plugins can call it in the same frame without any issue. But if a plugin decided to call it every frame during gameplay as a matter of course that would not be good for performance.

@nebel
Copy link
Contributor Author

nebel commented Jul 20, 2024

Looks good! Some notes, nothing major though - I would appreciate if you could add a self-test for this to Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps that does some basic nameplate modifications. Ideally you would test if they reflect in the "rendered data" but if that is super involved then don't bother.

Let me try to figure something out here. We could read the rendered data from nameplate nodes but it would require an additional post-hook and some probing of Atk nodes which might end up kind of ugly (the nice part about the current way is we don't have to do that). I'll at least try the first part and see what I can do with the second.

@goaaats goaaats merged commit 8ca4738 into goatcorp:master Jul 20, 2024
3 checks passed
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.

5 participants