What I want to build
I'm building an external-observer AI agent for EverQuest — screen capture + vision model + LLM + mouse/keyboard actions, running entirely outside the EQ process. No injection, no hooks, no memory reads. To get an idea of what I want to toy around with check out the Starcraft community. https://github.com/google-deepmind/pysc2. AlphaStar didn't learn to play StarCraft II from scratch via self-play — it was bootstrapped on ~1 million human replays via supervised learning, and only then refined with RL and league play. Every serious StarCraft bot that came before it (CherryPi, SAIDA, TStarBot) also leaned heavily on imitation or hard-coded expert systems as the foundation. The blunt lesson learned during the 2010s was: pure RL on a complex game from a cold start does not work at a practical compute budget. MacroQuest can provide a great foundation for this. The real question is can it be partially driven by an LLM alongside smaller RL models and a custom build expert system hosted on the same machine that runs EQ natively. My guess is yes!What I want to build is a one-time data-collection harness that runs under MQ on an emulator (TAKP / Quarm / P99) or Live, logging perfect in-memory game state alongside timestamped screenshots. That becomes labeled training data for the external vision pipeline. The final system never touches MQ — I just need it once to bootstrap the dataset. Target: ~20–40 hours of clean data across a couple of low-level old-school zones like West Freeport and various class/level combos.
Yes, the broader project shouldn't need MacroQuest once it gets going. Not trying to hide that. But MQ gives perfect structured ground truth, which is exactly what you want to train vision models against screenshots. This is a scoped and finite not ongoing automation work.
__
Background: I'm a Principal Machine Learning Engineer by day (demanding job, without much free time). I use agentic AI tools daily at work and can produce the Python side of this project quickly. I'm also EQ player since 1999 — classic-era grognard, 6-box RG alum — though I've barely played in recent years. In grad school, I studied Reinforcement Learning and NLP.The ML/AI side I've got covered. I've also read the MQ Lua docs and the bindings/ source enough to write the spec below. What I don't have is production Lua / VV experience — I've never shipped an MQ macro or plugin, and the RG community has people who can build this in a weekend where it would take me a month of fumbling.
If existing plugins cover part of this already, I'd rather build on top of them than reinvent. Happy to hear what you'd reuse — no need to rebuild an event manager from scratch if something already does 60% of the job.
The script at a high level
Runs on a test character on an emulator while that character plays normally (manual, VV, or CWTN). Emits two synchronized log streams:- State ticks — structured game-state snapshot every 200 ms (player, target, merc, pet, buffs, gems, disciplines, positions, group, nearby spawns).
- Event stream — combat, spell, chat, loot, death, zone, XP events with structured fields.
Requirements
I've read through the MQ Lua docs and the src/plugins/Lua/bindings/ source, so I know most of the state fields below are one-line TLO reads. The real work is event authoring, robust file handling, and not starving the frame loop. Framing the ask accordingly.Environment
- Modern MQ with Lua on an emulator (TAKP preferred; Quarm or P99 fine)
- Lua only — no .mac
- Must coexist with VV / CWTN / Muleassist without interference
Drive tick cadence on mq.gettime() deltas (not raw mq.delay(200)) so drift stays bounded under load. Each tick: high-res timestamp + monotonically increasing tick ID, then standard TLO reads for:
- Me: HP/Mana/End (cur/max/pct), X/Y/Z/Heading, Zone.ShortName, Class, Level, Combat/Sitting/Invis/Mount/Casting
- Target: CleanName, Type, PctHPs, ConColor, Distance3D, Level, Class, Casting
- Me.Mercenary.*, Me.Pet.*
- Me.Buff(i) — {name, duration_s, caster}. Caster can be nil/unreliable for buffs you didn't cast; keep the field but don't paper over the gap.
- Me.Gem(slot) + GemTimer + SpellReady
- ActiveDisc + CombatAbilityReady / CombatAbilityTimer for the class standard set
- Group.Member(i) — accept that Mana/End for non-self members is sparse until inspected
- SpawnCount('npc radius N los') + NearestSpawn(1,'npc radius N xtarhater') — exact search string is a config knob
Event stream — JSONL
There's no built-in event catalog in MQ, so this is the part I'd most like your judgment on. Target events: melee hits/misses, spell cast/land/resist/fizzle, death, loot, XP/AA gains, faction, chat (all channels), zone transitions, engage/disengage, res. Each event: {ts, tick_id, kind, actor, target, amount, detail}, unknowns preserved as kind: "other" with raw text.
If you have an existing event library (Mighty Lua Event Manager, a private one, whatever) that I can build on, that's ideal. mq.event + regex-ish patterns + mq.doevents() in the main loop is the expected pattern.
Capture-sync
Each tick, signal the current tick ID out of MQ so the external Python process can match a screenshot to it. Options I'm open to, in rough preference order:
- mq.actors message on a named mailbox — cleanest if Python can receive it (I may need to write a tiny bridge)
- Write tick ID to a sidecar file (atomic rename or single fixed-size line)
- Named pipe / local UDP
Config
Lua table in a file (config.Lua) or INI — whatever would be fastest. Tick rate, field-group toggles, event-type toggles, output dir, rotation size, spawn-search strings.
Action log
Ideal: log every /keypress and /doKey issued by any running script — {ts, key, source_script, command_text} — for behavior cloning. I understand /keypress goes straight through MQ without a Lua-visible hook and observing other scripts' keystrokes isn't doable in pure Lua. So: if this is a C++ plugin ask, flag it and skip. Logging this script's own commands is fine.
If anyone has any insights on how to best structure this, let me know!
Happy to answer questions about the broader project. I recognize that the data collection will require upwards of 200GB for several hours of gameplay, but that's Ok!

