• IS THIS SITE UGLY? Click "RG3" at the very bottom-left of this page to change it. To dismiss this notice, click the X --->
Resource icon

MQ2DotNet 1.0.2

Warning, long post, and fairly technical, if you're not currently writing, or considering writing, a macro or a plugin you probably aren't interested but feel free to read on anyway 🙂

I've been working on a wrapper to allow the use of .NET/C# within EQ & MQ2, releasing it here in the hope some of you may want to have a play with it and give some feedback. I feel like it will make more sense and be a bit more appealing with some background.

First of all, why bother, why not stick with macros and C++ plugins? Macros are a great starting point, they:


  • Are simple to get started with, not a lot of programming knowledge is required to write a basic one
  • Have near full access to EQ & MQ2 data/functions through TLOs (Top Level Objects, things like Spawn, Me, Item etc) and commands
  • Are fairly safe, you can't crash or lock up the client by running one
  • Run on the main thread, without having to worry about thread safety, but also with the ability to easily delay as required

But they have some limitations, especially once they get larger:


  • Difficult to debug, echos vs a real debugger are night and day
  • Have no common data types and algorithms, e.g. lists, hash tables, sorting, filtering. Seeing a string of a|b|c for a list makes me sad
  • No easy access to external functionality e.g. files, web
  • Inability to run more than one at a time
  • Limited (mostly) to running 80 lines per frame, this results in poor performance in critical parts of macros. There's a big difference between doing say 80 integer comparisons, and 80 ini file writes or 80 nav path calculations, but it has to account for the average case

This is where plugins start to get a look in. They've got a number of advantages over macros:


  • "Real" programming language, with full access to external libraries e.g. STL containers
  • Access to more EQ & MQ2 functionality (note if this is what you're after, this post won't change much for you :))
  • Ability to have more than one doing things at the same time
  • Can run as much as you want per frame, or even in a different thread if you're brave/naive enough ;)

It comes at a cost though:


  • Steeper learning curve. Raw pointers and memory management are hard concepts to get your head around.
  • No safety. Null pointer = game over. Even if you know what you're doing this is still annoying from time to time
  • Implementing long running code is difficult. Your choices are basically between an extra thread, and a state machine that runs from OnPulse. Threads are very difficult to get right, and need synchronization for access to game data. State machines are painful. If anyone is doing some long running code (more than 1 frame) and is set on C++, I highly recommend having a look at what dannuic has done with his MQ2Cast rewrite
  • I realize this is personal opinion, but C++, let alone C, is slow to develop in compared to modern languages. There's a reason people choose other languages, and it's not (at least not in all cases) because we're too dumb to handle C/C++

I think .NET and C# (or VB, but why would you use that?!) can be the best of both worlds. On top of all the conveniences of a modern language, this wrapper offers:


  • Easy access to EQ/MQ2 data with a data model that matches (not coincidentally) the macro types. Including intellisense!
  • Access to MQ2 functions through either executing slash commands, or for more common things, functions (very limited selection right now, more will be added eventually as required)
  • No messing with pointers
  • Safety, all exceptions are caught rather than crashing to desktop. If you do use it and manage to crash, I'll fix it. One caveat to this is infinite loops will still hang :)
  • A simple, straightforward model for executing long running functions from the main thread in a real language

Enough sales pitch, code time!

Programs

Rich (BB code):
using System.Threading.Tasks;
using MQ2DotNet.MQ2API;

namespace MyNamespace
{
    public class MyProgram
    {
        public static async Task Main(string[] args)
        {
            MQ2.WriteChat("Hello " + TLO.Me.Name);
        }
    }
}
I'm not going to give a C# tutorial here, but hopefully, with the possible exception of the "async Task" bit, this isn't too scary (more on this later). It's pretty similar to a standard C# hello world application (https://docs.microsoft.com/en-us/do...side-a-program/hello-world-your-first-program). The Main method is the only thing that matters, the rest is boilerplate (and I'll leave it out of future examples for brevity). To compile it, you'll need to create a new C# class library application, and add a reference to MQ2DotNet.dll.

To run it, put your compiled dll, along with the two attached ones, in your Release directory. You'll first need to bootstrap the .NET runtime, this is easy enough:

Rich (BB code):
/plugin mq2dotnetloader
This is just a plain old plugin, it loads the .NET runtime, and from the it also loads MQ2DotNet which is where the good stuff happens. To run your newly created program, assuming your dll is called MyProgram:

Rich (BB code):
/netrun MyProgram
That's it! If all goes according to plan, you should see "Hello <name>" printed in your chat window. In place of the MQ2.WriteChat function, you could have instead used:

Rich (BB code):
MQ2.DoCommand("/echo Hello " + TLO.Me.Name);
The TLO object provides access to the exact same things that macro variables do. For example:

Rich (BB code):
MQ2.WriteChat("There's " + TLO.SpawnCount["npc radius 500"].ToString() + " NPCs near me");
// or with string interpolation:
MQ2.WriteChat($"There's {TLO.SpawnCount["npc radius 500"]} NPCs near me");
Everything is strongly typed, and you can store things in a variable instead of accessing the whole thing each time:

Rich (BB code):
SpawnType spawn = TLO.NearestSpawn["npc radius 500"];
MQ2.WriteChat($"There's a level {spawn.Level} {spawn.Name} near me");
This last snippet could throw a null reference exception if there are no NPCs near you, but fear not, because that will be caught! If not by you in your own try catch block, MQ2DotNet will clean up your mess and write an error to your chat window instead of crashing to the desktop &#128578;

The args variable contains whatever parameters you passed when you did the /netrun, and you don't even have to use "this GetArg shit":

Rich (BB code):
        public static async Task Main(string[] args)
        {
            foreach (string arg in args)
                MQ2.WriteChat($"Hello {arg}");
        }
But what if we want to delay? For example do something, wait for a result, then do something else? Enter the await keyword, allowed because our method is declared as async:

Rich (BB code):
        public static async Task Main(string[] args)
        {
            MQ2.WriteChat("This executes straight away");
            await Task.Delay(5000); // 5000 millisecond delay
            MQ2.WriteChat("This executes 5 seconds later, still from the main thread!");
        }
This will, in effect, split the method into two parts. Once the compiler's done with it the end result looks something like:

Rich (BB code):
        public static void Main(string[] args)
        {
            MQ2.WriteChat("This executes straight away");
            Magical_CompilerGenerated_Method_That_Tells_OnPulse_To_Call_RestOfMethod_In_5_Seconds();
        }

        public static void RestOfMethod()
        {
            MQ2.WriteChat("This executes 5 seconds later, still from the main thread!");
        }

This of course is entirely transparent to you and you don't need to worry about it. All your variables are still available after the await, so you could do something like:


Rich (BB code):
        public static async Task Main(string[] args)
        {
            var startingExp = TLO.Me.PctExp;
            await Task.Delay(300000);
            MQ2.WriteChat($"I've gained {TLO.Me.PctExp - startingExp} in the last 5 minutes");
        }
You can await a certain amount of time, as in the examples above, or just for the next frame:

Rich (BB code):
        public static async Task Main(string[] args)
        {
            MQ2.WriteChat("This executes straight away");
            await Task.Yield(); // Delay until the next frame
            MQ2.WriteChat("This executes next frame, still from the main thread!");

        }
You can also await another function of your own making, provided it is declared async as well:

Rich (BB code):
        public static async Task Main(string[] args)
        {
            foreach (var spellName in args)
                await CastSpell(spellName);
        }

        public static async Task CastSpell(string spell)
        {
            MQ2.WriteChat($"Casting {spell}");
            MQ2.DoCommand($"/casting \"{spell}\" -maxtries|3");

            while (TLO.Cast.Status == "C" || TLO.Me.SpellInCooldown) // Wait for MQ2Cast + GCD to both finish
                await Task.Yield();

            MQ2.WriteChat($"Cast result: {TLO.Cast.Result}");
        }
This would cast each spell in sequence, e.g.

Rich (BB code):
/netrun myprogram "Claw of Qunard" "Ethereal Skyfire" "Shocking Vortex"
If you want to stop if halfway through, you can:

Rich (BB code):
/netend *
This will stop all programs you have started using /netrun. Did I mention you can run as many as you want at the same time? You can also stop just one with:

Rich (BB code):
/netend myprogram
At this point you might be thinking this is very similar to writing a macro, and that's the intention. If you've come from working in another programming language and found the macro language lacking (basic stuff like proper arrays, lists, for each iterators), give this a try!

A word of warning, if you don't await either in your command, it will never pass control back to EQ to continue running. So something like this is a bad idea:

Rich (BB code):
        public static async Task Main(string[] args)
        {
            MQ2.DoCommand("/casting \"Claw of Qunard\" -maxtries|3");
            while (TLO.Cast.Status == "C" || TLO.Me.SpellInCooldown)
            {
                // This will hang, control never gets passed back to EQ
            }
        }
Visual studio intellisense will show you a warning if you do this, pay attention to it! This includes any async functions of your own creation, eventually something needs to call a function that actually relinquishes control, e.g. Task.Yield or Task.Delay.

Plugins

If you want to write a regular old plugin, not just a "program", you can do that too, just have a class that inherits Plugin:

Rich (BB code):
namespace MyPlugin
{
    public class MyPlugin : Plugin
    {
        public override void InitializePlugin()
        {
            Commands.AddCommand("/stuff", DoStuff);
            Commands.AddAsyncCommand("/slowstuff", DoSlowStuff);
        }
        public override void ShutdownPlugin()
        {
            Commands.RemoveCommand("/slowstuff");
            Commands.RemoveCommand("/stuff");
        }
        private void DoStuff(string[] args)
        {
            // The same as a regular plugin command
        }
        private async Task DoSlowStuff(string[] args)
        {
            // The same as a program you'd run with /netrun
        }
    }
}
This goes in a class library that references MQ2DotNet.dll, same as above. There's no need to have a Main() method in there, just a class that inherits from Plugin. Copy the resulting dll to your Release folder, and load/unload with:

Rich (BB code):
/netplugin myplugin
/netplugin myplugin noauto
/netplugin myplugin unload
/netplugin myplugin unload noauto
Just like you would for a normal plugin. The list of plugins to be loaded automatically is stored in MQ2DotNet.ini.

All the regular plugin callbacks are there, go start typing stuff with visual studio intellisense, it'll sort you out. No support for creation of your own TLOs or MQ2Types from .NET, while possible it's not on my list at the moment.

But C# Is Slower Than C++

Performance on this will be somewhere between a macro and a pure C++ plugin, largely because all accesses through the TLO data type are sent through the MQ2Type API rather than accessing the structs directly (I'm not taking that on). Yes, you'll always be able to make something happen in less CPU cycles using C. No, it's probably not worth it. If you use it and find it slow, let me know and we'll worry about it then. But please don't discard it immediately for performance reasons! Write your code in half the time (and in far fewer lines) and deal with it running at 90% speed, the majority of the time it's a worthwhile trade off.

Something for another time

Both C++ and macros share one common limitation, and that's the difficulty of interrupting running code. A long running subroutine has to check for conditions that require aborting early. Take KISS's CheckBuffs subroutine for example, each iteration it has to call DoWeMove, CheckHealth, RezCheck, and maybe others I didn't spot, just in case anything more important needs doing. Wouldn't it be nice if CheckBuffs could just worry about checking buffs? It'd make it easier to grab and reuse in another macro too, as you wouldn't have to either comment out those calls to other subs, or bring them with you, if you didn't need them. I think there's a nice solution for this too, but that's a post for another day.
Author
Redbot
Downloads
6
First release
Last update
Rating
0.00 star(s) 0 ratings

More resources from Redbot

Top