<?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>DEV Community: Grigoriu Lorm</title>
    <description>The latest articles on DEV Community by Grigoriu Lorm (@aionon).</description>
    <link>https://dev.to/aionon</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4031024%2F89c5886c-4b84-4a79-b343-e6cb2e33838d.jpg</url>
      <title>DEV Community: Grigoriu Lorm</title>
      <link>https://dev.to/aionon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aionon"/>
    <language>en</language>
    <item>
      <title>Reverse-engineering an MMO Aion 2's network protocol to build a real-time DPS meter (Rust + Tauri)</title>
      <dc:creator>Grigoriu Lorm</dc:creator>
      <pubDate>Wed, 15 Jul 2026 21:04:15 +0000</pubDate>
      <link>https://dev.to/aionon/reverse-engineering-an-mmo-aion-2s-network-protocol-to-build-a-real-time-dps-meter-rust-tauri-3157</link>
      <guid>https://dev.to/aionon/reverse-engineering-an-mmo-aion-2s-network-protocol-to-build-a-real-time-dps-meter-rust-tauri-3157</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Disclosure: this is a write-up about my own side project — a combat analytics tool for AION 2. No affiliation with the game's publisher. Links at the end.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnkqftzgjlacwbus17gj6.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnkqftzgjlacwbus17gj6.jpg" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;A Windows desktop app: &lt;strong&gt;Rust backend + Tauri v2 webview UI&lt;/strong&gt;. It passively captures the game's TCP traffic (npcap), reassembles streams, parses the game's undocumented binary protocol, feeds a combat model (damage, heals, buffs, deaths, boss detection), and pushes aggregates to a small JS frontend. Nothing touches the game client — no injection, no memory reading. If the packet didn't say it, we don't know it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pain #1: the protocol is a moving target
&lt;/h2&gt;

&lt;p&gt;Nobody hands you a spec. The protocol is varint-heavy, partially compressed, and changes with game patches. You end up doing packet archaeology: capture a fight, stare at hex dumps, correlate "I pressed this skill at 19:32:04" with byte patterns, build a parser, and then — the fun part — &lt;strong&gt;keep it alive after every patch&lt;/strong&gt;, usually reverse-engineering the diff within hours because your users' raids are tonight, not next week.&lt;/p&gt;

&lt;p&gt;One hard-won lesson: log &lt;em&gt;everything&lt;/em&gt; behind toggleable trace categories. Our tracing setup keeps hot-path log callsites at literally zero cost when disabled (Rust &lt;code&gt;tracing&lt;/code&gt; with &lt;code&gt;Interest::never()&lt;/code&gt; + atomic per-category flags), so a user can flip a "Trace: Packets" checkbox, reproduce a bug, and send a log that actually contains the bytes we need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pain #2: entity identity is a lie
&lt;/h2&gt;

&lt;p&gt;The single hardest correctness problem wasn't parsing — it was &lt;em&gt;identity&lt;/em&gt;. A player is not one ID:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your character has a stable "owner" entity that carries your buff bar,&lt;/li&gt;
&lt;li&gt;your &lt;strong&gt;damage&lt;/strong&gt; lands under a transient combat entity whose ID changes between pulls,&lt;/li&gt;
&lt;li&gt;leave the dungeon and the game re-binds you to a brand-new ID,&lt;/li&gt;
&lt;li&gt;names arrive from different packets than damage, sometimes seconds later, sometimes never (mid-fight app start).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get any of this wrong and a healer's healing lands on a ghost row, or a player's buffs vanish from the saved fight because their ID was recycled 7 minutes after the boss died (yes, we hit exactly that bug — the post-dungeon re-bind silently  rewrote already-saved fight records with degraded data). The fix is a canonicalization layer: every ID is folded through summon-resolution → orphan remapping → nickname → canonical ID, &lt;em&gt;and&lt;/em&gt; every storage migration (damage, buffs, heals, Gear score) has to move together when an ID re-binds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pain #3: raw DPS is unfair, so we built rDPS
&lt;/h2&gt;

&lt;p&gt;Raw damage numbers make support classes invisible. If a buffer's party-wide buff amplifies everyone's damage by 12%, who "earned" that damage? We implemented an attribution model we call &lt;strong&gt;rDPS&lt;/strong&gt;: damage added by your buffs/debuffs is credited to &lt;em&gt;you&lt;/em&gt; and debited from the players who merely received it (&lt;code&gt;rDPS = your damage − received amp + provided amp&lt;/code&gt;). Details that make it hard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;non-stacking debuffs maintained by two same-class players must be &lt;strong&gt;split by uptime&lt;/strong&gt;, not winner-take-all;&lt;/li&gt;
&lt;li&gt;some debuffs arrive as &lt;em&gt;self-buffs on the caster&lt;/em&gt; instead of visible debuffs on the boss;&lt;/li&gt;
&lt;li&gt;buff level is encoded in the skill variant ID's middle digits — when it's encoded at all (passives: nope).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pain #4: healing is mostly overheal
&lt;/h2&gt;

&lt;p&gt;Heal packets tell you the cast amount, not what mattered. We confirm each heal against the target's actual HP delta (FIFO matching within a 3s window): the covered part is &lt;em&gt;effective healing&lt;/em&gt;, the rest is overheal and earns no credit. Suddenly healer rankings mean something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus frontend pain, because there's always one
&lt;/h2&gt;

&lt;p&gt;CSS &lt;code&gt;text-overflow: ellipsis&lt;/code&gt; clips text to whole glyphs but keeps the full flex width on the box — with CJK nicknames that leaves a dead zone of up to one glyph (~16px) between the "…" and the next element. Looks like a random hole in the layout. The fix: measure and truncate the &lt;em&gt;string&lt;/em&gt; in JS via canvas &lt;code&gt;measureText&lt;/code&gt;, so the box hugs the visible text. Two hours of my life for 16 pixels.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like today
&lt;/h2&gt;

&lt;p&gt;The meter feeds a community leaderboard site where verified boss kills are uploaded and ranked per class/server with an actual snapshot of FULL player's build: skills, specs, gear, titles, etc... The stack has survived several game patches, supports both regional game versions and 10 UI languages.&lt;/p&gt;

&lt;p&gt;If you're curious: the meter and leaderboard live at &lt;a href="https://aion2t.com" rel="noopener noreferrer"&gt;aion2t.com&lt;/a&gt;. Happy to answer questions about packet reverse-engineering, Tauri v2 quirks, or the rDPS model in the comments. As far as AI are concerned I used: Claude Code, Mimo, Deepseek and a little bit of Grok.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
