Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

If requesting help, make sure to mention what game you are attempting to use ACT with.
For the best FFXIV support, join Ravahn's Discord Server. Also check out OverlayPlugin's FFXIV FAQ and Setup Guide.

Best practices for cross-plugin object instance access?

Hello! After pouring through much of the ACT API documentation, I've determined that the data I need (Player data and stats, zone data, party data, etc.) for my plugin is within the FFXIV parser plugin object class. ACT does not seem to fire off an ActGlobals.oFormActMain.OnLogLineRead event for all LogLine types, only "0". This won't work for me, and I want to avoid the overhead of multiple log readers parsing the same log file as it's redundant and terribly inefficient. The FFXIV plugin does, however, have a public DataSubscriptions object that I can attach an event hook to the events I want to grab and circumvent this issue.

I realize I can iterate through the ActGlobals.oFormActMain.ActPlugins list and detect the parser, but trying to cast it from the simple IActPluginV1 interface to the actual class type exposed through the parser plugin assembly is causing a lot of headaches, since there's two separate assemblies (The ACT Assembly.LoadFrom/LoadFile and my referenced assembly type) and the types are not considered equal in that regard, even if sourced from the exact same DLL file, and an attempt to cast results in an InvalidCastException.

(See https://docs.microsoft.com/en-us/dotnet/framework/deployment/best-practices-for-assembly-loading#avoid-loading-an-assembly-into-multiple-contexts for more information/further explanation on what's happening under the hood in this situation.)

What's the best method for gaining access to the class object instance? I'm sure someone has attempted this at some point.

Thanks in advance!

Edit: I want to add in that I do recognize that Ravahn may have seen this problem in advance and thus repeats most of the data via the "0" channel, however since it's made "human-readable", it becomes inefficient to parse that text when the raw parse log data is far preferable. I also expect that his "DataSubscription" class is intended for this purpose as well... but I have no way of reaching out to him to confirm.
Tagged:

Comments

  • {...}
    ACT does not seem to fire off an ActGlobals.oFormActMain.OnLogLineRead event for all LogLine types, only "0".
    {...}
    ACT has no such filtering depending on DetectedType.  My understanding is that Rahvin's plugin does not feed ACT data directly, rather it appends a text log that ACT then parses using whatever engine he's implemented.  In this way, Custom Triggers and re-parsing historical data work correctly.  If the plugin directly called ActGlobals.oFormActMain.AddCombatAction(), the experience would work mostly the same... but there would be no way to get the data back once you closed ACT.  (except for exporting *.act files)

    I'm guessing that they are all type "0" because he didn't choose anything else.  The only real benefit of doing so is that you can change it to a (int)Color.ToArgb() and it will appear that color in certain windows.

    Are you sure that there are log lines in his appended log that do not raise an event in ACT?  You might want to try the BeforeLogLineRead event as well.  The only reason I can see that a line might not fire the event is if another event handler threw an unhandled exception (ACT's error log will say so) or an exception occurred during Custom Trigger parsing when CT threads is set to zero (synchronous).

    {...}

    What's the best method for gaining access to the class object instance? I'm sure someone has attempted this at some point.

    Thanks in advance!

    Edit: I want to add in that I do recognize that Ravahn may have seen this problem in advance and thus repeats most of the data via the "0" channel, however since it's made "human-readable", it becomes inefficient to parse that text when the raw parse log data is far preferable. I also expect that his "DataSubscription" class is intended for this purpose as well... but I have no way of reaching out to him to confirm.
    That's an interesting problem actually.  I'm not sure anyone has ever come to me for advice on such.  I don't play FFXIV so I can't do exactly what you're doing... but it sounds simple enough to simulate based off of your description.
  • edited September 2018
    Just for reference, ACT uses this pseudo-code to load pre-compiled assemblies.  I'm not entirely sure how it differs from Assembly.LoadFrom/LoadFile.  I think I probably wrote it close to 10 years ago, now.
    byte[] asmBytes;
    Assembly asm = AppDomain.CurrentDomain.Load(asmBytes, pdbBytes); pluginData.pluginObj = (IActPluginV1)asm.CreateInstance(asmTypes[i].FullName);
  • Well, I seemingly got it to work... but I don't think that object is as useful as you thought.  Or maybe it is... I'll let you decide...

    // reference:C:\PATH\TO\Advanced Combat Tracker\Plugins\FFXIV_ACT_Plugin.dll
    using Advanced_Combat_Tracker;
    using System.Windows.Forms;
     
    namespace Test_Plugin
    {
    	public partial class TestPlugin: IActPluginV1
    	{
    		public TestPlugin()
    		{
    		}
     
    		Label lblStatus;
    		TabPage tpPlugin;
    		FFXIV_ACT_Plugin.FFXIV_ACT_Plugin ffxivPlugin;
    		public void InitPlugin(TabPage pluginScreenSpace, Label pluginStatusText)
    		{
    			tpPlugin = pluginScreenSpace;
    			lblStatus = pluginStatusText;
    			lblStatus.Text = "Plugin Started";
     
    			foreach(ActPluginData pluginData in ActGlobals.oFormActMain.ActPlugins)
    			{
    				if(pluginData != null && pluginData.pluginObj != null && pluginData.pluginObj.GetType() == typeof(FFXIV_ACT_Plugin.FFXIV_ACT_Plugin))
    				{
    					ffxivPlugin = (FFXIV_ACT_Plugin.FFXIV_ACT_Plugin)pluginData.pluginObj;
    				}
    			}
    			lblStatus.Text = "Got plugin instance...";
     
    			ffxivPlugin.DataSubscriptions.ToString();
    		}
     
    		public void DeInitPlugin()
    		{
    			lblStatus.Text = "Plugin Exited";
    		}
     
    	}
    }



    As you can see, everything in that public property is uninitialized.  But they're not events either, so I'm not entirely sure what you were planning on doing if they were hooked up.

  • EQAditu said:
    Just for reference, ACT uses this pseudo-code to load pre-compiled assemblies.  I'm not entirely sure how it differs from Assembly.LoadFrom/LoadFile.  I think I probably wrote it close to 10 years ago, now.
    byte[] asmBytes;
    Assembly asm = AppDomain.CurrentDomain.Load(asmBytes, pdbBytes); pluginData.pluginObj = (IActPluginV1)asm.CreateInstance(asmTypes[i].FullName);

    Ahh, this explains why the exception stated, "Type A originates from 'FFXIV_ACT_Plugin, Version=1.7.0.13, Culture=neutral, PublicKeyToken=null' in the context 'LoadNeither' in a byte array."

    I will admit I was quite puzzled why it was inside of a byte array, but seeing your snippet explains a lot.

    I would say if you ever fancy a refresh on the plugin code, I tend to agree with the best practices doc I previously linked, where it suggests the Add-In model that's provided by the Framework. (https://docs.microsoft.com/en-us/dotnet/framework/deployment/best-practices-for-assembly-loading#consider-using-the-net-framework-add-in-model), which takes away a lot of the busywork of managing assembly domains, security contexts, etc.

  • EQAditu said:
    {...}
    ACT does not seem to fire off an ActGlobals.oFormActMain.OnLogLineRead event for all LogLine types, only "0".
    {...}
    ACT has no such filtering depending on DetectedType.  My understanding is that Rahvin's plugin does not feed ACT data directly, rather it appends a text log that ACT then parses using whatever engine he's implemented.  In this way, Custom Triggers and re-parsing historical data work correctly.  If the plugin directly called ActGlobals.oFormActMain.AddCombatAction(), the experience would work mostly the same... but there would be no way to get the data back once you closed ACT.  (except for exporting *.act files)

    I'm guessing that they are all type "0" because he didn't choose anything else.  The only real benefit of doing so is that you can change it to a (int)Color.ToArgb() and it will appear that color in certain windows.

    Are you sure that there are log lines in his appended log that do not raise an event in ACT?  You might want to try the BeforeLogLineRead event as well.  The only reason I can see that a line might not fire the event is if another event handler threw an unhandled exception (ACT's error log will say so) or an exception occurred during Custom Trigger parsing when CT threads is set to zero (synchronous).

    {...}

    What's the best method for gaining access to the class object instance? I'm sure someone has attempted this at some point.

    Thanks in advance!

    Edit: I want to add in that I do recognize that Ravahn may have seen this problem in advance and thus repeats most of the data via the "0" channel, however since it's made "human-readable", it becomes inefficient to parse that text when the raw parse log data is far preferable. I also expect that his "DataSubscription" class is intended for this purpose as well... but I have no way of reaching out to him to confirm.
    That's an interesting problem actually.  I'm not sure anyone has ever come to me for advice on such.  I don't play FFXIV so I can't do exactly what you're doing... but it sounds simple enough to simulate based off of your description.

    Also very interesting. I have only tiptoed into Ravahn's plug-in (by a subtle suggestion by him to disassemble his code, as he does not publish the source), and haven't quite pieced all the parts together.

    Ultimately, to explain as briefly as possible, I'm trying to gain access to the memory-based dataset that he grabs from the running binary based off of known address offsets. This is where I would be able to get close to realtime player stats such as hit points, magic points, job, level, experience, etc. as it doesn't seem to come consistently through the log parsing method sourced from the network buffer that he also reads from. His plugin internally seems to keep a merged set of data within classes that I would love to get at to be able to read from.

    What I really want to do is expose a full-set of game data in a common data format that external applications can hook to and get access to all the FFXIV data without needing to know anything about ACT, the XIV plugin, or the way XIV organizes it's data. This was inspired by trying to stream via OBS, and wanting real-time character stats available that aren't always visible or legible through the game screen capture.... in addition to all the combat stats that ACT produces. I thought if I could harness all that data and pipe it to an OBS plugin that's waiting for it, it would enable streamers to do some really cool things. But it seems that if I want more than combat stats, I'm going to have to hook up with the parsing add-ons to get the rest.

    BTW, thanks for the quick responses!
  • {...}
    Ahh, this explains why the exception stated, "Type A originates from 'FFXIV_ACT_Plugin, Version=1.7.0.13, Culture=neutral, PublicKeyToken=null' in the context 'LoadNeither' in a byte array."

    I will admit I was quite puzzled why it was inside of a byte array, but seeing your snippet explains a lot.
    {...}
    I think the reason for this was so that I could manage my own file handles on the plugins... or maybe it had something to do with loading PDB files.  Or possibly so that I could use the same code base for ACT compiled plugins and pre-compiled plugins.

    I already discovered that loading the assemblies in different domains was a pretty bad performance hit at the time and gave up on the idea of security for the sake of security.  What was I securing?  ACT's internal memory that I was already giving out on purpose?
  • edited September 2018
    EQAditu said:
    Well, I seemingly got it to work... but I don't think that object is as useful as you thought.  Or maybe it is... I'll let you decide...

    // reference:C:\PATH\TO\Advanced Combat Tracker\Plugins\FFXIV_ACT_Plugin.dll
    using Advanced_Combat_Tracker;
    using System.Windows.Forms;
     
    namespace Test_Plugin
    {
    	public partial class TestPlugin: IActPluginV1
    	{
    		public TestPlugin()
    		{
    		}
     
    		Label lblStatus;
    		TabPage tpPlugin;
    		FFXIV_ACT_Plugin.FFXIV_ACT_Plugin ffxivPlugin;
    		public void InitPlugin(TabPage pluginScreenSpace, Label pluginStatusText)
    		{
    			tpPlugin = pluginScreenSpace;
    			lblStatus = pluginStatusText;
    			lblStatus.Text = "Plugin Started";
     
    			foreach(ActPluginData pluginData in ActGlobals.oFormActMain.ActPlugins)
    			{
    				if(pluginData != null && pluginData.pluginObj != null && pluginData.pluginObj.GetType() == typeof(FFXIV_ACT_Plugin.FFXIV_ACT_Plugin))
    				{
    					ffxivPlugin = (FFXIV_ACT_Plugin.FFXIV_ACT_Plugin)pluginData.pluginObj;
    				}
    			}
    			lblStatus.Text = "Got plugin instance...";
     
    			ffxivPlugin.DataSubscriptions.ToString();
    		}
     
    		public void DeInitPlugin()
    		{
    			lblStatus.Text = "Plugin Exited";
    		}
     
    	}
    }



    As you can see, everything in that public property is uninitialized.  But they're not events either, so I'm not entirely sure what you were planning on doing if they were hooked up.


    They're all delegate functions, i.e.

    public DataSubscriptions.ZoneChangedDelegate ZoneChanged;

    ...and he calls them whenever an event happens internally...

    internal void OnZoneChanged(int ZoneID, string ZoneName)
    {
    this.ZoneChanged?.BeginInvoke(ZoneID, ZoneName, new AsyncCallback(this.DelegateCallback), (object) null);
    }

    So I think it needs to be subscribed to first. For all I know it's an unfinished feature.

    Interesting though, my code isn't so different than yours. Did you add a direct reference to the XIV DLL in the add-on project? I notice you're using the comment-reference technique you described in the coding tips section here, although not sure how that differs, especially at compile-time.

    The rough code I was working with:

  • edited September 2018
    I wrote it so that ACT would load it as a *.cs file, just as you described.  But yes, I did also add it as a reference in Visual Studio so intellisense would have some clue as to what I was doing.  I get the same result if I load it as a DLL or *.cs in ACT.

    Just as a comparison for my confusion... ACT's events look like the following:
    public delegate void LogLineEventDelegate(bool isImport, LogLineEventArgs logInfo);
    public event LogLineEventDelegate OnLogLineRead; OnLogLineRead += new LogLineEventDelegate(FormActMain_OnLogLineRead);
    The objects in that property never use the "event" keyword, so it didn't look like the same sort of thing to me.
    public delegate void NetworkReceivedDelegate(string connection, long epoch, byte[] message);
    public /* no event keyword */ FFXIV_ACT_Plugin.Memory.DataSubscriptions.NetworkReceivedDelegate NetworkReceived;
  • edited September 2018
    EQAditu said:
    I wrote it so that ACT would load it as a *.cs file, just as you described.  But yes, I did also add it as a reference in Visual Studio so intellisense would have some clue as to what I was doing.  I get the same result if I load it as a DLL or *.cs in ACT.

    Just as a comparison for my confusion... ACT's events look like the following:
    public delegate void LogLineEventDelegate(bool isImport, LogLineEventArgs logInfo);
    public event LogLineEventDelegate OnLogLineRead; OnLogLineRead += new LogLineEventDelegate(FormActMain_OnLogLineRead);
    The objects in that property never use the "event" keyword, so it didn't look like the same sort of thing to me.

    public delegate void NetworkReceivedDelegate(string connection, long epoch, byte[] message);
    public /* no event keyword */ FFXIV_ACT_Plugin.Memory.DataSubscriptions.NetworkReceivedDelegate NetworkReceived;
    // ???



    I'm guessing he did it this way to be cross-thread safe. Also using BeginInvoke/EndInvoke for asynchronous, non-blocking operation (in case someone wrote really bad code?).


    So I'm guessing the way to utilize his setup would be something similar to...

    FFXIV_ACT_Plugin.FFXIV_ACT_Plugin.DataSubscriptions.ZoneChanged = new FFXIV_ACT_Plugin.FFXIV_ACT_Plugin.Memory.DataSubscriptions.ZoneChangedDelegate(MyZoneChanged);

    public void MyZoneChanged(int ZoneID, string ZoneName)
    {
    ...
    }

    or at least that's the start of it. Getting a bit late here so I might have botched that up.

    I'll keep tinkering around with it and see if I can't get past that exception and play around with it. If I get it working I'll let you know!


  • BTW, the *.cs method has the advantage of using whatever version of the FFXIV plugin happens to be there during compile time. (Every time ACT starts)  Otherwise you have to do assembly binding redirects for versions you did not compile for.

    The disadvantage is that you have to somehow predict where the file is... which is sort of not possible in most ACT installations.  I watched the sad list of paths that ACT tried searching in to find it when I didn't specify a full path.  While it had some good guesses, none of them are where the Startup Wizard puts it.
  • So far I haven't been able to get past my InvalidCastException problem.

    You said you were able to compile to a DLL and it loaded your test code fine?
  • I've also noticed the load order seems to affect quite a bit, and there's no ability to re-order the load sequence of plugins unless you remove all of your plugins and re-add them in a specific order.
  • It doesn't seem to behave any differently.  The only difference I see between us is that I don't use any LINQ or any new C# constructs because the compiler available to ACT doesn't understand a lot of new stuff.



  • The only thing I have noticed, that I don't quite understand, is that the plugin is very unhappy if the FFXIV plugin has not been loaded.


    It makes it seem that my plugin is not storing its own copy of the assembly metadata and relies on the referenced assembly being in memory to understand its own references.

    Where as your plugin is storing concrete metadata about the referenced assembly and then complaining when it finds conflicting/duplicate metadata in memory.


  • EQAditu said:
    The only thing I have noticed, that I don't quite understand, is that the plugin is very unhappy if the FFXIV plugin has not been loaded.


    It makes it seem that my plugin is not storing its own copy of the assembly metadata and relies on the referenced assembly being in memory to understand its own references.

    Where as your plugin is storing concrete metadata about the referenced assembly and then complaining when it finds conflicting/duplicate metadata in memory.


    I think we're figuring out the difference. If I swap the load order, I get the same problem as above.

    Also, breaking into the debugger and inspecting the two object types, I see one is part of the ACT executable assembly, whereas the other is part of my add-on .DLL, which is why I'm probably getting the assembly base conflict that results in the Invalid Cast Exception, since .NET refuses to acknowledge two types are equal when they reside in different assembly domains. Otherwise both types are absolutely identical, even down to the GUID.
  • edited September 2018
    I hate to say it, but I think I may be out of luck in this particular use-case.

    Since the plugin loading wasn't done with cross-plugin communication in-mind, and since the FFXIV plugin itself doesn't lend it's data out to anyone but ACT, unless you're willing to forfeit performance/reliability/data-halflife, I'm starting to think it's not feasible to attempt to gain game data access via ACT.

    I thought of writing a parser that wrapped the FFXIV DLL, but that's asking for trouble.

    I may have to resign myself to directly accessing the network buffer and memory blocks of FFXIV directly, alongside the FFXIV plugin. Ravahn does have another project he wrote that does have published source for reading the network data of FFXIV, and I can see the FFXIV plugin memory access code from the disassembly output. This gives me a head-start. And I can always use ACT's plugin interface for the actual combat performance statistics, which I do want.

    This all being said, if you ever want a hand updating the plugin framework code, I'd certainly be willing to jump in. I think the current system would co-exist with a newer solution like the Add-In Framework. In fact, I believe all a plugin developer would need to do is attribute their main class that implements the IActPluginV1 interface to swap to that system. Most of the work is on the host side, and even then it's not much... and you gain a huge amount of reliability and flexibility.
  • It's too bad that you're considering giving up, since I actually understood what you were trying to do and it sounded interesting.

    The only reason my plugin method had those drawbacks is that my init method had explicit references to the FFXIV plugin which forced it to immediately resolve the references.  If I did matching verses a string representation of the type and tried to cast the plugin instance inside of a different method(or class) that I only called when I was 100% sure it was safe, it would have worked better.

    Similarly, I'm not exactly sure what you're doing in your plugin to make it so much more incompatible than my plugin, but you might want to consider going with my method of directly referencing the FFXIV plugin instead of duplicating its structure in your own plugin and trying to cast theirs into yours.

    As long as you keep your references separate from the plugin initialization, my method should go much smoother.  The only issue you run into after that is when the FFXIV plugin version changes and you need to explore assembly binding redirects.  Or at worst case, update your plugin when Rahvin updates.
Sign In or Register to comment.