<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: manasightgg</title>
    <description>The latest articles on Forem by manasightgg (@manasightgg).</description>
    <link>https://forem.com/manasightgg</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3806937%2F68a2bbcd-7e5c-4c98-8231-4a3b113217ef.jpg</url>
      <title>Forem: manasightgg</title>
      <link>https://forem.com/manasightgg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/manasightgg"/>
    <language>en</language>
    <item>
      <title>80,000 Lines of Code in 51 Days</title>
      <dc:creator>manasightgg</dc:creator>
      <pubDate>Sun, 29 Mar 2026 22:15:42 +0000</pubDate>
      <link>https://forem.com/manasightgg/80000-lines-of-code-in-51-days-2a3</link>
      <guid>https://forem.com/manasightgg/80000-lines-of-code-in-51-days-2a3</guid>
      <description>&lt;p&gt;&lt;em&gt;Four open-source workflow commands, the counterintuitive lesson behind them, and a question: what does your process look like?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Early on, I tried running multiple Claude agents in parallel. Hand them separate issues, let them all implement simultaneously, merge everything at the end. On paper, this should have been the optimal approach. AI writes code fast. More agents means more throughput. Simple math.&lt;/p&gt;

&lt;p&gt;It made things worse.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.manasight.gg/why-im-building-an-mtg-arena-companion-from-scratch/" rel="noopener noreferrer"&gt;Manasight&lt;/a&gt; is a desktop companion for MTG Arena, the free-to-play digital version of Magic: The Gathering. It's a Tauri app: Rust, TypeScript, Astro. One developer. The project and the game don't matter for this post — the development workflow does. &lt;a href="https://blog.manasight.gg/ai-testing-implementer-reviewer-pattern/" rel="noopener noreferrer"&gt;The last post&lt;/a&gt; covered testing. This one covers building: how GitHub issues become merged pull requests without me writing the code.&lt;/p&gt;

&lt;p&gt;The project now has roughly 80,000 lines of code across six repositories. 33,000 lines of production code, 47,000 lines of tests. 616 merged pull requests. Around 6,590 test cases. 51 days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why parallel agents made things worse
&lt;/h2&gt;

&lt;p&gt;First, I couldn't keep track of what each agent was doing. Each agent needed small corrections, and those corrections compounded when agents were working from stale context. By the time one agent finished, others had already changed the code it was building on.&lt;/p&gt;

&lt;p&gt;Second, merging was painful. Each agent had worked from a different snapshot of the codebase, so figuring out how the changes interacted was harder than reviewing them one at a time.&lt;/p&gt;

&lt;p&gt;Claude writes code so fast that running agents in parallel barely improves total wall-clock time. The long pole isn't implementation. It's human review and QA. Sequential processing is slower on paper but faster in practice because the human can actually think.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson: optimize for the human's reasoning capacity, not the AI's coding speed.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Four Claude Code commands, published as open source
&lt;/h2&gt;

&lt;p&gt;I built four workflow commands around that lesson. They're the system behind those 616 merged PRs, and I've open-sourced them at &lt;a href="https://github.com/manasight/manasight-claude-commands" rel="noopener noreferrer"&gt;manasight-claude-commands&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I don't know if these patterns generalize beyond one project. This is still evolving, and the best way to pressure-test it is to put it in front of other developers.&lt;/p&gt;

&lt;p&gt;The rest of this post walks through the system. The thinking behind the commands is the part I hope is useful regardless of whether you ever use the commands themselves.&lt;/p&gt;

&lt;p&gt;Here's how the four commands fit together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/issue-to-pr          The atomic unit. One issue in, one reviewed PR out.
    |
    +-- /self-review  The review gate. 5 parallel agents score findings
    |                 by confidence, filtering false positives.
    |
/sequential-issues    Chains /issue-to-pr across multiple issues.
    |                 Each PR stacks on the previous branch.
    |
    +-- /merge-stack  Collapses the stack. Retargets all PRs to main,
                      rebases unique commits, squash-merges in order.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Specs before code
&lt;/h2&gt;

&lt;p&gt;The commands don't work without a prerequisite: the work has to be well-specified before Claude touches it.&lt;/p&gt;

&lt;p&gt;Every PR starts with a fully specified GitHub issue. Depending on the size of the change, the issue might be backed by a feature spec, an architecture decision record, and/or a research doc — but the specificity lives in the issue itself.&lt;/p&gt;

&lt;p&gt;Before implementation, I prompt Claude to review the issue and tell me if it's ready to implement. This is the Implementer/Reviewer pattern applied outside of code: Claude flags ambiguity, missing acceptance criteria, or unstated assumptions before any code gets written. Under-specified work produces code I have to throw away.&lt;/p&gt;

&lt;p&gt;During planning, I ask Claude to break work into the fewest issues possible while keeping each under 200 lines of code (the median PR lands at 243). Claude produces noticeably better work at this scope. Above it, I find myself reworking or throwing away PRs. Below it, I haven't. Small, stacked changes are also easier for the human to reason about — reviewing is still my job, and small diffs make that job possible.&lt;/p&gt;

&lt;p&gt;Test code outweighs production code by 144%. &lt;a href="https://blog.manasight.gg/ai-testing-implementer-reviewer-pattern/" rel="noopener noreferrer"&gt;My last post&lt;/a&gt; covered the QA philosophy behind that number.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;/issue-to-pr&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The atomic unit. One GitHub issue URL in, one reviewed PR out (&lt;a href="https://github.com/manasight/manasight-claude-commands/blob/main/commands/issue-to-pr.md" rel="noopener noreferrer"&gt;source&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Five phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Research.&lt;/strong&gt; A sub-agent explores the codebase to understand the relevant code paths. This runs in isolation so the search results don't consume the parent's context window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implementation.&lt;/strong&gt; Write the code, write the tests, run the pre-commit checks defined in the project's CLAUDE.md.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR creation.&lt;/strong&gt; Branch, commit, push, open the pull request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-review.&lt;/strong&gt; A fresh-context agent examines the PR. This is the &lt;a href="https://blog.manasight.gg/ai-testing-implementer-reviewer-pattern/" rel="noopener noreferrer"&gt;Implementer/Reviewer pattern&lt;/a&gt; built directly into the workflow. The review agent has no memory of the implementation decisions. It reads the diff cold, the same way a teammate would on a real pull request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration.&lt;/strong&gt; If the review finds issues, the implementation agent fixes them and the reviewer runs again. Up to 10 rounds. Then it's ready for human review.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most PRs take one to three review iterations. The common findings: missing edge case tests, inconsistent error handling, a function that could be simplified. The things a human reviewer catches too, except this runs automatically on every PR regardless of how tired I am or how late it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;/sequential-issues&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;My preferred workflow for planned work (&lt;a href="https://github.com/manasight/manasight-claude-commands/blob/main/commands/sequential-issues.md" rel="noopener noreferrer"&gt;source&lt;/a&gt;). It chains &lt;code&gt;/issue-to-pr&lt;/code&gt; across a list of issues using stacked branches: each PR targets the previous one's branch, so each diff shows only its own changes.&lt;/p&gt;

&lt;p&gt;Two gates per issue, both mandatory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code review&lt;/strong&gt; passes (iterative, using &lt;code&gt;/self-review&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI green&lt;/strong&gt; (tests, linting, formatting all pass)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The human's work is front-loaded and back-loaded. Front-loaded: planning, writing specs, ordering the issues. Back-loaded: reviewing the PR stack, running QA. The middle is autonomous.&lt;/p&gt;

&lt;p&gt;This is where the Implementer/Reviewer pattern scales. Every issue in the sequence gets independent fresh-context review. The fifth PR in a stack gets the same review rigor as the first, because each review agent starts from scratch. No fatigue, no "I already looked at this codebase today" shortcuts. That's something I can't match as a human reviewer at 11 PM on a Tuesday.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;/self-review&lt;/code&gt; and &lt;code&gt;/merge-stack&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;(&lt;a href="https://github.com/manasight/manasight-claude-commands/blob/main/commands/self-review.md" rel="noopener noreferrer"&gt;self-review source&lt;/a&gt; | &lt;a href="https://github.com/manasight/manasight-claude-commands/blob/main/commands/merge-stack.md" rel="noopener noreferrer"&gt;merge-stack source&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/self-review&lt;/code&gt; started as a fork of Anthropic's &lt;a href="https://github.com/anthropics/claude-code/blob/main/plugins/code-review/commands/code-review.md" rel="noopener noreferrer"&gt;&lt;code&gt;/code-review&lt;/code&gt;&lt;/a&gt; plugin. Five parallel agents examine the PR independently, each finding scored by confidence. Only findings above a threshold surface in the feedback. Without that filtering, too many false positives and you stop reading the comments.&lt;/p&gt;

&lt;p&gt;Two things I changed. First, I lowered the confidence threshold from 80 to 75. Second, Anthropic's version won't re-review a PR it's already reviewed. Mine will. In the autonomous loop, &lt;code&gt;/self-review&lt;/code&gt; runs after each round of fixes, and it occasionally catches something on the second or third pass that it missed the first time. Because no human is watching the middle of the loop, I'd rather surface more findings and re-review than skip checks and let bugs into the product.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/merge-stack&lt;/code&gt; collapses a stack of reviewed PRs into main. It retargets each PR, rebases the unique commits, and squash-merges in order. The bookkeeping that's tedious to do by hand but straightforward to automate.&lt;/p&gt;

&lt;p&gt;Both are documented and open-sourced alongside the other two commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;A few weeks ago, &lt;a href="https://github.com/frankfaustino" rel="noopener noreferrer"&gt;frankfaustino&lt;/a&gt; found the Manasight parser repo, filed issues, and merged bug fixes. The first outside contributor. Thank you, keep them coming!&lt;/p&gt;

&lt;p&gt;All four commands are at &lt;a href="https://github.com/manasight/manasight-claude-commands" rel="noopener noreferrer"&gt;manasight/manasight-claude-commands&lt;/a&gt; with &lt;code&gt;&amp;lt;!-- CUSTOMIZE --&amp;gt;&lt;/code&gt; markers showing what to adapt. If you've built something different, I'd love to hear about it. The companion repo has &lt;a href="https://github.com/manasight/manasight-claude-commands/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt; enabled for exactly this.&lt;/p&gt;

&lt;p&gt;Optimize for the human. The AI will keep up.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Tim. I'm building &lt;a href="https://blog.manasight.gg" rel="noopener noreferrer"&gt;Manasight&lt;/a&gt;, a desktop companion for MTG Arena, solo with Rust and Tauri. Follow &lt;a href="https://x.com/manasightgg" rel="noopener noreferrer"&gt;@manasightgg&lt;/a&gt; on Twitter and &lt;a href="https://bsky.app/profile/manasight.gg" rel="noopener noreferrer"&gt;@manasight.gg&lt;/a&gt; on Bluesky for build-in-public updates.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Everyone Writes About AI Generating Code. Nobody Writes About AI Testing It.</title>
      <dc:creator>manasightgg</dc:creator>
      <pubDate>Thu, 26 Mar 2026 04:22:47 +0000</pubDate>
      <link>https://forem.com/manasightgg/everyone-writes-about-ai-generating-code-nobody-writes-about-ai-testing-it-171a</link>
      <guid>https://forem.com/manasightgg/everyone-writes-about-ai-generating-code-nobody-writes-about-ai-testing-it-171a</guid>
      <description>&lt;p&gt;&lt;em&gt;I play Magic the Gathering (MTG) Arena while background agents file bug reports.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I expected AI-assisted development to help me write code faster. It did. The part I didn't expect was how much it changed testing and debugging.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.manasight.gg/why-im-building-an-mtg-arena-companion-from-scratch/" rel="noopener noreferrer"&gt;Manasight&lt;/a&gt; is a desktop overlay for MTG Arena, the free-to-play digital version of Magic: The Gathering. It's a Tauri app with a Rust backend, TypeScript frontend, and a companion Astro website. I'm one developer. The project has roughly 70,000 lines of code across six repositories, with over 6,000 tests. Every line was written with Claude Code. Every line was tested with it too.&lt;/p&gt;

&lt;p&gt;This post is about the testing side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code, slash commands, and agents
&lt;/h2&gt;

&lt;p&gt;If you haven't used &lt;a href="https://docs.anthropic.com/en/docs/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;: it's an AI coding assistant that runs in your terminal. You give it access to your codebase, and it can read files, write code, run shell commands, and use tools, all within a conversation.&lt;/p&gt;

&lt;p&gt;Two Claude Code features matter for this post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slash commands&lt;/strong&gt; are reusable prompt scripts. I write a markdown file that describes a workflow: what files to read, what tools to use, what rules to follow, what the output should look like. Then I run it with &lt;code&gt;/command-name&lt;/code&gt;. Think of them as executable runbooks. I have commands for debugging, triaging feedback, code review, and turning GitHub issues into pull requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents&lt;/strong&gt; are Claude instances that run tasks in the background. The parent conversation spawns them with a specific job ("trace this code path and report what you find"), and they report back when done. You don't wait for them. You keep working.&lt;/p&gt;

&lt;h2&gt;
  
  
  QA as a solo developer
&lt;/h2&gt;

&lt;p&gt;The standard solo-dev testing experience: write code, switch to testing mode, find bugs, switch back to fix them, test again. Every context switch costs time and focus.&lt;/p&gt;

&lt;p&gt;With a game overlay, it's worse. You can't play your own game and debug the overlay simultaneously. You can't run a 41-test-case platform validation protocol yourself across four operating systems. You can't triage user feedback submissions while writing the next feature.&lt;/p&gt;

&lt;p&gt;A solo dev without AI either skips QA or stops shipping. I chose a third option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging while playing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/debug-game&lt;/code&gt; is a slash command I wrote for live QA sessions. I launch MTG Arena, start Manasight, and play a match. When something looks wrong: a card missing from the tracker, a lag spike, a panel in the wrong position. I describe it in the conversation or say "look at the latest screenshot." Claude already knows where my screenshot folder is, so there's no file paths or copy-pasting.&lt;/p&gt;

&lt;p&gt;No code changes happen during the QA session. Claude launches background agents to investigate: one traces the code path from the game event through the parser to the overlay, another checks the Manasight application log for errors, another reads the Arena Player.log to see what the game client actually reported. Each agent works independently while the parent conversation stays responsive. I keep playing, keep finding things, keep reporting.&lt;/p&gt;

&lt;p&gt;At the end of the session, I have a tracker table. Each finding has a type (bug, performance issue, feature request), the repo it belongs to, a description, and an estimated fix size. Each row is ready to file as a GitHub issue.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F982pgmm2ick8hipgd455.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F982pgmm2ick8hipgd455.png" alt="Bug tracker table screenshot        " width="800" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is where Agent Teams clicked. Claude Code has a Task mode where you kick off background agents and wait for results -- fine for implementation work. Agent Teams are different: multiple background agents run while the parent conversation stays fully interactive. I keep talking, keep reporting findings, and the investigations happen in parallel without blocking me. QA requires that live back-and-forth. I'm playing, spotting issues, describing them in real time. The parent conversation coordinates while team agents root-cause in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback pipeline
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;/debug-game&lt;/code&gt; sessions taught me something: Claude is fast at diagnosing problems when it has logs. The Manasight application log, the Arena Player.log, a description of what happened. That's usually enough. The bottleneck isn't diagnosis. It's getting the right files in front of the right context.&lt;/p&gt;

&lt;p&gt;So I built a "Send Feedback" button right into Manasight's UI. Players describe what happened, and the submission automatically uploads the Manasight application log and their Arena Player.log to a backend storage bucket for future debugging.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9cppi0p3z5w8noklxvos.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9cppi0p3z5w8noklxvos.png" alt="Send Feedback button screenshot" width="568" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbvhupy1uhdzgh9sdkou.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbvhupy1uhdzgh9sdkou.png" alt="Feedback form screenshot" width="566" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The macOS crash
&lt;/h2&gt;

&lt;p&gt;What happened next? An early Mac beta-tester reported that the app closed immediately when they clicked Send Feedback. The irony: the crash was in the feature designed to collect diagnostic data, so the automatic log collection couldn't help.&lt;/p&gt;

&lt;p&gt;I asked them to send three files manually: the Arena Player.log, the Manasight application log, and the IPS crash report that macOS generates for unexpected app exits. Even before the files arrived, Claude had inspected the Send Feedback code path and narrowed down a short list of suspects based on which function calls could cause a crash on macOS but not Windows.&lt;/p&gt;

&lt;p&gt;Once the logs arrived, Claude identified the root cause immediately. A &lt;code&gt;set_focusable()&lt;/code&gt; call that re-enables keyboard input for the feedback form was being called on macOS, where the overlay window is an NSPanel with &lt;code&gt;can_become_key_window: false&lt;/code&gt;. That combination causes the app to exit. The fix was a one-line platform guard: &lt;code&gt;#[cfg(target_os = "windows")]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This kind of bug used to be painful. A crash you can't reproduce, on a platform you don't use daily, reported by someone who just wants the app to work. Now I look forward to getting the logs, because the path from "user reported a crash" to "fix deployed" is measured in minutes, not days.&lt;/p&gt;

&lt;p&gt;I used to dread bug reports. Now I want them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the loop
&lt;/h2&gt;

&lt;p&gt;Building a feedback feature creates a second problem: someone has to read the submissions. The usual approach is an admin dashboard. I built a &lt;code&gt;/triage-feedback&lt;/code&gt; slash command for Claude Code instead.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/triage-feedback&lt;/code&gt; queries a Cloudflare D1 database which tracks feedback submissions, and fetches attached logs and screenshots from a backend storage bucket. For each submission, Claude shows the user comment, the operating system and app version, which log files were attached, and the game context. It summarizes long logs, suggests a priority level, and can create a GitHub issue directly from the triage session.&lt;/p&gt;

&lt;p&gt;No admin UI. No web dashboard. The CLI is the dashboard. For a solo developer, that's one less thing to build and maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementer/Reviewer pattern
&lt;/h2&gt;

&lt;p&gt;A theme runs through all of this. Whatever an agent builds or investigates, a different agent reviews it with fresh context.&lt;/p&gt;

&lt;p&gt;I didn't plan this pattern. I stumbled into it. Early on, I tried a GitHub integration from Anthropic that ran Claude Code inside a GitHub Action to review PRs. Fresh-context agents posted review comments directly on the pull request.&lt;/p&gt;

&lt;p&gt;The concept was right. Agents reviewing from fresh context found problems the implementation agent missed, and the PR comments felt like having another person on the team. But it fell apart with multi-language work. Rust feedback was terse and rarely surfaced anything useful. TypeScript was the opposite: commenting on every small detail, not distinguishing must-fix from nice-to-have.&lt;/p&gt;

&lt;p&gt;I ended up using Claude Code's built-in code review plugin (&lt;code&gt;/code-review&lt;/code&gt;). It runs five parallel agents from a Claude conversation on my developer machine (not Github Actions). Each agent independently scores findings. Only findings scored 80 or above are reported in final feedback. I integrated this review into slash commands I use to raise PRs autonomously.&lt;/p&gt;

&lt;p&gt;This Implementer/Reviewer pattern extends beyond code review. Everything I build with Claude gets an independent fresh-context review. Feature specs get fresh-context review before implementation. Task breakdowns that I generate from feature specs often go through multiple rounds of review. Even these blog posts go through 5-10 rounds of agentic review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fresh context at every level
&lt;/h2&gt;

&lt;p&gt;The agentic review layers don't eliminate human review. My job is reviewing findings and the final product. How carefully I review depends on the risk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure and security code&lt;/strong&gt; gets my closest read&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend APIs&lt;/strong&gt;, I focus on the tests and behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend code&lt;/strong&gt; gets a lighter pass -- the real validation happens when I run it in the browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blog posts&lt;/strong&gt; I review word by word, multiple times, read aloud&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is AI-generated. The quality isn't.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Tim. I'm building &lt;a href="https://blog.manasight.gg" rel="noopener noreferrer"&gt;Manasight&lt;/a&gt;, an MTG Arena companion tool, solo with Rust and Tauri. Follow &lt;a href="https://x.com/manasightgg" rel="noopener noreferrer"&gt;@manasightgg&lt;/a&gt; on Twitter for build-in-public updates.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next up: How my GitHub issues become fully reviewed pull requests autonomously, why sequential beats parallel, and what the human actually does when the AI is writing the code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Everything We Know About the MTG Arena Log File in 2026</title>
      <dc:creator>manasightgg</dc:creator>
      <pubDate>Fri, 13 Mar 2026 15:00:00 +0000</pubDate>
      <link>https://forem.com/manasightgg/everything-we-know-about-the-mtg-arena-log-file-in-2026-1k7l</link>
      <guid>https://forem.com/manasightgg/everything-we-know-about-the-mtg-arena-log-file-in-2026-1k7l</guid>
      <description>&lt;p&gt;&lt;em&gt;Most of what's been written about the Arena log format is outdated. Here's what it actually looks like.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;MTG Arena is the free-to-play digital version of Magic: The Gathering. When I started building a companion tool and needed to &lt;a href="https://github.com/manasight/manasight-parser" rel="noopener noreferrer"&gt;parse the Arena log file&lt;/a&gt;, I based my initial design on existing community documentation. About half of my assumptions turned out to be wrong.&lt;/p&gt;

&lt;p&gt;API method names I pulled from older parsers didn't exist in real logs. Game result detection worked differently than documented. A field I expected to be nested inside another was actually its sibling. An entire parser module I wrote had to be deleted because the log event it targeted doesn't appear in real Player.log files. Parser projects from 2019–2022 documented a format that has changed substantially since, and community gists reference API endpoints that were removed years ago.&lt;/p&gt;

&lt;p&gt;This post is what I wish existed when I started. Everything here has been verified against current Arena logs as of early 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log File Location
&lt;/h2&gt;

&lt;p&gt;Arena writes a single log file per session. When you close and reopen the game, the previous session's log is preserved as a backup.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Log Path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%LOCALAPPDATA%Low\Wizards Of The Coast\MTGA\Player.log&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/Library/Logs/Wizards Of The Coast/MTGA/Player.log&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Previous session&lt;/td&gt;
&lt;td&gt;Same directory, &lt;code&gt;Player-prev.log&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prerequisite: You must enable "Detailed Logs (Plugin Support)" in Options &amp;gt; Account. Without this, the log contains only basic diagnostic output and none of the JSON game data that parsers need. This is the most common reason new users can't get companion tools working.&lt;/p&gt;

&lt;p&gt;The log file used to be called &lt;code&gt;output_log.txt&lt;/code&gt; and lived in a different location (renamed in May 2020). If you're reading older parser code and the paths don't match, that's why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log Structure
&lt;/h2&gt;

&lt;p&gt;The log isn't a clean structured format. It's a mix of plain text diagnostic lines and embedded JSON objects. Game-relevant entries are marked by the &lt;code&gt;[UnityCrossThreadLogger]&lt;/code&gt; header prefix. Other bracketed headers appear in the log (&lt;code&gt;[PhysX]&lt;/code&gt;, &lt;code&gt;[Manifest]&lt;/code&gt;, &lt;code&gt;[TaskLogger]&lt;/code&gt;, etc.) but contain engine diagnostics, not game data.&lt;/p&gt;

&lt;p&gt;A typical sequence looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;UnityCrossThreadLogger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;PM&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;==&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;EventJoin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"EventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"QuickDraft_MKM_20260307"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;UnityCrossThreadLogger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;PM&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"greToClientEvent"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"greToClientMessages"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"GREMessageType_GameStateMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}]}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSON payloads can span multiple lines. Parsing requires accumulating lines between header boundaries and extracting JSON using brace-depth counting (tracking open/close braces while respecting string literals). A new &lt;code&gt;[UnityCrossThreadLogger]&lt;/code&gt; header signals the end of the previous entry.&lt;/p&gt;

&lt;p&gt;Lines before the first &lt;code&gt;[UnityCrossThreadLogger]&lt;/code&gt; header in a file are Unity engine boot diagnostics (Mono initialization, GPU info, assembly loading) and should be skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timestamps
&lt;/h3&gt;

&lt;p&gt;Arena writes timestamps in the user's locale format. This creates a parsing challenge: the log uses multiple date/time formats depending on the player's system settings, plus several machine-oriented formats inside JSON payloads.&lt;/p&gt;

&lt;p&gt;Formats observed in real logs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;US 12-hour&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3/11/2026 6:08:07 PM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;[UnityCrossThreadLogger]&lt;/code&gt; header lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ISO 8601 + Z&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-03-11T22:48:34.663Z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON datetime fields (&lt;code&gt;progressedDateTimeUTC&lt;/code&gt;, &lt;code&gt;_dailyRewardResetTimestamp&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ISO 8601 + offset&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026-03-11T18:25:33.906174-07:00&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSON &lt;code&gt;ServerTime&lt;/code&gt;, &lt;code&gt;lastRemotePing&lt;/code&gt; fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET ticks&lt;/td&gt;
&lt;td&gt;&lt;code&gt;639088754738962389&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ClientToGRE &lt;code&gt;timestamp&lt;/code&gt; fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Epoch milliseconds&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1773278674334&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GRE &lt;code&gt;timestamp&lt;/code&gt; fields&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On European-locale systems, header timestamps likely use a day-first format (e.g., &lt;code&gt;11/03/2026 12:34:56&lt;/code&gt;), but I haven't been able to verify this directly since all my test data comes from a US-locale system.&lt;/p&gt;

&lt;p&gt;This ambiguity is problematic: when the day is 12 or less, &lt;code&gt;3/7/2026&lt;/code&gt; could be March 7 (US) or July 3 (European). There is no reliable way to resolve this from the log alone. My parser uses a US-first convention and accepts the loss for ambiguous dates. When I have EU-based logs to validate with, I am considering updating the parser to take a locale hint to break the tie.&lt;/p&gt;

&lt;p&gt;Timestamps appear after the header prefix on the same line. If a header line has no parseable timestamp, store the event without one. Synthetic timestamps break deduplication and chronological ordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client API Messages
&lt;/h2&gt;

&lt;p&gt;Outside of gameplay, the log records REST-style API calls between the Arena client and Wizards' servers. These handle collection data, events, matchmaking, rank, and inventory.&lt;/p&gt;

&lt;p&gt;This is the section most existing documentation gets wrong. Older parsers describe a &lt;code&gt;Namespace.MethodName&lt;/code&gt; convention with endpoints like &lt;code&gt;PlayerInventory.GetPlayerInventory&lt;/code&gt; and &lt;code&gt;PlayerInventory.GetPlayerCardsV3&lt;/code&gt;. Most of those endpoints were removed in the August 2021 breaking change and never restored.&lt;/p&gt;

&lt;p&gt;Current logs use an arrow-delimited format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;UnityCrossThreadLogger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;==&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;EventJoin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;EventName&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;QuickDraft_MKM_20260307&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;RankGetCombinedRankInfo(e&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;f&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;b&lt;/span&gt;&lt;span class="mi"&gt;4-5678-90&lt;/span&gt;&lt;span class="err"&gt;cd-ef&lt;/span&gt;&lt;span class="mi"&gt;12-34567890&lt;/span&gt;&lt;span class="err"&gt;abcd)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"constructedClass"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Gold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"constructedLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;==&amp;gt;&lt;/code&gt; marks a client request. &lt;code&gt;&amp;lt;==&lt;/code&gt; marks a server response, followed by a UUID in parentheses (matching the request's &lt;code&gt;id&lt;/code&gt; field) and a JSON payload. Note that &lt;code&gt;==&amp;gt;&lt;/code&gt; lines carry the &lt;code&gt;[UnityCrossThreadLogger]&lt;/code&gt; prefix, but &lt;code&gt;&amp;lt;==&lt;/code&gt; lines appear bare with no header prefix. Request payloads often contain string-escaped JSON nested inside a &lt;code&gt;request&lt;/code&gt; field.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Available in 2026
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;What It Contains&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;==&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EventJoin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Player joins an event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;==&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EventClaimPrize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prize claim after an event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;==&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EventEnterPairing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Player enters matchmaking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;==&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RankGetCombinedRankInfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Constructed and Limited rank, tier, level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;==&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;StartHook&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Startup data: inventory (gold, gems, wildcards), deck summaries, card metadata, server time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;==&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;==&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DraftCompleteDraft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Human draft completion (request and response)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;==&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;==&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BotDraftDraftPick&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bot draft pack presentation and pick selection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;StartHook&lt;/code&gt; is the primary startup data delivery mechanism. It arrives as a single large response containing &lt;code&gt;InventoryInfo&lt;/code&gt; (gold, gems, wildcards, vault progress), &lt;code&gt;DeckSummariesV2&lt;/code&gt;, &lt;code&gt;CardMetadataInfo&lt;/code&gt;, and a dozen other fields. Notably, the player's card collection (owned cards) does not appear in any &lt;code&gt;==&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;==&lt;/code&gt; API call in current logs. The old &lt;code&gt;GetPlayerCardsV3&lt;/code&gt; endpoint was removed in August 2021 and was not replaced with an equivalent in this API layer.&lt;/p&gt;

&lt;p&gt;A complete catalog of every API method that can appear in the log doesn't exist. You discover new ones by watching the log during different game activities.&lt;/p&gt;

&lt;h2&gt;
  
  
  GRE Messages
&lt;/h2&gt;

&lt;p&gt;GRE (Game Rules Engine) messages contain the actual game data: every card, zone, phase transition, and player action. This is what makes deck tracking, action logs, and replay viewers possible.&lt;/p&gt;

&lt;p&gt;GRE messages are wrapped in a &lt;code&gt;greToClientEvent&lt;/code&gt; envelope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"greToClientEvent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"greToClientMessages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GREMessageType_GameStateMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"gameStateMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GRE Message Types
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GREMessageType_ConnectResp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Initial connection response with starting game state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GREMessageType_GameStateMessage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full or partial game state update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GREMessageType_QueuedGameStateMessage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Queued state update (same structure as above)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GREMessageType_TimerStateMessage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rope timers and timeout info&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GREMessageType_IntermissionReq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Between-game transition in Bo3 (contains game result)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GREMessageType_SubmitDeckReq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sideboarding prompt between Bo3 games&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GREMessageType_UIMessage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UI-related noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GREMessageType_SetSettingsResp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Settings acknowledgment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Client-to-GRE Messages
&lt;/h3&gt;

&lt;p&gt;Player actions also appear in the log:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ClientMessageType_MulliganResp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep or mulligan decision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ClientMessageType_SelectNResp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Card selection (e.g., discard, scry ordering)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ClientToGREUIMessage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UI interactions (hover, chat). Noise.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Some client-to-GRE payloads contain string-escaped JSON nested inside JSON. Your parser needs to handle both already-parsed objects and string-encoded payloads that need a second deserialization pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  GameStateMessage
&lt;/h2&gt;

&lt;p&gt;This is the core data structure. Each &lt;code&gt;GameStateMessage&lt;/code&gt; contains some or all of these fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gameStateMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gameObjects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"zones"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gameInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"stage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GameStage_Play"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matchState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MatchState_GameInProgress"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"turnInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"turnNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"phase"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Phase_Main1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"activePlayer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"decisionPlayer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"annotations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"diffDeletedInstanceIds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;turnInfo&lt;/code&gt; is a sibling of &lt;code&gt;gameInfo&lt;/code&gt;, not nested inside it. I initially had it as &lt;code&gt;gameStateMessage.gameInfo.turnInfo&lt;/code&gt;, which returned null every time. This is the kind of structural assumption that only breaks when you test against real data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Fields
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;gameObjects&lt;/code&gt;&lt;/strong&gt;: Every card and permanent in the game. Each object has an &lt;code&gt;instanceId&lt;/code&gt;, &lt;code&gt;grpId&lt;/code&gt; (the card's global ID), &lt;code&gt;zoneId&lt;/code&gt;, &lt;code&gt;ownerSeatId&lt;/code&gt;, &lt;code&gt;controllerSeatId&lt;/code&gt;, &lt;code&gt;visibility&lt;/code&gt;, &lt;code&gt;cardTypes&lt;/code&gt;, &lt;code&gt;subtypes&lt;/code&gt;, &lt;code&gt;power&lt;/code&gt;, &lt;code&gt;toughness&lt;/code&gt;, and more. The &lt;code&gt;name&lt;/code&gt; field is a numeric ID, not a human-readable string; card names require a separate database lookup. Tracking zone transitions of game objects is how you build a deck tracker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;zones&lt;/code&gt;&lt;/strong&gt;: Library, hand, battlefield, graveyard, exile, stack, and others. Each zone has a &lt;code&gt;zoneId&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt; (e.g., &lt;code&gt;ZoneType_Hand&lt;/code&gt;), &lt;code&gt;ownerSeatId&lt;/code&gt;, and &lt;code&gt;objectInstanceIds&lt;/code&gt; listing what's in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;annotations&lt;/code&gt;&lt;/strong&gt;: Records of game actions: zone transfers, damage, counters, life total changes. The format is less intuitive than you'd expect (see next section).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;timers&lt;/code&gt;&lt;/strong&gt;: Rope timer state, timeout counts, priority timing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;diffDeletedInstanceIds&lt;/code&gt;&lt;/strong&gt;: Instance IDs that should be purged from your local game state. When Arena sends incremental updates (not full state snapshots), this field tells you what no longer exists. If you don't process this, your tracker will show phantom cards.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full State vs. Incremental Updates
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;GameStateMessage&lt;/code&gt; can be a full state snapshot or a partial delta containing only what changed. There's no explicit flag that distinguishes them. In practice, early messages in a game (especially from &lt;code&gt;ConnectResp&lt;/code&gt;) tend to be full snapshots, and subsequent messages tend to be deltas. Your parser needs to merge incoming fields into a running game state and remove anything listed in &lt;code&gt;diffDeletedInstanceIds&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Annotations
&lt;/h2&gt;

&lt;p&gt;Annotations record game actions within a &lt;code&gt;GameStateMessage&lt;/code&gt;. Their format is worth calling out because it's less intuitive than the rest of the GRE schema.&lt;/p&gt;

&lt;p&gt;You might expect named wrapper objects with camelCase fields. What Arena actually writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"affectorId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;312&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"affectedIds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;455&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"AnnotationType_ZoneTransfer"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zone_src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"KeyValuePairValueType_int32"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"valueInt32"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zone_dest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"KeyValuePairValueType_int32"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"valueInt32"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"KeyValuePairValueType_string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"valueString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"PlayLand"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two surprises here. First, &lt;code&gt;type&lt;/code&gt; is an &lt;strong&gt;array of strings&lt;/strong&gt;, not a plain string. In every annotation I've seen in current logs, it's a single-element array, but the array wrapper is always present.&lt;/p&gt;

&lt;p&gt;Second, the data lives in a uniform &lt;code&gt;details&lt;/code&gt; array of key-value pairs, each with a typed value field (&lt;code&gt;valueInt32&lt;/code&gt;, &lt;code&gt;valueString&lt;/code&gt;, etc.). The key names use &lt;code&gt;snake_case&lt;/code&gt; (&lt;code&gt;zone_src&lt;/code&gt;, &lt;code&gt;orig_id&lt;/code&gt;, &lt;code&gt;new_id&lt;/code&gt;), not camelCase. You need helper functions to search the details array by key name.&lt;/p&gt;

&lt;p&gt;Annotation types include &lt;code&gt;AnnotationType_ZoneTransfer&lt;/code&gt;, &lt;code&gt;AnnotationType_ObjectIdChanged&lt;/code&gt;, &lt;code&gt;AnnotationType_ResolutionComplete&lt;/code&gt;, &lt;code&gt;AnnotationType_DamageDealt&lt;/code&gt;, &lt;code&gt;AnnotationType_ModifiedLife&lt;/code&gt;, &lt;code&gt;AnnotationType_CounterAdded&lt;/code&gt;, &lt;code&gt;AnnotationType_PhaseOrStepModified&lt;/code&gt;, &lt;code&gt;AnnotationType_TappedUntappedPermanent&lt;/code&gt;, and others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Message Batching
&lt;/h2&gt;

&lt;p&gt;This is the implementation detail I most wish someone had told me upfront.&lt;/p&gt;

&lt;p&gt;Arena frequently batches multiple &lt;code&gt;GameStateMessage&lt;/code&gt; values into a single &lt;code&gt;greToClientMessages&lt;/code&gt; array. In my testing across multiple play sessions, over half of all GRE events that contain game state data have two or more &lt;code&gt;GameStateMessage&lt;/code&gt; entries bundled together. A single log entry might contain three or four game state updates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"greToClientEvent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"greToClientMessages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GREMessageType_GameStateMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"gameStateMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GREMessageType_GameStateMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"gameStateMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GREMessageType_QueuedGameStateMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"gameStateMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use a find-first approach to extract the &lt;code&gt;GameStateMessage&lt;/code&gt; from this array, you silently discard the rest. In my case, the parser was missing turn changes, creature deaths, and annotation data until I refactored it to iterate every message in the batch. This was the source of most of my "why is the tracker missing data" bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Match Lifecycle
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Match Start
&lt;/h3&gt;

&lt;p&gt;Match boundaries come from &lt;code&gt;matchGameRoomStateChangedEvent&lt;/code&gt; JSON entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"matchGameRoomStateChangedEvent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gameRoomInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"stateType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MatchGameRoomStateType_Playing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"gameRoomConfig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matchId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Constructed_BestOf1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"reservedPlayers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"systemSeatId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"playerName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"systemSeatId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"playerName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you the match ID, event type, and both players' seat assignments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Game Result
&lt;/h3&gt;

&lt;p&gt;Game results are embedded in GRE &lt;code&gt;GameStateMessage&lt;/code&gt; payloads, not in a separate event type. When a game ends, the &lt;code&gt;gameInfo&lt;/code&gt; field inside a &lt;code&gt;GameStateMessage&lt;/code&gt; transitions to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GameStage_GameOver"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"matchState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MatchState_GameComplete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MatchScope_Game"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ResultType_WinLoss"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"winningTeamId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ResultReason_Game"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a catch: Arena sends &lt;strong&gt;two&lt;/strong&gt; &lt;code&gt;GameStage_GameOver&lt;/code&gt; messages per game end. The first has &lt;code&gt;matchState: "MatchState_GameComplete"&lt;/code&gt; (game-scope result). The second has &lt;code&gt;matchState: "MatchState_MatchComplete"&lt;/code&gt; (match-scope result, which also includes both &lt;code&gt;MatchScope_Game&lt;/code&gt; and &lt;code&gt;MatchScope_Match&lt;/code&gt; entries). If you emit a game result event for both, you get duplicates. Filter on &lt;code&gt;MatchState_GameComplete&lt;/code&gt; and skip &lt;code&gt;MatchState_MatchComplete&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I initially built a separate game result parser around &lt;code&gt;LogBusinessEvents&lt;/code&gt; entries with a &lt;code&gt;WinningType&lt;/code&gt; field, based on documentation from older parsers. That event never appeared in any real log I tested. The entire module had to be deleted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Match Complete
&lt;/h3&gt;

&lt;p&gt;Match completion uses the same &lt;code&gt;matchGameRoomStateChangedEvent&lt;/code&gt; structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"matchGameRoomStateChangedEvent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gameRoomInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"stateType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MatchGameRoomStateType_MatchCompleted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"finalMatchResult"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matchId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matchCompletedReason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MatchCompletedReasonType_Success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resultList"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MatchScope_Game"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ResultType_WinLoss"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"winningTeamId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MatchScope_Match"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ResultType_WinLoss"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"winningTeamId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Bo3 matches, &lt;code&gt;resultList&lt;/code&gt; contains one &lt;code&gt;MatchScope_Game&lt;/code&gt; entry per game played, plus a final &lt;code&gt;MatchScope_Match&lt;/code&gt; entry for the overall result. In a 2-0 match, for example, the list contains three entries: two game-scope results and one match-scope result. Between games, the GRE sends &lt;code&gt;GREMessageType_IntermissionReq&lt;/code&gt; (containing the game result) followed by &lt;code&gt;GREMessageType_SubmitDeckReq&lt;/code&gt; for sideboarding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Draft Events
&lt;/h2&gt;

&lt;p&gt;Draft parsing requires handling two completely different log formats depending on the draft type.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bot Drafts (Quick Draft)
&lt;/h3&gt;

&lt;p&gt;Bot draft messages use a &lt;code&gt;CurrentModule&lt;/code&gt;/&lt;code&gt;Payload&lt;/code&gt; envelope, where the payload is string-escaped JSON. Pack presentation arrives as a &lt;code&gt;BotDraftDraftPick&lt;/code&gt; response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CurrentModule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BotDraft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Result&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Success&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;EventName&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;QuickDraft_ECL_20260223&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;DraftStatus&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;PickNext&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;PackNumber&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:0,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;PickNumber&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:0,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;NumCardsToPick&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:1,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;DraftPack&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;98361&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;98498&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;98358&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,...],&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;PickedCards&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[...]}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;DraftPack&lt;/code&gt; is an array of &lt;strong&gt;strings&lt;/strong&gt;, not integers. Same for &lt;code&gt;PickedCards&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Pick selection is sent as a &lt;code&gt;==&amp;gt; BotDraftDraftPick&lt;/code&gt; request (no underscore) with &lt;code&gt;PickInfo&lt;/code&gt; containing the chosen &lt;code&gt;CardIds&lt;/code&gt; (plural, an array), &lt;code&gt;PackNumber&lt;/code&gt;, and &lt;code&gt;PickNumber&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;==&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;BotDraftDraftPick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;EventName&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;QuickDraft_ECL_20260223&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;PickInfo&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;EventName&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;QuickDraft_ECL_20260223&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;CardIds&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;98546&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;],&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;PackNumber&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:0,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;PickNumber&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:0}}"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Draft completion for bot drafts does &lt;strong&gt;not&lt;/strong&gt; use &lt;code&gt;DraftCompleteDraft&lt;/code&gt;. Instead, the final &lt;code&gt;BotDraftDraftPick&lt;/code&gt; response returns &lt;code&gt;"DraftStatus":"Completed"&lt;/code&gt; with a full &lt;code&gt;PickedCards&lt;/code&gt; array and an empty &lt;code&gt;DraftPack&lt;/code&gt;. The response also includes &lt;code&gt;DTO_InventoryInfo&lt;/code&gt; with card grant details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Human Drafts (Premier/Traditional)
&lt;/h3&gt;

&lt;p&gt;Pack presentation uses a &lt;code&gt;Draft.Notify&lt;/code&gt; entry with &lt;code&gt;draftId&lt;/code&gt;, &lt;code&gt;SelfPack&lt;/code&gt;, &lt;code&gt;SelfPick&lt;/code&gt;, and &lt;code&gt;PackCards&lt;/code&gt; (a comma-separated string of card IDs, not an array). One caveat: the very first pick of pack 1 does not generate a &lt;code&gt;Draft.Notify&lt;/code&gt;. The first one appears at &lt;code&gt;SelfPick:2&lt;/code&gt;. New packs (pack 2 and pack 3) do generate &lt;code&gt;Draft.Notify&lt;/code&gt; for their first pick.&lt;/p&gt;

&lt;p&gt;Pick selection arrives as &lt;code&gt;EventPlayerDraftMakePick&lt;/code&gt; with &lt;code&gt;DraftId&lt;/code&gt;, &lt;code&gt;GrpIds&lt;/code&gt; (an array of selected card IDs), &lt;code&gt;Pack&lt;/code&gt;, and &lt;code&gt;Pick&lt;/code&gt; numbers. In formats that allow picking multiple cards (like Pick Two Draft), &lt;code&gt;GrpIds&lt;/code&gt; contains more than one entry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Draft Completion
&lt;/h3&gt;

&lt;p&gt;Human drafts emit a &lt;code&gt;DraftCompleteDraft&lt;/code&gt; entry when the draft finishes. Note the exact name: no underscore between "Draft" and "Complete." The request contains &lt;code&gt;EventName&lt;/code&gt; and &lt;code&gt;IsBotDraft: false&lt;/code&gt; in string-escaped JSON. The response includes &lt;code&gt;CourseId&lt;/code&gt;, &lt;code&gt;InternalEventName&lt;/code&gt;, and &lt;code&gt;CardPool&lt;/code&gt; (an array of integer card IDs — the complete pool of drafted cards).&lt;/p&gt;

&lt;p&gt;Bot drafts (QuickDraft) do &lt;strong&gt;not&lt;/strong&gt; emit &lt;code&gt;DraftCompleteDraft&lt;/code&gt;. Completion is signaled by the final &lt;code&gt;BotDraftDraftPick&lt;/code&gt; response with &lt;code&gt;DraftStatus: "Completed"&lt;/code&gt; (see above).&lt;/p&gt;

&lt;h2&gt;
  
  
  Session Events
&lt;/h2&gt;

&lt;p&gt;A few log events track the player session:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signature&lt;/th&gt;
&lt;th&gt;What It Contains&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;authenticateResponse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Login confirmation with &lt;code&gt;clientId&lt;/code&gt;, &lt;code&gt;sessionId&lt;/code&gt;, and &lt;code&gt;screenName&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FrontDoorConnection.Close&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Logout or disconnect (includes reason, e.g., &lt;code&gt;"OnDestroy"&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;authenticateResponse&lt;/code&gt; is the primary source of player identity. It appears at session start and again on each match server reconnection. Identity data also appears in &lt;code&gt;matchGameRoomStateChangedEvent&lt;/code&gt;, where the &lt;code&gt;reservedPlayers&lt;/code&gt; array includes &lt;code&gt;userId&lt;/code&gt; and &lt;code&gt;playerName&lt;/code&gt; for both players in a match.&lt;/p&gt;

&lt;p&gt;If you're building a tool that processes this data, strip or hash identity fields before anything leaves the user's machine. WotC has been moving in this direction themselves: screen names were removed from most log entries in July 2021, and opponent display name tags were removed in July 2024 (see the breaking changes timeline below). Treat the remaining identity fields as data you have access to, not data you're entitled to store or transmit.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the Parser
&lt;/h2&gt;

&lt;p&gt;The parser described in this post is &lt;a href="https://github.com/manasight/manasight-parser" rel="noopener noreferrer"&gt;open source on GitHub&lt;/a&gt; (Rust, MIT/Apache-2.0). I'm using it to build a desktop overlay for Arena. If you're working on Arena tooling, I'd love to hear what you're building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Breaking Changes
&lt;/h3&gt;

&lt;p&gt;Fair warning: the log format is unstable and has broken without notice before.&lt;/p&gt;

&lt;p&gt;In the 2021.8.0.3855 update, Wizards removed several log endpoints without warning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PlayerInventory.GetPlayerInventory&lt;/code&gt;: removed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PlayerInventory.GetPlayerCardsV3&lt;/code&gt;: removed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Inventory.Updated&lt;/code&gt;: removed&lt;/li&gt;
&lt;li&gt;Several draft-related endpoints: removed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Collection tracking, inventory updates, and draft pick logging all broke overnight. A &lt;a href="https://web.archive.org/web/20251109061807/https://feedback.wizards.com/forums/918667-mtg-arena-bugs-product-suggestions/suggestions/44050746-broken-logs-in-2021-8-0-3855" rel="noopener noreferrer"&gt;community feedback thread&lt;/a&gt; collected over 700 votes. Some data eventually reappeared through different endpoints (inventory now comes through &lt;code&gt;StartHook&lt;/code&gt;), but the format changed and old parsers needed rewrites. Card collection data (&lt;code&gt;GetPlayerCardsV3&lt;/code&gt;) was not replaced with an equivalent in the current API layer.&lt;/p&gt;

&lt;p&gt;That wasn't an isolated incident. A timeline of data removals:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;What Changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sept 2019&lt;/td&gt;
&lt;td&gt;Vault progress info removed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 2020&lt;/td&gt;
&lt;td&gt;Log file renamed from &lt;code&gt;output_log.txt&lt;/code&gt; to &lt;code&gt;Player.log&lt;/code&gt;, path changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;July 2021&lt;/td&gt;
&lt;td&gt;Screen name removed from most log entries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 2021&lt;/td&gt;
&lt;td&gt;Collection, inventory, and draft endpoints removed (partially restored later in new format)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aug 2022&lt;/td&gt;
&lt;td&gt;MMR/rating data removed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;July 2024&lt;/td&gt;
&lt;td&gt;Opponent display name tag removed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trend is obvious. Wizards has been reducing the data available in the log over time, not expanding it. If you're building a tool that depends on the log, design for resilience. Assume any field can disappear in the next patch. Anything your parser extracts today might not be there tomorrow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open-Source Parsers
&lt;/h3&gt;

&lt;p&gt;If you're building something, start by reading existing implementations. Status as of early 2026:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/manasight/manasight-parser" rel="noopener noreferrer"&gt;manasight-parser&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Rust · Active&lt;/strong&gt;&lt;br&gt;
My project. Verified against current logs. Full GRE, client API, draft, and match lifecycle parsing. MIT/Apache-2.0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/rconroy293/mtga-log-client" rel="noopener noreferrer"&gt;rconroy293/mtga-log-client&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Python · Active&lt;/strong&gt;&lt;br&gt;
17Lands' official client. Clean, focused on draft and game event upload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/gathering-gg/parser" rel="noopener noreferrer"&gt;gathering-gg/parser&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Go · Unmaintained (2019)&lt;/strong&gt;&lt;br&gt;
Comprehensively typed. Good for understanding message structures, but the format has changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/mtgatool/mtgatool-desktop" rel="noopener noreferrer"&gt;mtgatool/mtgatool-desktop&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;TypeScript · Low activity (last release Oct 2024)&lt;/strong&gt;&lt;br&gt;
Full Electron app with log watcher and message dispatcher.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/mtgatracker/mtgatracker" rel="noopener noreferrer"&gt;mtgatracker/mtgatracker&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Python · Unmaintained (2020)&lt;/strong&gt;&lt;br&gt;
Websocket-based overlay. Historical reference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Razviar/mtgap" rel="noopener noreferrer"&gt;Razviar/mtgap&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;TypeScript · Archived (April 2025)&lt;/strong&gt;&lt;br&gt;
MTGA Pro Tracker. Rewrote its parser multiple times as the format evolved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/riQQ/MtgaProto" rel="noopener noreferrer"&gt;riQQ/MtgaProto&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Protobuf · Updated periodically&lt;/strong&gt;&lt;br&gt;
Extracted &lt;code&gt;.proto&lt;/code&gt; definitions from Arena's installation files. Reference, not a parser.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Manasight is not affiliated with, endorsed by, or sponsored by Wizards of the Coast or Hasbro.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Have corrections or additions? I'd love to hear from other developers working in this space.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>gamedev</category>
      <category>buildinpublic</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why I Chose Tauri v2 for a Desktop Overlay in 2026</title>
      <dc:creator>manasightgg</dc:creator>
      <pubDate>Mon, 09 Mar 2026 03:54:18 +0000</pubDate>
      <link>https://forem.com/manasightgg/why-i-chose-tauri-v2-for-a-desktop-overlay-in-2026-597h</link>
      <guid>https://forem.com/manasightgg/why-i-chose-tauri-v2-for-a-desktop-overlay-in-2026-597h</guid>
      <description>&lt;p&gt;&lt;em&gt;How a prototype spike validated a framework decision on real hardware.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Building an overlay that sits on top of a fullscreen game sounds straightforward until you start listing requirements. Transparent background. Always on top. Click-through on empty space. Rich UI with styled lists and icons. Windows and Mac. Low CPU and RAM overhead.&lt;/p&gt;

&lt;p&gt;The list of frameworks that can do all of that is short.&lt;/p&gt;

&lt;p&gt;My project is &lt;a href="https://blog.manasight.gg/why-im-building-an-mtg-arena-companion-from-scratch/" rel="noopener noreferrer"&gt;Manasight&lt;/a&gt;, a companion overlay for Magic: The Gathering Arena. I'd committed to Windows and Mac from day one but hadn't picked the framework yet. This post is the story of how I chose one, what scared me about it, and how I tested whether those fears were justified.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an overlay requires
&lt;/h2&gt;

&lt;p&gt;Manasight's overlay needs to do a few things that most desktop apps don't. It has to render transparent panels on top of a running game, stay visible at all times, and let mouse clicks pass through to the game on transparent areas while capturing clicks on the UI panels themselves.&lt;/p&gt;

&lt;p&gt;That last requirement, selective click-through, eliminates most options immediately.&lt;/p&gt;

&lt;p&gt;The overlay also isn't just a technical exercise. It needs to look like a modern app: styled card lists, a scrollable event log, opacity controls, responsive layout that adapts to panel resizing. Users expect the polish of a web app, not the look of native system controls.&lt;/p&gt;

&lt;p&gt;So the framework needs transparent always-on-top windows &lt;em&gt;and&lt;/em&gt; rich UI rendering.&lt;br&gt;
That means HTML/CSS in an overlay, which narrows the field fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ruling out options
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Electron&lt;/strong&gt; was the first to go. Bundling Chromium means 200-300 MB of memory before the app does anything. For a tool that runs alongside a game, that overhead matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overwolf&lt;/strong&gt; is Windows-only and takes up to a 30% revenue cut. &lt;strong&gt;Flutter Desktop&lt;/strong&gt; has no selective click-through support. &lt;strong&gt;Qt/QML&lt;/strong&gt; has had recurring macOS transparency issues since Qt 5.15 and comes with complex LGPL licensing.&lt;/p&gt;

&lt;p&gt;That left two real options: &lt;strong&gt;Tauri v2&lt;/strong&gt; or &lt;strong&gt;going fully native&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tauri vs. native
&lt;/h2&gt;

&lt;p&gt;I have years of C++ and Win32/Direct2D experience (I even worked on the Microsoft team that originally built Direct2D nearly 20 years ago). Building the Windows overlay natively was on the table. Native gives you first-class selective click-through on both platforms (&lt;code&gt;WM_NCHITTEST&lt;/code&gt; on Windows, &lt;code&gt;hitTest&lt;/code&gt; on macOS) and the lowest possible resource footprint.&lt;/p&gt;

&lt;p&gt;But my experience also told me it would be painful. Going fully native means two separate rendering implementations. Direct2D on Windows, Core Animation on macOS. Manual pixel-level layout to achieve what a few lines of CSS give you for free: flexbox, web fonts, scrollable containers, hover states. No hot reload, no browser dev tools. I estimated weeks of additional UI work just to get something that would look worse than HTML/CSS.&lt;/p&gt;

&lt;p&gt;macOS was a blank slate for me -- I have never built a macOS app in my life. Building the same UI twice in two rendering frameworks, as one person, would at least double the implementation timeline. That's plenty of time for a side project to lose momentum.&lt;/p&gt;

&lt;p&gt;Tauri avoids that trade-off. The backend is Rust, so system-level code like log parsing, file watching, and cursor polling runs at native speed. The UI is HTML/CSS rendered in the platform's built-in webview. You're not choosing between Rust and "not Rust." You're choosing where Rust does the work.&lt;/p&gt;

&lt;p&gt;Tauri also handles distribution: a built-in auto-updater with Ed25519 signature verification, MSI/NSIS installer generation for Windows, DMG generation for macOS, and code signing for both platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I knew I was giving up
&lt;/h2&gt;

&lt;p&gt;Tauri won on development speed and single-codebase economics, but it came with known costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No native click-through. Tauri doesn't expose per-region hit-testing. The workaround: a Rust loop polls cursor position at ~60fps and toggles &lt;code&gt;setIgnoreCursorEvents&lt;/code&gt; based on whether the cursor is over a UI panel or transparent space. It sounds expensive, but it's a tight compiled loop, not JavaScript.&lt;/li&gt;
&lt;li&gt;macOS transparency bugs. There were three open issues in Tauri's tracker. E.g., transparent windows reportedly worked in development but rendered with an opaque background in production &lt;code&gt;.app&lt;/code&gt; bundles. If true, that would make the overlay unusable on Mac.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;macOSPrivateApi: true&lt;/code&gt;. Required for transparency, and it permanently disqualifies the Mac App Store. It's acceptable for Manasight (distributed via DMG), but a real constraint.&lt;/li&gt;
&lt;li&gt;WebView differences. WebView2 (Chromium-based) on Windows, WebKit on macOS. Minor rendering inconsistencies are expected.&lt;/li&gt;
&lt;li&gt;Framework dependency. Tied to Tauri's project health and release cadence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Tauri didn't pan out, I had a fallback plan: build native window shells with embedded WebView2/WKWebView, keeping the HTML/CSS rendering advantage while gaining full native window control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prototype spike
&lt;/h2&gt;

&lt;p&gt;I used Claude Code to research Tauri's GitHub issue tracker, dig through platform-specific overlay mechanics (Win32 &lt;code&gt;WM_NCHITTEST&lt;/code&gt; behavior, AppKit &lt;code&gt;hitTest&lt;/code&gt; overrides), and study how existing tools in this space implement their overlays. That research surfaced six specific risks, some of which looked like potential hard blockers. The question was whether they were real on actual hardware.&lt;/p&gt;

&lt;p&gt;Rather than assume, I built a validation spike: I opened 20 GitHub issues covering 5 risk areas including transparency, click-through, focus management, multi-monitor behavior, and performance. Testing covered five OS versions: Windows 10, Windows 11, macOS Sonoma, macOS Sequoia, and macOS Tahoe. If the overlay couldn't pass validation on all target platforms, the project would fall back to native with embedded WebView.&lt;/p&gt;

&lt;p&gt;This wasn't a "move fast and see what happens" prototype. It was a risk-reduction exercise with a solid plan B.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing on real hardware
&lt;/h2&gt;

&lt;p&gt;Claude generated the prototype code. GitHub Actions built the installers for Windows and Mac. For macOS testing, I bought an M2 Mac Mini specifically because it could run all three recent macOS versions (Sonoma, Sequoia, and Tahoe). It was the cheapest way to get real hardware coverage across OS generations.&lt;/p&gt;

&lt;p&gt;That purchase says something about how seriously I take Mac support. I bought hardware before writing a line of product code.&lt;/p&gt;

&lt;p&gt;Claude was one of the first things I installed on each OS. It walked me through a 30+ test case protocol on every platform, giving me step-by-step instructions, gathering resource metrics automatically, and running installers directly from CI artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What didn't break
&lt;/h2&gt;

&lt;p&gt;Six risks from the research simply didn't reproduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transparency lost in production builds. Worked fine on all three macOS versions.&lt;/li&gt;
&lt;li&gt;Sonoma visual glitches after focus change. Didn't happen.&lt;/li&gt;
&lt;li&gt;Ghost titlebar artifact on Windows. Didn't happen.&lt;/li&gt;
&lt;li&gt;Taskbar z-order issues. Didn't happen.&lt;/li&gt;
&lt;li&gt;Shadow artifacts with transparency. Didn't happen.&lt;/li&gt;
&lt;li&gt;Monthly Screen Recording re-authorization on Sequoia. Never triggered because the app doesn't need the permission at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI-assisted research identified these risks. AI-assisted prototyping proved most of them weren't real.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did break
&lt;/h2&gt;

&lt;p&gt;macOS being macOS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overlay invisible over fullscreen apps. macOS isolates fullscreen apps from other windows by default. I fixed this with &lt;a href="https://github.com/ahkohd/tauri-nspanel" rel="noopener noreferrer"&gt;&lt;code&gt;tauri-nspanel&lt;/code&gt;&lt;/a&gt;, a third-party plugin that converts the window to an NSPanel that macOS allows to float above fullscreen apps.&lt;/li&gt;
&lt;li&gt;App appearing in Dock and Cmd+Tab. I needed to set &lt;code&gt;ActivationPolicy::Accessory&lt;/code&gt; to hide the overlay from the app switcher.&lt;/li&gt;
&lt;li&gt;CSS &lt;code&gt;backdrop-filter: blur()&lt;/code&gt; broken with transparency. Cosmetic only, but means frosted-glass effects aren't available on the overlay.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And some problems nobody warned about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Windows overlay stealing focus on click. Clicking a UI panel pulled focus from the application underneath. Required platform-specific window flag adjustments.&lt;/li&gt;
&lt;li&gt;macOS Tahoe using ~4x the RAM of Sequoia. 110 MB vs. 29 MB on identical hardware, running identical code. An OS-level webview regression, not something the application can control.&lt;/li&gt;
&lt;li&gt;NSIS installer not bootstrapping WebView2 on Windows 10. WebView2 doesn't ship with Win10. During testing Edge provided it, but on a clean Win10 machine without Edge the app would fail to launch. Fix: a single config line to bundle the bootstrapper.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance numbers
&lt;/h2&gt;

&lt;p&gt;On Windows 11: &lt;strong&gt;14 MB RAM and under 1% CPU.&lt;/strong&gt; Tauri uses the system webview instead of bundling Chromium, and the backend is compiled Rust with no garbage collector, no runtime, no interpreter. The only overhead is a webview rendering HTML panels.&lt;/p&gt;

&lt;p&gt;macOS varied by version:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;macOS Version&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sequoia&lt;/td&gt;
&lt;td&gt;20-29 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonoma&lt;/td&gt;
&lt;td&gt;66 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tahoe&lt;/td&gt;
&lt;td&gt;110 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All within the 200 MB RAM and 15% CPU upper bounds I'd set. Active CPU was borderline on Tahoe (15.6%) but acceptable. The overlay runs alongside a game without the player noticing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision validated
&lt;/h2&gt;

&lt;p&gt;Most risks didn't reproduce. The ones that did had workarounds. Nothing was a hard blocker. The native fallback still exists if something changes in a future Tauri release or macOS update.&lt;/p&gt;

&lt;p&gt;Tauri earned the "proceed" verdict through testing, not faith. &lt;/p&gt;

&lt;p&gt;The prototype is now complete. Product code is underway, and the framework decision is validated with real numbers on real hardware. That's the kind of foundation I want to build on.&lt;/p&gt;

&lt;p&gt;Now there's evidence, not just a belief.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Tim -- I'm building Manasight, an MTG Arena companion tool, solo with Rust and Tauri. Follow &lt;a href="https://x.com/manasightgg" rel="noopener noreferrer"&gt;@manasightgg&lt;/a&gt; on Twitter or read the full blog at &lt;a href="https://blog.manasight.gg" rel="noopener noreferrer"&gt;blog.manasight.gg&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Manasight is not affiliated with, endorsed by, or sponsored by Wizards of the Coast or Hasbro.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tauri</category>
      <category>rust</category>
      <category>buildinpublic</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
