• You've discovered RedGuides, an EverQuest multi-boxing and scripting community 🧙‍♀️⚙️. We want you to play several EQ characters at once, come join us and say hello! 👋

  • A TLP without truebox has thawed (Very Vanilla ready)
    Frostreaver
Resource icon

Release .NET / C# Plugin and "Program" API

Joined
Jan 13, 2018
RedCents
3,352¢
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.
 

Attachments

Last edited:
Fantastic idea! As an old C programmer I too prefer working on C# than C++ :)

With this plugin we get full access to TLOs right?

All MQ commands can be accessed through MQ2.DoCommand or are there any exceptions?

Is there anything missing that regular MQ2 macros have right now?
 
I like this concept. Haven't written C# in a bit but may get back to it for something like this.

Do you have this up in github?

Not up on git right now, it'll happen but not just yet.


RE: Full access to TLOs, yep almost everything in core is exposed. Some indexed members aren't done properly, this is on the list, along with xmldoc comments for everything while I go through it (takes a while).

Functionality yeah DoCommand should have you covered. It's hacky and apparently a bit slow, so I'm working on a better way.

Regular macros have events & binds, which I don't. Events should be easy enough, I'll add an event so you can do regular events like, something like

Rich (BB code):
MQ2.OnChat += (s, e) => { Console.WriteLine(e.Text); }

Binds I don't fully understand, but I'm pretty sure by being able to add Commands you should be able to do the same sort of thing. If you can't I guess I could learn what they are, but one thing at a time :)
 
This is awesome, but I have a quick question. Is there a way to import TLOs outside of the built-in TLOs? I'm particularly interested in accessing the MQ2NetBots methods if it's possible. If not, no big deal but thought I'd ask!
 
Any way to access missing TLOs ?

Rich (BB code):
MQ2.WriteChat("Hello " + TLO.Me.Name);
MQ2.WriteChat($"I am {TLO.Me.Name} in {TLO.EverQuest.Server}  and {TLO.Me.X}, {TLO.Me.Y} in {TLO.Zone} "); // missing {TLO.Zone.ID}");

These two lines are acting bizarrely:
First one gives me characters name as expected.

Second gives me server name twice! That is second TLO.Me.Name is also giving me server name just like TLO.EverQuest.Server .

Then it seems TLO.Me.Zone is not implemented at least TLO.Me.Zone.ID is not.
Any way to work around that?

Finally, how would one integrate this with a plugin like MQ2AdvPath?

My macros use a ton of AdvPath

common pattern is
Rich (BB code):
/play mypath
/call Wait4Play
/dostuffatdestination

Sub Wait4Play

	:PlayLoop
	/if (${AdvPath.Playing}) {
		/delay 1
		/goto :PlayLoop
	}
	
/return

How would I convert the loop to Await style?

We need to start creating a documentation wiki for MQ2NetPlugin :)

EDIT:
For the AdvPath this is what I have but I am stuck on how to check for ${AdvPath.Playing}
Rich (BB code):
MQ2.DoCommand("/play tpath");
await Task.Delay(2000);
//I would like to loop here but how do I check for value of ${AdvPath.Playing} ?
MQ2.DoCommand("/echo ${AdvPath.Playing}"); //This writes to MQ2 console as expected
MQ2.WriteChat("Finished");
 
Extra TLOs, for now you will have to parse manually:

Rich (BB code):
string tankName = MQ2.Parse("${NetBots[tank].Name}");
bool playing = MQ2.If("${AdvPath.Playing}");

The server thing is weird, I'll look into it. For zone, I think there's a TLO.CurrentZone, though if Me.Zone is a macro thing that should work too. In macro land Zone returns a Current zone type with no index, and otherwise a zone type. This doesn't translate well since they don't inherit, so CurrentZone is an addition to get around this.
You can loop on an if e.g.

Rich (BB code):
while (MQ2.If("${AdvPath.Playing}") await Task.Delay(10);

The ability to add plugin TLOs is something I want to add, but needs some thought and probably some overall changes to the variable handling. Right now it caches types, so if the plugin that supplies the type is unloaded, it'll crash. There's a similar bug with holding references to types that are stored internally as a pointer to an eq object, e.g.

Rich (BB code):
var spawn = TLO.NearestSpawn["npc"];
await Task.Delay(60000); // kill nearest npc, so eq removes that spawn
var level = spawn.Level; // crash

Solution here is for the spawn to invalidate itself when eq calls the destructor on the underlying type, and throw a managed exception on access. Similar story for plugin types, need to track unloads and invalidate objects accordingly.

It'll happen, just not sure when yet. In the meantime, use Parse (it's atomic wrt plugin loading) and be careful if storing vars across an await boundary!
 
jackstraw01 source is now available here: https://gitlab.com/alynel/MQ2DotNet

I've made an example project that shows how to add another data type.

Also another example one (Toolbox) to remove/block spells by SPA number as requested by kaen01.

playj the server name twice issue is a more general problem for strings. MQ2 copies the string into a global var, e.g. https://github.com/macroquest/macro...20684f07a142fc/MQ2Main/MQ2DataTypes.cpp#L2523 and this gets used by anything that returns a string. Since I'm storing a pointer to this and lazy evaluating it, all strings will return the most recent string when converted. Solution is to not lazy evaluate, and I've done this in the latest commit. TLDR, I fixed it.

I've also added xmldoc on pretty much everything, including lots of stuff that wasn't on the MQ2 wiki.

New version attached to the original post.
 
MQ2DotNet has been removed from Very Vanilla. If there's demand we can get it working again.

Here's the issue stopping our build,
Code:
mq2main\mq2datatypes.h(4907,0): Error C2039: 'DZName': is not a member of 'EQData::_DYNAMICZONE'
mq2main\mq2datatypes.h(4907,0): Error C2660: 'strcpy_s': function does not take 2 arguments

If you know of a solution please let me know!
 
Last edited:
This is pretty awesome. I've been waiting for the day that we could write Macros in .NET. I have been messing around with this and was able to get it working with the 1/9 TEST build locally. Hopefully this will gain more traction and will eventually be distributed with Very Vanilla again.
 
I just saw this from the EQ software rewards... I'm SUPER interested in using this...
Then I scrolled through the comments after reading the overview and see Redbot's comments... FeelsBadMan


@alynel is this still in active dev? I would love to pair up with you on using it / providing you feedback to make it better as I implement more things with it.
 
HERE is the gitlab for it

Yea, I was able to dig through this and get it working... appears the MQ2DotNetLoader and MQ2DotNet from this post/docs aren't compatible with the gitlabs....
I got it where it will see /netrun, but it can't find anything I use... literally copy and pasted the testprogram from the gitlab samples...

It has a crap ton of potential, just sad I didn't discover this until it hit unmaintained status :(
 
I was able to figure out how to get something basic working...

The key was program name was being hardcoded as the expected.dll name as well... So if you had like a single .dll as your library of many programs that won't really work.
I was trying to do something like MQ2Tone.dll -> which would have multiple programs, and then do /netrun program1, but the loader expects program1 to be inside program1.dll.
I assumed it would load the assembly, and use reflection to see the classes that implement IProgram and load those... I was wrong.

Key to this working for anyone else trying...
Code:
                // First look for it in its own folder
                var programFilePath = $"{_mq2DirectoryPath}\\DotNet\\Programs\\{programName}\\{programName}.dll";
                if (!File.Exists(programFilePath))
                {
                    // Then in the programs folder
                    programFilePath = $"{_mq2DirectoryPath}\\DotNet\\Programs\\{programName}.dll";
                    if (!File.Exists(programFilePath))
                    {
                        MQ2.WriteChatProgramError($"Couldn't find program: {programName}");
                        return;
                    }
                }


Time to get crackin and see what all I can do with this...
 
It's pretty freaking sweet, too bad it isn't fully developed... Looks like CombatAbilityReady isn't implemented, and some other stuff
Code:
        internal CharacterType(MQ2TypeFactory mq2TypeFactory, MQ2TypeVar typeVar) : base(mq2TypeFactory, typeVar)
        {
            Language = new IndexedStringMember<int, IntType, string>(this, "Language");
            XTarget = new IndexedMember<XTargetType, int>(this, "XTarget");
            XTAggroCount = new IndexedMember<IntType, int>(this, "XTAggroCount");
            SpellReady = new IndexedMember<BoolType, int, BoolType, string>(this, "SpellReady");
            SPA = new IndexedMember<IntType, int>(this, "SPA");
            Song = new IndexedMember<BuffType, string, BuffType, int>(this, "Song");
            SkillCap = new IndexedMember<IntType, string, IntType, int>(this, "SkillCap");
            SkillBase = new IndexedMember<IntType, string, IntType, int>(this, "SkillBase");
            Skill = new IndexedMember<IntType, string, IntType, int>(this, "Skill");
            RaidAssistTarget = new IndexedMember<SpawnType, int>(this, "RaidAssistTarget");
            RaidMarkNPC = new IndexedMember<SpawnType, int>(this, "RaidMarkNPC");
            PetBuff = new IndexedMember<SpellType, int, IntType, string>(this, "PetBuff");
            MercList = new IndexedStringMember<int, IntType, string>(this, "MercList");
            LanguageSkill = new IndexedMember<IntType>(this, "LanguageSkill");
            ItemReady = new IndexedMember<BoolType>(this, "ItemReady");
            Inventory = new IndexedMember<ItemType, string, ItemType, int>(this, "Inventory");
            HaveExpansion = new IndexedMember<BoolType, int>(this, "HaveExpansion");
            GroupMarkNPC = new IndexedMember<SpawnType, int>(this, "GroupMarkNPC");
            GemTimer = new IndexedMember<TimeStampType, int, TimeStampType, string>(this, "GemTimer");
            Gem = new IndexedMember<SpellType, int, IntType, string>(this, "Gem");
            BoundLocation = new IndexedMember<WorldLocationType, int>(this, "BoundLocation");
            AutoSkill = new IndexedMember<SkillType, int>(this, "AutoSkill");
            AltCurrency = new IndexedMember<IntType, int, IntType, string>(this, "AltCurrency");
            Buff = new IndexedMember<BuffType, string, BuffType, int>(this, "Buff");
            Book = new IndexedMember<SpellType, int, IntType, string>(this, "Book");
            Aura = new IndexedMember<AuraType, string, AuraType, int>(this, "Aura");
            AltAbilityReady = new IndexedMember<BoolType, int, BoolType, string>(this, "AltAbilityReady");
            AltAbilityTimer = new IndexedMember<TimeStampType, int, TimeStampType, string>(this, "AltAbilityTimer");
            AltAbility = new IndexedMember<AltAbilityType, int, AltAbilityType, string>(this, "AltAbility");
            AbilityReady = new IndexedMember<BoolType, int, BoolType, string>(this, "AbilityReady");
            Ability = new IndexedStringMember<int, IntType, string>(this, "Ability");
            Bandolier = new IndexedMember<BandolierType, string, BandolierType, int>(this, "Bandolier");
        }

Easy fix though if I had the ability to compile
Just need to add:
Code:
            CombatAbilityReady = new IndexedMember<BoolType, int, BoolType, string>(this, "CombatAbilityReady");
            CombatAbilityTimer = new IndexedMember<TimeStampType, int, TimeStampType, string>(this, "CombatAbilityTimer");
To the above
 
I just wanted to follow up and show how powerful / flexible this project is.




Code:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MQ2DotNet.MQ2API;
using MQ2DotNet.Program;
using MQ2DotNet.Services;
namespace RezAccept
{
    public class RezAccept : IProgram
    {
        private readonly MQ2 _mq2;
        private readonly Chat _chat;
        private readonly Commands _commands;
        private readonly Events _events;
        private readonly Spawns _spawns;
        private readonly TLO _tlo;
        public RezAccept(MQ2 mq2, Chat chat, Commands commands, Events events, Spawns spawns, TLO tlo)
        {
            _mq2 = mq2;
            _chat = chat;
            _commands = commands;
            _events = events;
            _spawns = spawns;
            _tlo = tlo;


        }
        public async Task Main(CancellationToken token, string[] args)
        {
            while (true)
            {
                try
                {
                    CheckRez();
                    await Task.Delay(1000 * 3);
                }
                catch (Exception ex)
                {
                    _mq2.WriteChat($"{ex.ToString()}");
                    WriteLog(ex.ToString());
                }
            }
        }
        public async Task CheckRez()
        {
            var confirmWindow = "ConfirmationDialogBox";
            var confirmText = "cd_textoutput";
            var confirmYes = "Yes_Button";
            var confirmNo = "No_Button";
            var respawnWindow = "RespawnWnd";
            var respawnOptions = "RW_OptionList";
            var respawnSelect = "RW_SelectButton";
            var respawnBind = "1";
            var respawnCorpse = "2";
            var leftClick = "leftmouseup";
            
            var caster = string.Empty;
            var rezPct = string.Empty;
            var confirm = _tlo.Window[confirmWindow];
            if (confirm.Open)
            {
                //_mq2.WriteChatSafe("Confirm Open");
                
                var windowText = confirm.Child[confirmText].Text.Trim();
                //_mq2.WriteChatSafe($"{windowText}");
                //Likely a rez spell
                if (windowText.Contains("corpse") || windowText.Contains("percent"))
                { 
                    //_mq2.WriteChatSafe("Likely a rez spell");
                
                    caster = windowText.Substring(0, windowText.IndexOf(" "));
                    //_mq2.WriteChat($"Caster: {caster}");
                   
                    
                    if (windowText.Contains("attempting to return you to your corpse."))
                    {
                        rezPct = "0";
                    }
                    else
                    {
                        rezPct = windowText.Substring(windowText.IndexOf("("));
                        rezPct = rezPct.Substring(0, rezPct.IndexOf(")")).Substring(1, rezPct.IndexOf(" "));
                    }
                    _mq2.WriteChat($"{caster} is trying to rez us with a {rezPct}pct rez");
                    
                    //Accept
                    if (Convert.ToInt32(rezPct) >= 90)
                    {
                        _mq2.WriteChatSafe("Accepting rez...");
                        _mq2.DoCommand($"/nomodkey /notify {confirmWindow} {confirmYes} {leftClick}");
                        await Task.Delay(1000 * 2);
                        while (_tlo.Me.Zoning)
                        {
                            await Task.Yield();
                        }
                        await Task.Delay(1000 * 2);
                        if (_mq2.Parse("${Me.State}").Equals("HOVER"))
                        {
                            //Need to accept spawn list
                            var spawn = _tlo.Window[respawnWindow];
                            if (spawn.Open)
                            {
                                _mq2.DoCommand($"/echo Respawning...");
                                _mq2.DoCommand($"/notify {respawnWindow} {respawnOptions} listselect {respawnCorpse}") 
                                await Task.Delay(1000 * 2);
                                _mq2.DoCommand($"/notify {respawnWindow} {respawnSelect} {leftClick}");
                            }
                        }
                    }
                    else
                    {
                        _mq2.WriteChat("Ignoring rez...");
                        _mq2.DoCommand($"/nomodkey /notify {confirmWindow} {confirmNo} {leftClick}");

                    } 
                }
                
            }
        }
        public void WriteLog(string message)
        {
            var logFile = @"~\rezaccept.log";
            File.WriteAllText(logFile,message);
        }



    }
}
 
I forked this project, so I could get some changes into it.
I was able to implement CombatAbility related items and I also now load debug symbols, to make debugging your MQ2DotNet Apps easier.
 
Yes, I develop my net new stuff in this . . . .

In the middle of porting some of my bigger framework projects over, and that was when I found the CombatAbility related stuff not implemented . . . now that is fixed.
I'm going to port some of my bigger stuff over and see what else I can find that I need to fix.
 
I should submit a PR. I just took a look at that RezAccept class and had some feedback.

C#:
public async Task Main(CancellationToken token, string[] args)
        {
            while (true)
            {
                try
                {
                    CheckRez();
                    await Task.Delay(1000 * 3);
                }
                catch (Exception ex)
                {
                    _mq2.WriteChat($"{ex.ToString()}");
                    WriteLog(ex.ToString());
                }
            }
        }

The try/catch basically does nothing. CheckRez is an async function, so if the intent to capture/log any exception, it *definitely* should be await'ed. Currently, if the CheckRez throws, the process may just crash.
The cancellation token isn't used at all...if thats intentional, the name should be prefixed with an underscore.
`ex.ToString()` is called twice.
Unnecessary string interpolation on the WriteChat method.

C#:
windowText.Contains("percent")

case sensitive code is too fragile. This should just use the 2nd arg, StringComparison.OrdinalIgnoreCase.

File.WriteAllText can just use the async version instead, File.WriteAllTextAsync

C#:
caster = windowText.Substring(0, windowText.IndexOf(" "));

This can be done elegantly with linq, new string(windowText.TakeWhile(x => !char.IsWhiteSpace(x))

C#:
if (Convert.ToInt32(rezPct) >= 90)

This could be something like byte.TryParse(rezPct, out var val), and then include a very clear debug message if it returns false. As is, if rezPct was some bizarre string, the error would just be "Input string was not in a correct format", which wouldn't not be helpful in debugging at all.

Anyway, I'm rambling at this point. If you need any help with c#, let me know.
 
Yes, I develop my net new stuff in this . . . .

In the middle of porting some of my bigger framework projects over, and that was when I found the CombatAbility related stuff not implemented . . . now that is fixed.
I'm going to port some of my bigger stuff over and see what else I can find that I need to fix.

Can you please share a short standalone script or two?
Would like to start diving in to this but its still a bit over my head
 
I should submit a PR. I just took a look at that RezAccept class and had some feedback.

C#:
public async Task Main(CancellationToken token, string[] args)
        {
            while (true)
            {
                try
                {
                    CheckRez();
                    await Task.Delay(1000 * 3);
                }
                catch (Exception ex)
                {
                    _mq2.WriteChat($"{ex.ToString()}");
                    WriteLog(ex.ToString());
                }
            }
        }

The try/catch basically does nothing. CheckRez is an async function, so if the intent to capture/log any exception, it *definitely* should be await'ed. Currently, if the CheckRez throws, the process may just crash.
The cancellation token isn't used at all...if thats intentional, the name should be prefixed with an underscore.
`ex.ToString()` is called twice.
Unnecessary string interpolation on the WriteChat method.

C#:
windowText.Contains("percent")

case sensitive code is too fragile. This should just use the 2nd arg, StringComparison.OrdinalIgnoreCase.

File.WriteAllText can just use the async version instead, File.WriteAllTextAsync

C#:
caster = windowText.Substring(0, windowText.IndexOf(" "));

This can be done elegantly with linq, new string(windowText.TakeWhile(x => !char.IsWhiteSpace(x))

C#:
if (Convert.ToInt32(rezPct) >= 90)

This could be something like byte.TryParse(rezPct, out var val), and then include a very clear debug message if it returns false. As is, if rezPct was some bizarre string, the error would just be "Input string was not in a correct format", which wouldn't not be helpful in debugging at all.

Anyway, I'm rambling at this point. If you need any help with c#, let me know.



Lol thanks... I've done C# since 1.0, that was a quick and dirty script to show the power of why you want to use C# over MQ2Scripting.
No where in there did I say this was production ready, it was a snippet. The Try/Catch being empty was just to log output while I work out the kinks dealing with failing gracefully. I wanted to see if I could get the stack trace etc ...
Turns out I couldn't and I had to load the symbol files into the assembly space (another fix I added since forking to load pdb files) ... I picked up this project off a fork of some one else's code with ZERO handoff / conversation...

So thank you for the unsolicited code review
EQ Rez windows text isn't exactly a dynamic environment that is being deployed to constantly.
 
Can you please share a short standalone script or two?
Would like to start diving in to this but its still a bit over my head

Here is an example, I hated using _mq2, etc... and wanted to be able to pass the TLO object around to other methods more easily.
I made this wrapper, and tossed in a few Helper routines, so you can see how you might want to piece this together. I didn't include the MessageHelper class, but all it does is wrap MQ2Objects.MQ2.WriteChat, with some pretty messages for the MQ2Window.

Pseudo Code:
C#:
   public class TestProgram : IProgram
    {
        public MQ2Objects MQ2Objects { get; set; }

        public TestProgram(MQ2 mq2, Chat chat, Commands commands, Events events, Spawns spawns, TLO tlo)
        {

            MQ2Objects = new MQ2Objects(mq2, chat, commands, events, spawns, tlo);
            MQ2Objects.Debug = true;

        }

        public async Task Main(CancellationToken token, string[] args)
        {
            // Need to Implement the IProgram interface
            // Think of this as your Sub Main in an MQ2 Macro
            // I normally wrap this with a top level try / catch block to handle any exceptions / avoid exploding
            // MQ2DotNet programs crashing doesn't crash EQ, as it also captures your assembly exceptions that bubble up

            // This uses ASYNC so make sure you are using Async/Await when possible to avoid locking up the
            // Main thread

            await TestStuff ();


        }

        //CombatAbilityReady wasn't supported, so this is a snippet
        // Of me verifying the TLO worked after I made changes to MQ2DotNet
        //If you wanted to test something simple:
        public async Task TestStuff()
        {
            /if (await MQ2Objects.CombatAbilityReady("Thwart"))
            {
                await MQ2Objects.DoDisc("Thwart");
            }
        }

        //This would check if Thwart was available and activate it.
        // Before I get another peer review on using strings... no shit this would be a variable
        // Instead of "Thwart", but he asked for quick snippets.


    }


C#:
    public class MQ2Objects
    {
        public bool Debug { get; set; }
        public MQ2 MQ2 { get; set; }
        public Chat Chat { get; set; }
        public Commands Commands { get; set; }
        public Events Events { get; set; }
        public Spawns Spawns { get; set; }
        public TLO TLO { get; set; }
        public TargetType Target { get; set; }
        public MessageHelper MessageHelper {get;set;}

  

        public MQ2Objects(MQ2 mq2, Chat chat, Commands commands, Events events, Spawns spawns, TLO tlo)
        {
            MQ2 = mq2;
            Chat = chat;
            Commands = commands;
            Events = events;
            Spawns = spawns;
            TLO = tlo;
            Debug = true;
               
        }
        public bool MQ2If(string expession)
        {
            return MQ2.If(expession);
        }
        public async Task<bool> AltAbilityReady(string aa)
        {
            WriteDebugMessage($"Checking AltAbilityReady: {aa}");
            var isReady = TLO.Me.AltAbilityReady[aa];
            WriteDebugMessage($"{aa} isReady: {isReady}");
            await Task.Yield();
            return isReady;
        }
        public async Task<bool> CombatAbilityReady(string ability)
        {
            WriteDebugMessage($"Checking CombatAbilityReady: {ability}");
            var rankCheck = MQ2.Parse($"${{Spell[{ability}].RankName}}");
            WriteDebugMessage($"Checking CombatAbilityReady: {rankCheck}");
       
            await Task.Delay(20);
            var isReady = TLO.Me.CombatAbilityReady[ability];
            WriteDebugMessage($"{ability} isReady: {isReady}");
            return isReady;
        }
        public async Task<bool> SpellReady(string spell)
        {
            WriteDebugMessage($"Checking SpellReady: {spell}");
            var rankCheck = MQ2.Parse($"${{Spell[{spell}].RankName}}");
            var isReady = TLO.Me.SpellReady[rankCheck];
            WriteDebugMessage($"{rankCheck} isReady: {isReady}");
            await Task.Yield();
            return isReady;
        }
        public async Task DoSpellCast(string spell)
        {
            var rankCheck = MQ2.Parse($"${{Spell[{spell}].RankName}}");
            await DoCommand($"/casting \"{rankCheck}\" -maxtries|2");
            while(TLO.Me.SpellInCooldown || MQ2.Parse("${Cast.Status}")  == "C")
            {
                await Task.Yield();
            }
       
        }
        public async Task DoCast(string spell)
        {
            await DoCommand($"/casting \"{spell}\" -maxtries|2");
            while (TLO.Me.SpellInCooldown || MQ2.Parse("${Cast.Status}") == "C")
            {
                await Task.Yield();
            }
        }

        public async Task DoDisc(string disc)
        {
       
            await DoCommand($"/disc {disc}");
            while (TLO.Me.SpellInCooldown || MQ2.Parse("${Cast.Status}") == "C")
            {
                await Task.Yield();
            }
        }

        public async Task DoCommand(string command)
        {
            WriteDebugMessage($"Doing command: {command}");
            MQ2.DoCommand(command);
            while (TLO.Me.SpellInCooldown || MQ2.Parse("${Cast.Status}") == "C")
            {
                await Task.Yield();
            }

        }
        public void WriteMessage(string message)
        {
            MessageHelper.WriteMessage(message);
        }
        public void WriteDebugMessage(string message)
        {
            if (Debug)
            {
                WriteMessage(message);
            }
        }
 
Can you please share a short standalone script or two?
Would like to start diving in to this but its still a bit over my head


If I get some time this weekend I'll make a practically empty repo on gitlab, so you can at least clone it and open in Visual Studio, and have a starting point.
That will probably help you get started faster than anything.
 
I tried to figure this out, probably do need an actual tutorial for it. Got to the point of creating a .dll file and "bootstrapping" a main to it, but need a walk through for just the basic skeleton of necessity. Very nice of you to put this out there though. I think that a lot of people are already thinking the same thing of how much more efficient it would be to have a few more plugins so that the macros don't have to work so hard. Being able to customize game play is a huge part of it. I like to have one "controller" for my main tank and then a universal controller for the other characters which reference their own ini's.

I have been programing again in C#. I'm starting to see this code in another perspective now. Getting some knowledge of aggregates/delegates and looking at Interfaces. I will try to work with some of this example code for a skeleton plugin soon.
 
Last edited:
yeah i tried a few months ago and couldnt get off the ground, hopefully @Tone or @alynel or someone that has gotten it working can post a decent script and plugin with enough instruction to get us started
 
Yea, I've got a couple empty projects that should be enough to get people started...

Just been swamped with work shit from the rona etc...
 
Has anyone investigated what would be required to build our macro's targetting .net core? I took a look at the MQ2DotNet source code and the .net project is using the old project format and targetting full framework 4.7.1. The project would need to be converted to the new project format and the framework target(s) updated. Not sure what else is required. My best guess is that the cpp loader plugin project is using a hard coded .net runtime version that would needed updated / made configurable?

Just curious.

Cheers,
Rhino
 
Here is an example, I hated using _mq2, etc... and wanted to be able to pass the TLO object around to other methods more easily.
I made this wrapper, and tossed in a few Helper routines, so you can see how you might want to piece this together. I didn't include the MessageHelper class, but all it does is wrap MQ2Objects.MQ2.WriteChat, with some pretty messages for the MQ2Window.

Pseudo Code:
C#:
   public class TestProgram : IProgram
    {
        public MQ2Objects MQ2Objects { get; set; }

        public TestProgram(MQ2 mq2, Chat chat, Commands commands, Events events, Spawns spawns, TLO tlo)
        {

            MQ2Objects = new MQ2Objects(mq2, chat, commands, events, spawns, tlo);
            MQ2Objects.Debug = true;

        }

        public async Task Main(CancellationToken token, string[] args)
        {
            // Need to Implement the IProgram interface
            // Think of this as your Sub Main in an MQ2 Macro
            // I normally wrap this with a top level try / catch block to handle any exceptions / avoid exploding
            // MQ2DotNet programs crashing doesn't crash EQ, as it also captures your assembly exceptions that bubble up

            // This uses ASYNC so make sure you are using Async/Await when possible to avoid locking up the
            // Main thread

            await TestStuff ();


        }

        //CombatAbilityReady wasn't supported, so this is a snippet
        // Of me verifying the TLO worked after I made changes to MQ2DotNet
        //If you wanted to test something simple:
        public async Task TestStuff()
        {
            /if (await MQ2Objects.CombatAbilityReady("Thwart"))
            {
                await MQ2Objects.DoDisc("Thwart");
            }
        }

        //This would check if Thwart was available and activate it.
        // Before I get another peer review on using strings... no shit this would be a variable
        // Instead of "Thwart", but he asked for quick snippets.


    }


C#:
    public class MQ2Objects
    {
        public bool Debug { get; set; }
        public MQ2 MQ2 { get; set; }
        public Chat Chat { get; set; }
        public Commands Commands { get; set; }
        public Events Events { get; set; }
        public Spawns Spawns { get; set; }
        public TLO TLO { get; set; }
        public TargetType Target { get; set; }
        public MessageHelper MessageHelper {get;set;}

 

        public MQ2Objects(MQ2 mq2, Chat chat, Commands commands, Events events, Spawns spawns, TLO tlo)
        {
            MQ2 = mq2;
            Chat = chat;
            Commands = commands;
            Events = events;
            Spawns = spawns;
            TLO = tlo;
            Debug = true;
              
        }
        public bool MQ2If(string expession)
        {
            return MQ2.If(expession);
        }
        public async Task<bool> AltAbilityReady(string aa)
        {
            WriteDebugMessage($"Checking AltAbilityReady: {aa}");
            var isReady = TLO.Me.AltAbilityReady[aa];
            WriteDebugMessage($"{aa} isReady: {isReady}");
            await Task.Yield();
            return isReady;
        }
        public async Task<bool> CombatAbilityReady(string ability)
        {
            WriteDebugMessage($"Checking CombatAbilityReady: {ability}");
            var rankCheck = MQ2.Parse($"${{Spell[{ability}].RankName}}");
            WriteDebugMessage($"Checking CombatAbilityReady: {rankCheck}");
      
            await Task.Delay(20);
            var isReady = TLO.Me.CombatAbilityReady[ability];
            WriteDebugMessage($"{ability} isReady: {isReady}");
            return isReady;
        }
        public async Task<bool> SpellReady(string spell)
        {
            WriteDebugMessage($"Checking SpellReady: {spell}");
            var rankCheck = MQ2.Parse($"${{Spell[{spell}].RankName}}");
            var isReady = TLO.Me.SpellReady[rankCheck];
            WriteDebugMessage($"{rankCheck} isReady: {isReady}");
            await Task.Yield();
            return isReady;
        }
        public async Task DoSpellCast(string spell)
        {
            var rankCheck = MQ2.Parse($"${{Spell[{spell}].RankName}}");
            await DoCommand($"/casting \"{rankCheck}\" -maxtries|2");
            while(TLO.Me.SpellInCooldown || MQ2.Parse("${Cast.Status}")  == "C")
            {
                await Task.Yield();
            }
      
        }
        public async Task DoCast(string spell)
        {
            await DoCommand($"/casting \"{spell}\" -maxtries|2");
            while (TLO.Me.SpellInCooldown || MQ2.Parse("${Cast.Status}") == "C")
            {
                await Task.Yield();
            }
        }

        public async Task DoDisc(string disc)
        {
      
            await DoCommand($"/disc {disc}");
            while (TLO.Me.SpellInCooldown || MQ2.Parse("${Cast.Status}") == "C")
            {
                await Task.Yield();
            }
        }

        public async Task DoCommand(string command)
        {
            WriteDebugMessage($"Doing command: {command}");
            MQ2.DoCommand(command);
            while (TLO.Me.SpellInCooldown || MQ2.Parse("${Cast.Status}") == "C")
            {
                await Task.Yield();
            }

        }
        public void WriteMessage(string message)
        {
            MessageHelper.WriteMessage(message);
        }
        public void WriteDebugMessage(string message)
        {
            if (Debug)
            {
                WriteMessage(message);
            }
        }

The only real issues I see with this is that you're relying on MQ2Cast and macroblock code rather than calling native MQ2 API functions directly. This will slow down your code, and potentially reduce its reliability.
 
I took a look into what would need to be done to target .net core. Some things I've identified that would have to happen.

1. Update project files to the new project format
2. Eliminate 3rd party dependencies that don't target .net standard or reliably multi-target the different major versions of .net core (e.g. `GitVersionTask`)
3. Replace the use of AppDomains with the use of AssemblyLoadContext for dynamic loading of the assemblies (I'm haven't determined yet if MQ2DotNet was relying on the app domains for some sort of isolation concerns or just dynamic assembly loading. If it's the former then we'll have to investigate alternate approaches)
4. Fix other dependency/signature changes <-- still investigating
5. Figure out a way to make the cpp code that loads the CLR runtime version configurable (currently hard coded to "v4.0.30319")
 
I'm haven't determined yet if MQ2DotNet was relying on the app domains for some sort of isolation concerns or just dynamic assembly loading

It was done that way to be able to unload the dll from memory. Once a dll is loaded into a domain it cannot be unloaded. The separate domain allowed that domain to be unloaded taking the dll with it. Why do this? Because you'd have to restart eq everytime you wanted to update the .net dll. This way, the loader stays in memory, but the target .net dll can be loaded, unloaded, and updated all while eq is running.
 
I've had the loader, unloader working for a while but never had time to figure out an efficient way to hook it in to the MQ2 main data. Now I finally had some time but it seems others are working on it, too.
 
I wouldn't worry too much about what others are working on. Collaborate if you can and save each other some work, but otherwise people start and stop work all the time.
 
Release .NET / C# Plugin and "Program" API

Users who are viewing this thread

Back
Top
Cart