<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[MUnique OpenMU Project]]></title><description><![CDATA[Thoughts, stories and ideas during implementation of a MMORPG Game Server]]></description><link>https://munique.net/</link><image><url>https://munique.net/favicon.png</url><title>MUnique OpenMU Project</title><link>https://munique.net/</link></image><generator>Ghost 5.82</generator><lastBuildDate>Wed, 09 Sep 2026 02:07:30 GMT</lastBuildDate><atom:link href="https://munique.net/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[The Attribute System, five years later]]></title><description><![CDATA[Five years after the attribute system post: the whole Season 6 skill set went through it, and the core barely changed. A look at what that took.]]></description><link>https://munique.net/the-attribute-system-five-years-later/</link><guid isPermaLink="false">6a8840f098bb3803d5a9e1d6</guid><category><![CDATA[OpenMU]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Wed, 02 Sep 2026 19:00:00 GMT</pubDate><content:encoded><![CDATA[<p>In April 2020 I claimed on this blog that OpenMU expresses the game&apos;s mechanics as data instead of as code, and I called that post <a href="https://munique.net/power-of-the-attribute-system/">the power of the attribute system</a>. It&apos;s an easy claim to make while a system is young and the database holds three item options and a handful of relationships.</p>
<p>Since then the complete Season 6 skill set has gone through it, including all six master skill trees. That&apos;s enough contact with the actual game to see whether the promise survived it.</p>
<h2 id="a-short-refresher">A short refresher</h2>
<p>For those who don&apos;t want to read the old post first, the vocabulary in three sentences. An <strong>attribute definition</strong> describes a kind of value - &quot;Strength&quot;, &quot;Maximum Physical Damage&quot;, &quot;Ice Resistance&quot;. An <strong>attribute relationship</strong> says how one value feeds another (&quot;every point of Strength adds 1/4 to maximum damage&quot;), and an <strong>aggregate type</strong> says how it&apos;s applied: added to the raw value, multiplied, or added to the final value. <strong>Power-ups</strong> from items, buffs and passive skills plug into the same graph.</p>
<p>The important consequence: nothing in the game logic computes a character&apos;s damage from a formula. It asks the attribute system for a number, and the number is the result of whatever relationships the configuration happens to contain.</p>
<h2 id="the-real-test-master-skill-trees">The real test: master skill trees</h2>
<p>Master skills are a good stress test, because they&apos;re not skills in the usual sense. Most of them modify something that already exists: more damage on a skill you already have, more defense, longer buff duration, and sometimes something structural, like an additional arrow on Triple Shot.</p>
<p>In a hardcoded design, every single one of those is a special case somewhere in the combat code. Here&apos;s how the additional arrow works instead. The master skill is declared like this:</p>
<pre><code class="language-csharp">this.AddMasterSkillDefinition(
    SkillNumber.TripleShotMastery, /* ... */,
    Stats.ExtraProjectiles, AggregateType.AddRaw);
</code></pre>
<p>Higher-level bows grant the same attribute, as a plain item power-up:</p>
<pre><code class="language-csharp">item.BasePowerUpAttributes.Add(
    this.CreateItemBasePowerUpDefinition(Stats.ExtraProjectiles, 1, AggregateType.AddRaw));
</code></pre>
<p>And the code which actually fires the arrows contains exactly this:</p>
<pre><code class="language-csharp">extraProjectiles += (int)player.Attributes![Stats.ExtraProjectiles];
</code></pre>
<p>Three places that know nothing about each other: a master skill tree, an item definition, and a skill action. The skill action doesn&apos;t know whether the extra arrow comes from the master tree, from the bow, or from something we haven&apos;t invented yet - a buff, an event, a server-specific item. Add another source in the configuration and it just works.</p>
<p>There are over 170 master skill definitions in the Season 6 configuration now, and that&apos;s how all of them work.</p>
<h2 id="what-the-system-needed-to-grow">What the system needed to grow</h2>
<p>It would be a boring post if I claimed nothing had to change. The core stayed, but it needed a few extensions, and each of them came from a mechanic that couldn&apos;t be expressed otherwise:</p>
<ul>
<li><strong>A <code>Maximum</code> aggregate type.</strong> Some values must not add up. If you wear three rings with ice resistance, MU doesn&apos;t sum them - only the highest one counts. So next to &quot;add to raw&quot;, &quot;multiply&quot; and &quot;add to final&quot;, there&apos;s now &quot;take only the highest value&quot;.</li>
<li><strong>Relationships whose operand is an attribute.</strong> In 2020 a relationship multiplied an input attribute by a constant. Now the operand itself can be an attribute, so &quot;this value scales with that other value&quot; is expressible in data instead of in code. Several master formulas need exactly that.</li>
<li><strong>More operators.</strong> Multiply and add weren&apos;t enough - the official formulas contain exponentiation (in both directions), and minimum and maximum, so those became input operators too.</li>
<li><strong>A lot more attributes.</strong> <code>Stats</code> holds over 280 attribute definitions today. Among them the seven element resistances, each mapped to its damage bonus counterpart, which is how the jewelry system ended up being pure configuration as well.</li>
</ul>
<p>That&apos;s the honest list. Four extensions in five years, all of them additive, none of them a redesign. Meanwhile the project files themselves - the composable attribute, the elements, the relationship evaluation - are still the ones the 2020 post described, and in the last year they only saw trivial changes.</p>
<p>I&apos;ll take that as the promise holding.</p>
<h2 id="the-unglamorous-half-migrating-data">The unglamorous half: migrating data</h2>
<p>There&apos;s a part of the data-driven approach which I underestimated in 2020.</p>
<p>If the game&apos;s mechanics are configuration, then adding a mechanic means changing configuration - and the configuration of a running server lives in its database, not in the source code. Shipping a new attribute in <code>Stats.cs</code> does nothing for anyone who already has a server. Their database has to learn about it.</p>
<p>So OpenMU grew a mechanism for that: configuration update plugins. Each one has a version number, a description, a creation date, and an <code>ApplyAsync</code> which does the change against an existing configuration. The elf one, for example, creates the new attribute definitions, adds power-ups to existing magic effects and fixes skill values:</p>
<pre><code class="language-csharp">var extraProjectiles = context.CreateNew&lt;AttributeDefinition&gt;(
    Stats.ExtraProjectiles.Id, Stats.ExtraProjectiles.Designation, Stats.ExtraProjectiles.Description);
gameConfiguration.Attributes.Add(extraProjectiles);
</code></pre>
<p>There are over 100 of these updates by now. It&apos;s the price of the design: data which behaves like code has to be migrated like a schema. If you build something similar, plan for this from day one - it&apos;s much less pleasant to add later.</p>
<h2 id="thanks-ze-dom">Thanks, ze-dom</h2>
<p>The system only proves anything if someone fills it with the actual game, and since September 2024 that someone has mostly been <a href="https://github.com/ze-dom?ref=munique.net">ze-dom</a>. He worked his way from spawn points and jewelry options through the damage and defense calculations right into the master skill trees, each as an update plugin, each with whatever the attribute system was still missing for it.</p>
<p>That was the last big gap in Season 6 feature completeness, and it is exactly the kind of work nobody sees: no new subsystem, no impressive screenshot, just hundreds of small values which have to be right, in a game where the community notices immediately when one of them isn&apos;t. Thank you.</p>
<h2 id="would-i-build-it-the-same-way-again">Would I build it the same way again?</h2>
<p>Yes - with open eyes about the cost.</p>
<p>The cost is indirection. When a damage number is wrong, you don&apos;t read a formula; you walk a graph of relationships that four different configuration files contributed to. The tooling for that is better than it was (the admin panel can show you the attributes of a player), but it&apos;s still harder than reading twenty lines of arithmetic. And, as above, the configuration needs migrations forever.</p>
<p>The benefit is everything in this post. Six master skill trees, elemental jewelry, pets, buffs, event modifiers and a good number of server-specific customizations went in without touching the core of the system, and mostly without touching the game logic either. Server owners change values in the admin panel that would be a source code change in most other emulators.</p>
<p>Five years in, the attribute system is the OpenMU design decision I&apos;d defend the most.</p>
]]></content:encoded></item><item><title><![CDATA[Bots that play your server]]></title><description><![CDATA[Server-side bots which hunt, group up and log in and out over the day - no client, no packets. How they work, what they cost, and what a thousand of them found in the engine.]]></description><link>https://munique.net/bots-that-play-your-server/</link><guid isPermaLink="false">6a88483d98bb3803d5a9e1e7</guid><category><![CDATA[OpenMU]]></category><category><![CDATA[MU Online]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Thu, 27 Aug 2026 17:30:53 GMT</pubDate><content:encoded><![CDATA[<p>Every new MU server has the same first evening. Somebody sets it up, posts the address, a handful of players log in, and each of them sees a Lorencia with two people standing in it. Most of them log out again, and the ones who stay are looking at an empty world - which is the one thing an MMORPG cannot be, no matter how correct its damage formulas are.</p>
<p>OpenMU can now populate itself. Since this summer the server can run bots: persistent characters which hunt, level up, spend their points, keep their buffs up, wear the gear they find, restock in town, group up, and log in and out over the day. The feature was built by <a href="https://github.com/nolt?ref=munique.net">nolt</a> and is off by default - switching it on is a deliberate decision by the server admin.</p>
<p>I want to write about it because the design turned out more interesting than &quot;fake players walking around&quot;, and because most of the hard parts had nothing to do with making a character move.</p>
<h2 id="no-client-involved">No client involved</h2>
<p>Years ago I wrote about a <a href="https://munique.net/demo-console-client/">demo console client</a> - a program which connected to the server and spoke the protocol like a real client would. That is the obvious way to build a bot, and it&apos;s the wrong one for this purpose: a thousand of them means a thousand connections, a thousand encryption pipelines, and a thousand copies of a client&apos;s state, to simulate players who are already living in the server&apos;s own memory.</p>
<p>So a bot here isn&apos;t a client at all. It&apos;s an <code>OfflinePlayer</code> - the same class that keeps a character playing after its owner logs out, which is what MU Helper&apos;s offline mode does - with a navigator on top. No connection, no packets, nothing to encrypt.</p>
<p>Two ticks make up its mind. The offline helper AI runs twice a second and does the close-up work: attack, heal, buff, pick things up. The navigator runs once a second and decides the things an offline session never had to answer: where to hunt, when to travel, when to go shopping, whom to follow.</p>
<p>And bots are ordinary accounts. Each one is a real <code>Account</code> with an <code>IsBot</code> flag, its characters saved in the database like anyone else&apos;s, reloaded on the next start. A bot&apos;s progress belongs to the server&apos;s data, not to a process.</p>
<p>The rule which holds the whole thing together is this: <strong>a bot acts through the regular player actions.</strong> Moving an item, talking to a merchant, consuming a jewel, joining a party - all of it goes through the same code path with the same validations a client&apos;s packet would trigger. A bot cannot do anything a player couldn&apos;t, and when a rule changes, it applies to the bots for free. It&apos;s the same principle as the attribute system: don&apos;t build a second version of the game next to the game.</p>
<h2 id="the-interesting-problems">The interesting problems</h2>
<p>Almost none of them are about walking or hitting things.</p>
<p><strong>Everybody went to the same map.</strong> The obvious rule - always hunt the best map you can survive - turns every bot of a level band into the same answer, and leaves the rest of the band deserted. So the map is drawn instead of maximized: the best option still wins about a third of the picks, the runners-up split the rest, and the population spreads out the way players of that band would. Only maps which are actually an improvement take part in the draw.</p>
<p><strong>A map can pass every check and still pay nothing.</strong> The monsters a bot is allowed to fight might be rare there, or other hunters get to them first. A bot notices this the way a player does - it hasn&apos;t landed a hit in minutes - and steps down one notch at a time until it finds something it can farm. The normal map choice carries it back up when its level and gear recover.</p>
<p><strong>A mastered bot wants the weakest monsters, not the strongest.</strong> Master experience is only granted above a monster level threshold, and it barely grows with the monster&apos;s level after that. So the cheapest kill above the line is the best one - the opposite of what a bot does for the rest of its life. Those monsters still carry 40.000+ health, which is far beyond what a bot&apos;s found gear normally budgets for, so the hit budget is stretched for them. Its survivability isn&apos;t: a monster whose hits it can&apos;t take is still refused.</p>
<p><strong>A veteran can own a skill it cannot cast.</strong> On a reset server, a reset keeps every learned skill but takes back the level which unlocked it. A character back at level 12 still owns Swell Life, which asks for 120. The game refuses such a cast silently - so a bot which kept trying would stand there forever, buffing something that never takes effect and never getting as far as attacking. Skills which can&apos;t currently be cast are passed over, in the attack rotation and in the buff rotation alike.</p>
<p><strong>Being broke is a trap.</strong> A merchant trip only pays off if the bot can pay for something. A broke bot buys nothing, comes back as poor as it left, and would set out again instead of hunting - which is the only way it could have earned the money in the first place. Restocking therefore needs the means to pay, or loot worth selling once it&apos;s there.</p>
<p><strong>A pet is not a stat.</strong> Plasma Storm draws its damage from the Fenrir, but the attribute behind it is derived from the character&apos;s own stats - nothing except the pet slot distinguishes a mounted character from one riding nothing. So pet skills are excluded unless the pet is really equipped, and the same goes for skills the game only activates during a castle siege.</p>
<p>My favourite of the lot is the PvP rule, because it&apos;s defensive in an unusual sense: a bot fights back only as far as the game&apos;s own rules allow - inside the self-defense window, or against someone already flagged as a killer. That means it can never be provoked into becoming an outlaw which players could then farm for free. It does remember who hit it, though, and a killed bot walks back to its killer to wait for a legal opening.</p>
<h2 id="a-population-is-a-deployment-question">A population is a deployment question</h2>
<p>The part I didn&apos;t expect to be interesting: bots count towards the player count of their game server, exactly like players do. And a server which reached its maximum player count turns new connections away. So a bot population large enough to fill a server would lock out the humans it was supposed to attract.</p>
<p>Hence a <code>Bot capacity %</code> - 60 by default - which is the share of a server&apos;s player limit its bots may occupy, with the rest reserved for players. And because OpenMU can run each game server as its own process, the split has to work without the servers negotiating: which accounts a server animates is a pure function of the account index and the set of configured game servers, so every server computes the same answer on its own. What doesn&apos;t fit stays offline until the deployment has room.</p>
<p>The cost, measured on a 12-core host: about 0.35 core and 760 MB for 250 bots, about 1.7 cores and 1.2 GiB for 1100. Generating a fresh population costs roughly a second per account - the password hashing dominates - and starting an existing population of 1100 takes some 15 seconds.</p>
<h2 id="the-honest-part">The honest part</h2>
<p>Yes, this is a feature which can make a server look busier than it is. There&apos;s no point pretending otherwise, and it&apos;s why it ships disabled: turning it on is the admin&apos;s decision, not something OpenMU does behind anyone&apos;s back.</p>
<p>What surprised me is the other direction. A thousand characters playing at once is the load test nobody ever bothered to write, and it found real problems in the engine: neither <code>MagicEffectsList</code> nor <code>ComposableAttribute</code> is thread-safe, and a population that size runs into those races far more often than human players do - a few caught exceptions per minute. The bots deal with it pragmatically, by counting failed ticks and restarting themselves after twenty of them, which is what a player would do too. But the fix belongs in the engine, and now there&apos;s a reproducible way to hit it.</p>
<p>The limitations are documented rather than hidden. Bots never buy equipment - they wear what drops for them, so a bot at maximum level is weaker than a player of the same level would be. They do no quests and don&apos;t trade with players, the latter deliberately, because trading would be an abuse surface. Master skills which cost ten points at once are never learned, because a bot spends every point as it earns it.</p>
<h2 id="thanks">Thanks</h2>
<p>The bot feature is <a href="https://github.com/nolt?ref=munique.net">nolt&apos;s</a> work - the initial one in July, and then a series of changes which turned a set of characters into something that behaves like a population. <a href="https://github.com/eduardosmaniotto?ref=munique.net">Eduardo</a> added, among other things, the fresh-start option, which lets generated bots begin at level 1 with starter gear like a real new character instead of appearing with a random level and upgraded equipment.</p>
<p>Reviewing a feature of that size is its own kind of work, and most of what&apos;s in this post came out of that review going back and forth. The details above - the drawn map choice, the skill a reset character can&apos;t cast, the bot that can&apos;t afford to go shopping - are the ones which took the longest to get right, and they&apos;re the reason a bot is hard to tell apart from a quiet human player.</p>
]]></content:encoded></item><item><title><![CDATA[The MU Online protocol, explained]]></title><description><![CDATA[A TCP socket, a stream of bytes, and nobody who wrote down what they mean. A tour of the MU Online wire format: framing, the three things called encryption, and what we changed.]]></description><link>https://munique.net/the-mu-online-protocol-explained/</link><guid isPermaLink="false">6a88505898bb3803d5a9e1f6</guid><category><![CDATA[OpenMU]]></category><category><![CDATA[MU Online]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Tue, 25 Aug 2026 19:07:10 GMT</pubDate><content:encoded><![CDATA[<p>Every MU Online server emulator begins at the same place: a TCP socket, a stream of bytes, and nobody who ever wrote down what they mean. Everything the community knows about this protocol was taken apart packet by packet, and after ten years of doing that in OpenMU, it seems worth writing the overview down in one piece.</p>
<p>This is the tour: how a packet is framed, what the three things called &quot;encryption&quot; actually are, how a client gets from a connect server into the world - and what we changed once we had a client of our own.</p>
<h2 id="the-frame">The frame</h2>
<p>Every packet starts with a byte which says two things at once: whether the payload is encrypted, and how long the packet is allowed to be.</p>
<table>
<thead>
<tr>
<th>First byte</th>
<th>Encrypted</th>
<th>Length field</th>
<th>Maximum size</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>0xC1</code></td>
<td>no</td>
<td>1 byte</td>
<td>255 bytes</td>
</tr>
<tr>
<td><code>0xC2</code></td>
<td>no</td>
<td>2 bytes</td>
<td>65535 bytes</td>
</tr>
<tr>
<td><code>0xC3</code></td>
<td>yes</td>
<td>1 byte</td>
<td>255 bytes</td>
</tr>
<tr>
<td><code>0xC4</code></td>
<td>yes</td>
<td>2 bytes</td>
<td>65535 bytes</td>
</tr>
</tbody>
</table>
<p>After the length comes the packet code, and for some codes a subcode. That&apos;s the whole framing, and it gets small: a packet can consist of nothing but its header. When you close an NPC dialog, your client sends three bytes.</p>
<pre><code>C1 03 31
</code></pre>
<p><code>C1</code>: unencrypted, one length byte. <code>03</code>: three bytes in total, the header included. <code>31</code>: the code, which means &quot;I closed the dialog&quot;. There is nothing else to say, so nothing else is sent. A good part of the protocol looks like this - in OpenMU&apos;s definitions, 69 of the packets a client can send are four bytes or shorter.</p>
<p>With a subcode it&apos;s one byte more. This is the greeting the connect server sends right after a client connects:</p>
<pre><code>C1 04 00 01
</code></pre>
<p>Code <code>00</code>, subcode <code>01</code>, no payload: &quot;hello, I&apos;m here&quot;. A game client answers it by asking for the server list.</p>
<p>Subcodes are not a sign that the code space ran out - roughly half of the 256 possible codes are still unused today. They group what belongs together. <code>F1</code> is the session: <code>00</code> for having entered the game server, <code>01</code> for login, <code>02</code> for logout, <code>03</code> for the logout the client sends when its own cheat detection triggers. <code>F3</code> is everything about a character - list, creation, deletion, selection, stat points - and features which came later got a code of their own with the whole feature behind its subcodes: <code>B2</code> for castle siege, <code>F6</code> for quests, <code>AA</code> for duels, <code>3F</code> for player shops. In OpenMU&apos;s definitions, the server sends 129 distinct codes, 25 of which carry subcodes; <code>F3</code> alone holds 48 different packets. And, as I mentioned in <a href="https://munique.net/why-the-server-project-needed-a-client/">an earlier post</a>, the same message can have a different code depending on which localized client the player runs - the hit packet is <code>0x11</code> in the English client and <code>0xD6</code> in the Japanese one. The protocol has no version field to ask; the server has to know.</p>
<p>One more detail in encrypted packets: the first byte of the decrypted payload is a counter which runs from <code>0x00</code> to <code>0xFF</code>, so that two identical packets never look identical on the wire. It makes replay attacks harder - at least for attackers who can&apos;t count.</p>
<h2 id="three-things-called-encryption">Three things called encryption</h2>
<p>&quot;MU packet encryption&quot; refers to three different mechanisms, applied in different places and in different directions.</p>
<p><strong>SimpleModulus</strong> is the block cipher behind <code>C3</code> and <code>C4</code>. According to a Korean change log it appeared in version 0.74.01, originally with 16 keys encrypting blocks of 32 bytes. Somewhere between 0.75 and 0.97 the block size was reduced to 8 bytes, so each block is now touched by 4 keys instead of 16. I wrote about how it works <a href="https://munique.net/a-closer-look-at-the-mu-online-packet-encryption/">in detail</a> and later <a href="https://munique.net/simplemodulus-revisited/">revisited it</a>.</p>
<p><strong>Xor32</strong> is a rolling XOR with a 32 byte key, applied on top of SimpleModulus in the client-to-server direction. Webzen changed those keys during maintenances, which tells you how much they trusted them. They&apos;re also really easy to be calculated, if you have the encrypted and unencrypted content of at least one longer packet. <em>Hint: When you send a long chat message, the client sends it encrypted. The server sends the same message back, but unencrypted. Additionally, a chat message structure is so simple, the unencrypted content can be guessed.</em></p>
<p><strong>Xor3</strong> is three bytes - <code>0xFC 0xCF 0xAB</code> - and it protects your login credentials. That is not a typo, and those three bytes have been public for two decades.</p>
<p>Which is the honest summary of this whole layer: the algorithms and keys have been known in the server and cheater community for well over ten years, and the keys can be derived from known packet content or brute-forced in a fraction of a second. This is obfuscation, not security. Everything arriving from a client is hostile input, and the server has to validate every single thing a packet claims - which is exactly why item duplication bugs <a href="https://munique.net/item-duplication-exploits/">were possible</a> in the first place. If you run OpenMU for real, the encryptors sit behind interfaces so you can replace them, and you should.</p>
<h2 id="from-connecting-to-standing-in-lorencia">From connecting to standing in Lorencia</h2>
<p>The path a client takes is short and worth knowing, because most &quot;I can&apos;t connect&quot; reports fail somewhere in the middle of it.</p>
<p>First, the <strong>connect server</strong>. It&apos;s a separate, tiny service whose only job is to tell clients where the game servers are. The conversation is four packets: the server says hello, the client asks for the server list, the server answers with the list plus a load percentage per server, the client asks for the connection info of the one it picked, and gets an IP and a port back.</p>
<p>Then, the <strong>game server</strong>. The client opens a second connection, this time encrypted, and sends its login with the credentials wrapped in the three-byte Xor3, wrapped by SimpleModulus. The server answers with the character list, the client picks one, and after a short exchange the character enters the world - which is the moment the server starts sending the endless stream of &quot;these objects are now in your scope&quot; and &quot;this one moved&quot; packets that make up the actual game.</p>
<h2 id="we-describe-packets-as-data-not-as-code">We describe packets as data, not as code</h2>
<p>The interesting part of maintaining this protocol isn&apos;t any single packet - it&apos;s that there are so many of them. OpenMU currently defines 195 packets sent by the client, 281 sent by the game server, 11 for the connect server and 7 for the chat server.</p>
<p>Nobody wants to write and maintain that as hand-written parsing code, so we don&apos;t. Every packet is described in XML - its code, its subcode, its fields with their types and offsets, what it does, and what it causes on the other side - and everything else is generated from that description: the C# structs which wrap the raw bytes without allocating, the extension methods for sending, the tests, the 500 markdown files in the repository&apos;s <code>docs/Packets</code> folder, and these days even the C++ bindings the client uses. I wrote about that machinery when it was new, in <a href="https://munique.net/handling-packet-structures-dotnet/">handling packet structures in .NET</a> and <a href="https://munique.net/generating-message-structs-by-data/">generating message structs by data</a>.</p>
<p>The practical effect is that &quot;the protocol&quot; isn&apos;t spread over the codebase. It&apos;s four XML files, and everything which speaks it derives from them.</p>
<h2 id="what-we-changed">What we changed</h2>
<p>For most of OpenMU&apos;s life, all of the above was fixed: the client was a binary we couldn&apos;t touch, so the protocol was a given. Since we have <a href="https://munique.net/why-the-server-project-needed-a-client/">our own client</a>, it isn&apos;t anymore.</p>
<p>The extensions are the ones the old format made impossible. Damage and experience are no longer capped at 16 bits, so a big hit is one packet instead of a loop of them. Items are serialized dynamically in 5 to 15 bytes with a flags byte, instead of squeezing every property into a fixed 12-byte bit puzzle. The appearance of a character gets 27 bytes instead of 18. Monsters send a health bar after being hit. The server tells the client which chat commands it may use.</p>
<p>None of that breaks the original client, because the server picks its view implementations per client version: an original Season 6 client gets the original packets, and our client - which announces itself as season 106 - gets the extended ones. Same server, same game logic, different bytes on the wire.</p>
<h2 id="if-you-want-to-look-at-it-yourself">If you want to look at it yourself</h2>
<p>Two things in the repository are worth knowing about. The <code>docs/Packets</code> folder contains a markdown page per packet, generated from the definitions, including when it&apos;s sent and what it triggers - that&apos;s the closest thing to a protocol specification that exists. And the <a href="https://munique.net/munique-openmu-network-analyzer/">network analyzer</a> sits between a client and a server, shows the traffic live, resolves it against the packet definitions and shows you the values instead of raw bytes.</p>
<p>Both of them exist because reading someone else&apos;s protocol is normally a lonely activity with a hex editor. It doesn&apos;t have to be.</p>
]]></content:encoded></item><item><title><![CDATA[A .NET library inside the game client]]></title><description><![CDATA[The client's network code is C# now - a Native AOT library loaded into a 20-year-old C++ game's own process, with no runtime installed. Here's how it works, and why it needed .NET 9 to be possible.]]></description><link>https://munique.net/a-net-library-inside-the-game-client/</link><guid isPermaLink="false">6a882dc998bb3803d5a9e1c9</guid><category><![CDATA[C#]]></category><category><![CDATA[MuMain]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Sun, 23 Aug 2026 08:00:00 GMT</pubDate><content:encoded><![CDATA[<p>This is the post I promised at the end of <a href="https://munique.net/why-the-server-project-needed-a-client/">the last one</a>: the MU Online client doesn&apos;t implement the network protocol in C++ anymore. It calls into a .NET library instead - the same one the server uses - and that library is loaded straight into the client&apos;s own process. No service, no second process, no IPC, no .NET runtime installed on the player&apos;s machine. Just a DLL sitting next to <code>main.exe</code>, exporting plain C functions.</p>
<p>I&apos;ve written a fair amount of interop code over the years, but I have never seen this particular combination anywhere else, and until recently it wasn&apos;t even possible. So it deserves its own post.</p>
<h2 id="the-problem-two-implementations-of-one-protocol">The problem: two implementations of one protocol</h2>
<p>OpenMU has a network library, <code>MUnique.OpenMU.Network</code>. It knows the packet structures, it knows the two encryption schemes (SimpleModulus and the Xor stuff I wrote about <a href="https://munique.net/a-closer-look-at-the-mu-online-packet-encryption/">years ago</a>), and it&apos;s the part of the project which is best covered by tests, because getting a byte wrong there means nothing works at all.</p>
<p>The client had its own C++ implementation of exactly the same thing. So every protocol change meant doing the work twice, in two languages, with two chances to get the bit shifting wrong. And since I had just started <a href="https://munique.net/why-the-server-project-needed-a-client/">extending the protocol</a>, &quot;twice&quot; was going to be a lot of work.</p>
<p>The obvious idea - let both ends use the same library - has an obvious problem: one end is a 20-year-old C++ program and the other is .NET.</p>
<h2 id="why-this-only-works-now">Why this only works now</h2>
<p>The traditional answer is to host the runtime: link against <code>nethost</code>/<code>hostfxr</code>, start CoreCLR inside your process, load an assembly, get a function pointer, and ship a runtime with your game. That works, and it&apos;s a lot of moving parts to put into a game client which players install by unpacking a zip.</p>
<p>Native AOT changes the shape of the problem. The library is compiled ahead of time into native code, it&apos;s self-contained, there is no JIT and no runtime to install, and - the important part for this use case - it can export ordinary C entry points that any native program can resolve with <code>GetProcAddress</code> or <code>dlsym</code>. From the client&apos;s point of view it is simply a native DLL. It doesn&apos;t know or care that the code inside was written in C#.</p>
<p>And here&apos;s the detail which made this a &quot;newest .NET&quot; story rather than something I could have done in 2023: <strong>the client is 32-bit.</strong> Native AOT on Windows supported x64 and Arm64 in .NET 8 - x86 was <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?ref=munique.net#platformarchitecture-restrictions">added in .NET 9</a>. A 20-year-old game client is exactly the kind of program that is still x86, and before that table gained its third entry, this whole approach was off the table for it. The library targets .NET 10 today, and CMake picks the runtime identifier</p>
<ul>
<li><code>win-x86</code>, <code>win-x64</code> or <code>linux-x64</code> - from the build it&apos;s producing.</li>
</ul>
<h2 id="how-it-fits-together">How it fits together</h2>
<p>Three pieces: the exports, the loading, and the code generation which writes most of it.</p>
<h3 id="the-exports">The exports</h3>
<p>The managed side is a static class whose methods carry <code>[UnmanagedCallersOnly]</code>. That attribute makes a method callable directly from native code, with a chosen export name, and no marshalling magic in between - you get what a C function gets. Connecting looks like this:</p>
<pre><code class="language-csharp">[UnmanagedCallersOnly(EntryPoint = &quot;ConnectionManager_Connect&quot;)]
public static int Connect(
    IntPtr hostPtr,
    int port,
    byte isEncrypted,
    delegate* unmanaged&lt;int, int, byte*, void&gt; onPacketReceived,
    delegate* unmanaged&lt;int, void&gt; onDisconnected)
</code></pre>
<p>Two things to notice. First, the return value is an <code>int</code> handle, not a pointer to anything managed - the C++ side never holds a reference to a .NET object, it holds a number, and the library keeps a dictionary of connections behind it. Second, the last two parameters are function pointers back into C++. That&apos;s how received packets and disconnects get delivered: the managed side reads from the socket, decrypts, and calls the client&apos;s static handler with the handle, a length and a pointer to the bytes.</p>
<p>Around that there are <code>ConnectionManager_Send</code>, <code>ConnectionManager_BeginReceive</code>, <code>ConnectionManager_Disconnect</code> - and then one export per packet type. Currently that&apos;s over 200 exported entry points.</p>
<h3 id="the-loading">The loading</h3>
<p>No hosting API, no <code>coreclr_initialize</code>. The client loads the library the way it would load any other DLL, on first use:</p>
<pre><code class="language-cpp">inline HINSTANCE get_munique_client_library_handle()
{
    static const HINSTANCE handle = LoadLibrary(L&quot;MUnique.Client.Library.dll&quot;);
    return handle;
}
</code></pre>
<p>On Linux it&apos;s <code>dlopen</code> of <code>MUnique.Client.Library.so</code>, resolved through <code>/proc/self/exe</code> so it&apos;s found regardless of the working directory. Each exported function is then resolved once into an inline function pointer, and the call sites just call it.</p>
<p>The construct-on-first-use pattern above isn&apos;t decoration, by the way. The function pointers are inline globals in other translation units, and they resolve their symbols during their own dynamic initialization - a plain global handle would have been a static initialization order fiasco waiting for a full moon.</p>
<h3 id="the-code-generation">The code generation</h3>
<p>I was not going to write 200 exports by hand, and I was not going to write their C++ counterparts by hand either.</p>
<p>The packet definitions live in XML - the same XML the server generates its packet structs from, which I wrote about in <a href="https://munique.net/generating-message-structs-by-data/">Generating message structs by data</a>. The client library pulls them in as a NuGet package, so it&apos;s pinned to a version of the definitions instead of copy-pasted from somewhere.</p>
<p>From that XML, five XSL transformations produce:</p>
<ul>
<li>the C# methods with their <code>[UnmanagedCallersOnly]</code> attributes,</li>
<li>C++ headers and sources with a nice class-based API for the client,</li>
<li>C++ binding headers with the <code>typedef</code>s and symbol lookups,</li>
<li>and the enums, for both languages.</li>
</ul>
<p>The generated C# for the client-to-server direction alone is about 6.700 lines. On the C++ side, sending a chat message ends up as:</p>
<pre><code class="language-cpp">void PacketFunctions_ClientToServer::SendPublicChatMessage(
    const wchar_t* character, const wchar_t* message)
{
    dotnet_SendPublicChatMessage(this-&gt;GetHandle(), character, message);
}
</code></pre>
<p>which is exactly as boring as it should be. The client code calling it has no idea that the next stop is a garbage-collected language.</p>
<h2 id="what-bit-us">What bit us</h2>
<p><strong><code>wchar_t</code> is not <code>wchar_t</code>.</strong> On Windows it&apos;s 2 bytes and holds UTF-16, on Linux it&apos;s 4 bytes and holds UTF-32, while <code>Marshal.PtrToStringAuto</code> always decodes UTF-16. The result of that mismatch, when the client was first brought up on Linux, was that the server address turned into nonsense, <code>connect()</code> hung on the main thread, and the game looked like it was frozen on a black screen. The library now decodes by the platform&apos;s real <code>wchar_t</code> width.</p>
<p><strong>Cross-compilation has a hard edge.</strong> A Linux <code>dotnet</code> cannot AOT-publish a Windows library. The build has to distinguish between the build-time C# tools (which run on any host and generate C++ source) and the AOT publish step (which can&apos;t cross the OS boundary). We learned that by having the CI fail on missing generated headers.</p>
<p><strong>MSBuild will happily redo everything.</strong> The XSL transformation target had no <code>Inputs</code>/<code>Outputs</code> at first, so it re-ran and rewrote every generated file on every single build, which in turn made CMake think the world had changed. Incremental builds are a feature you have to ask for.</p>
<p><strong>And the usual Native AOT rules apply</strong>: no runtime code generation, trimming is part of the deal, and every managed exception has to be caught before it reaches the C++ frame above it. The exported methods are all wrapped accordingly.</p>
<h2 id="was-it-worth-it">Was it worth it?</h2>
<p>Yes, and not by a small margin. There is exactly one implementation of the protocol now. When I add a packet, I add it to the XML, rebuild, and both sides have it - the server because it always did, the client because its bindings are generated from the same file. The encryption is maintained once. The tests which cover the server&apos;s network code cover the client&apos;s network code, because it is the same code.</p>
<p>The strange part is how unexciting it feels from the inside. A C++ game from 2005 calls a function pointer, and on the other side of that pointer sits modern C# with pipelines and spans, doing the actual socket work. The two halves are about twenty years apart and they get along fine.</p>
]]></content:encoded></item><item><title><![CDATA[Why the server project needed a client]]></title><description><![CDATA[For ten years the client was the one part of the system we couldn't change. This is what that cost in bytes - and what opened up once one end was finally ours.]]></description><link>https://munique.net/why-the-server-project-needed-a-client/</link><guid isPermaLink="false">6a882bf398bb3803d5a9e1b8</guid><category><![CDATA[MU Online]]></category><category><![CDATA[MuMain]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Sat, 22 Aug 2026 14:13:35 GMT</pubDate><content:encoded><![CDATA[<p>Hey folks, as promised in the <a href="https://munique.net/recap-of-2023-2026/">recap</a>, here&apos;s the first post about the client fork. Not about how it works - that comes later - but about why we maintain a game client at all.</p>
<p>The short answer: because for the last ten years the client was the one part of the system we couldn&apos;t change, and almost every ugly thing in OpenMU&apos;s network code exists because of that.</p>
<h2 id="the-client-is-the-specification">The client is the specification</h2>
<p>When you write a server emulator, you don&apos;t design a protocol. You obey one. The client is a compiled binary from another decade, it expects certain bytes in a certain order, and if you send something it doesn&apos;t like, it either ignores you or crashes. That&apos;s the whole design space.</p>
<p>A good part of this blog is a monument to that: how the <a href="https://munique.net/a-closer-look-at-the-mu-online-packet-encryption/">packet encryption works</a>, how <a href="https://munique.net/simplemodulus-revisited/">SimpleModulus</a> was analyzed, the <a href="https://munique.net/munique-openmu-network-analyzer/">network analyzer</a> I built to see what&apos;s going over the wire, and the <a href="https://munique.net/handling-packet-structures-dotnet/">packet structures</a> which I ended up <a href="https://munique.net/generating-message-structs-by-data/">generating from data</a> because there are so many of them.</p>
<p>None of that work is wasted - the original client is still supported, and supporting it stays a goal. But everything above is the cost of not owning one end of the connection.</p>
<h2 id="what-that-costs-in-bytes">What that costs, in bytes</h2>
<p>Three examples, all from the server code as it is today.</p>
<p><strong>Damage doesn&apos;t fit.</strong> The hit packet carries the damage as a 16 bit value. That&apos;s a hard ceiling of 65535 per hit. Modern servers hand out far bigger numbers than that, so the workaround has been in OpenMU for years: send the packet more than once until the damage is used up.</p>
<pre><code class="language-csharp">// do/while, so that a &apos;miss&apos; with 0 damage sends a message, too.
do
{
    var healthDamage = (ushort)Math.Min(0xFFFF, remainingHealthDamage);
    var shieldDamage = (ushort)Math.Min(0xFFFF, remainingShieldDamage);

    await connection.SendObjectHitAsync(/* ... */).ConfigureAwait(false);

    remainingShieldDamage -= shieldDamage;
    remainingHealthDamage -= healthDamage;
}
while (remainingHealthDamage &gt; 0 || remainingShieldDamage &gt; 0);
</code></pre>
<p>A 200.000 damage hit is four packets. Experience gain has exactly the same problem and exactly the same loop. It works, and it&apos;s nonsense.</p>
<p><strong>An item is a 12 byte bit puzzle.</strong> In Season 6 an item is serialized into 12 bytes, and there is no room in them. The item level sits in four bits of byte 1, so it can&apos;t go beyond 15. The option level is split up: two bits go into byte 1, the third bit is smuggled into bit 6 of the &quot;excellent&quot; byte. My comment in that code has been there for years:</p>
<blockquote>
<p>// The item option level is splitted into 2 parts. Webzen... :-/</p>
</blockquote>
<p>To support more than 256 items per group, the 9th bit of the item number is parked in yet another free bit. The item group takes the high nibble of byte 5, and the guardian option flag lives in the low one. Every bit in those 12 bytes is spoken for. If you want to add a new item property to your server - go find a bit.</p>
<p>For comparison, the same item in the old versions: 3 bytes in 0.75, 4 bytes in 0.95. The character appearance, which decides what everyone around you looks like, is 9, 11 and 18 bytes respectively. The whole visual state of a player fits into 18 bytes, and that&apos;s why some things simply cannot be shown.</p>
<p><strong>The same packet has different codes per language.</strong> Not per version - per localized binary. The hit packet is <code>0x11</code> for the English client, <code>0xD6</code> for Japanese, <code>0xDC</code> for Vietnamese, <code>0xDF</code> for Korean and Filipino, <code>0xD0</code> for Chinese and <code>0xD2</code> for Thai. So the server doesn&apos;t only have to know which season you&apos;re running, it has to know which regional build you downloaded.</p>
<h2 id="and-the-bugs-you-cant-fix">And the bugs you can&apos;t fix</h2>
<p>The other half of the cost isn&apos;t the protocol, it&apos;s everything the client decides on its own. If the Dark Lord&apos;s raven is drawn wrong when he walks, if an item label is broken, if a window doesn&apos;t show a value the server has known all along: there is nothing you can do about it from the server side. You can&apos;t add a UI element for a feature you invented. You can&apos;t show a number the client has no field for. You can only work around it, and the usual workaround is a chat message.</p>
<p>That&apos;s the real reason. Not the missing bits - the missing say.</p>
<h2 id="so-i-took-the-sources">So I took the sources</h2>
<p>In March 2023 I started to clean up the Season 5.2 sources which were made public, with the goal of getting them to Season 6 Episode 3. This doesn&apos;t replace the original client, full support for the original Season 6 client stays a goal of OpenMU. Most people running an OpenMU server will keep using the original client, and that&apos;s fine. The point is that there is now one client we <em>can</em> change, so we can find out what a MU server looks like when nothing forces it into 12 bytes.</p>
<h2 id="what-opened-up">What opened up</h2>
<p>The nice surprise was how little the server needed for it. OpenMU has had a <a href="https://munique.net/recap-of-2019/">plugin system</a> since 2019, and its whole purpose is that view implementations are selected by the client version at runtime - that&apos;s how 0.75, 0.95 and Season 6 clients can be served by the same server. So the new client just became another version: it announces itself as season 106, and about two dozen view plugins and serializers are registered for it, right next to the old ones. No fork of the server, no <code>#if</code>, no second code path in the game logic. The game logic doesn&apos;t even know.</p>
<p>What those extended plugins do differently:</p>
<ul>
<li><strong>Damage and experience are sent once</strong>, with the values they actually have. The loops above are gone.</li>
<li><strong>The item serializer is dynamic</strong>, 5 to 15 bytes, with a flags byte that says which parts follow: option, luck, skill, excellent, ancient, harmony, guardian, sockets. The item number is a real 16 bit number, the level is a real byte. No bit smuggling, and there&apos;s room to grow.</li>
<li><strong>The appearance got 27 bytes</strong> instead of 18, which is what makes it possible to show things the old format had no space for.</li>
<li><strong>Monsters have a health bar</strong> after you hit them.</li>
<li><strong>The server tells the client which chat commands exist</strong>, so the command window can list them instead of expecting players to memorize them.</li>
<li>Character list, stats, level, master stats, quests, mail, player shops - all got extended versions.</li>
</ul>
<h2 id="was-it-worth-it">Was it worth it?</h2>
<p>Honestly, it doubled the amount of code I look after, and the second half is a C++ codebase from 2005 with all the charm that implies. I&apos;ve spent evenings on <code>wchar_t</code> widths and <code>fclose</code> calls instead of on game features.</p>
<p>But the ceiling is gone. When we now want damage to exceed 16 bit, we change a packet definition and both ends follow, instead of writing a loop that lies to the player. That&apos;s worth a lot of evenings.</p>
<p>Next time I&apos;ll write about how the client got rid of its C++ network code entirely and loads a .NET Native AOT library instead - which is the part where &quot;one packet definition, both ends&quot; actually becomes true.</p>
]]></content:encoded></item><item><title><![CDATA[Recap of 2023 - 2026]]></title><description><![CDATA[Almost four years without a post - but not without work. A recap of what happened to OpenMU since 2022, and of the project I never mentioned here: my fork of the MU Online client sources.]]></description><link>https://munique.net/recap-of-2023-2026/</link><guid isPermaLink="false">6a88174c98bb3803d5a9e18d</guid><category><![CDATA[MU Online]]></category><category><![CDATA[OpenMU]]></category><category><![CDATA[News]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Thu, 20 Aug 2026 20:34:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1501139083538-0139583c060f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fHRpbWV8ZW58MHx8fHwxNzg3MzA1MTE2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1501139083538-0139583c060f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fHRpbWV8ZW58MHx8fHwxNzg3MzA1MTE2fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="Recap of 2023 - 2026"><p>Hey folks, the last post on this blog is from December 2022. That&apos;s a long time<br>
to say nothing, and it&apos;s not because nothing happened - quite the opposite. In<br>
the meantime OpenMU moved on by three .NET versions, and I started a second<br>
project which I never wrote a single word about here: a fork of the MU Online<br>
client sources.</p>
<p>So, this post is the overdue recap. It&apos;s a long one, because there are almost<br>
four years to cover. I&apos;ll go into the details of the individual topics in<br>
separate posts.</p>
<h2 id="the-big-one-we-have-our-own-client-now">The big one: we have our own client now</h2>
<p>Since I started OpenMU, the client was always the fixed point. It was a binary we couldn&apos;t change, so every design decision on the server side had to bend around what the original client expected. In March 2023 I started to work on <a href="https://github.com/sven-n/MuMain?ref=munique.net">my fork</a> of the Season 5.2 client sources which were <a href="https://github.com/LouisEmulator/Main5.2?ref=munique.net">uploaded by Luois</a>, with the goal to clean them up and make them feature complete for Season 6 Episode 3.</p>
<p>To be clear about one thing, because I get asked: this doesn&apos;t replace the original client. Full support of the original Season 6 client stays a goal of OpenMU. The fork is an addition, not a substitute.</p>
<p>It connects to OpenMU and it&apos;s playable. Here are the parts I consider most interesting:</p>
<h3 id="more-than-25-frames-per-second">More than 25 frames per second</h3>
<p>This one is older than the rest and it&apos;s still the change I&apos;m most fond of.</p>
<p>The original client was never designed to run at more than about 25 fps. The frame rate wasn&apos;t just a display property - it was the unit of time of the whole game logic. Every movement, every rotation, every animation step and every effect assumed that one frame equals one fixed slice of time. Run it faster and characters sprint, effects race, water boils.</p>
<p>My approach was <code>FPS_ANIMATION_FACTOR</code>: a factor derived from the actual frame time, by which all these per-frame calculations get scaled. The idea is simple. Applying it was not - it means touching every movement and rotation calculation of every model in the game, and the effects are the really nasty part, because some of them are built from several animations which have to stay in sync with each other. A few thousand lines of code were changed for it, and to this day that factor appears about 3,300 times across 59 files.</p>
<p>All of that was done by hand, years before I let any AI agent near this codebase.</p>
<p>As far as I know this was the first implementation of a higher frame rate for MuMain in the MU Online community, and it was adopted by other forks pretty quickly after that.</p>
<p>Today the client runs with V-Sync and without an fps limit by default, and if V-Sync isn&apos;t available it falls back to 60 fps. There are chat commands to hange that (<code>$fps</code>, <code>$vsync</code>) and to look at what you&apos;re getting (<code>$fpscounter</code>, <code>$details</code>).</p>
<h3 id="unicode">Unicode</h3>
<p>The original client is an ANSI code page application from another era. In autumn 2023 I converted it to use UTF-16LE in memory - all strings and char arrays - while strings coming from files and from the network are handled as UTF-8. If you ever wondered why some server names or chat messages look broken in older clients: that&apos;s why. It was a huge, boring diff, and it fixed a whole class of problems at once.</p>
<h3 id="the-network-stack-is-c-now">The network stack is C# now</h3>
<p>Instead of maintaining a second implementation of the protocol in C++, the client uses <code>MUnique.OpenMU.Network</code> - the same library the server uses. It&apos;s built as a .NET 10 Native AOT library which the C++ client loads, and the bindings between both worlds are generated by XSLT from the packet definitions, just like the packet structs on the server side (see <a href="https://munique.net/generating-message-structs-by-data/">Generating message structs by data</a>).</p>
<p>The practical effect: when I add or change a packet, I change one XML file and both sides get it. The packet definitions come in as a NuGet package, so client and server are coupled by a version number instead of by a copy-paste.</p>
<h3 id="the-protocol-isnt-standard-anymore">The protocol isn&apos;t standard anymore</h3>
<p>Once both ends are under our control, the old limits are just old limits. So the protocol got extended:</p>
<ul>
<li>Damage, experience etc. can exceed 16 bit now.</li>
<li>Item serialization got improved.</li>
<li>Appearance serialization got improved.</li>
<li>Monsters show a health status bar after they were attacked.</li>
</ul>
<h3 id="rendering-the-first-steps">Rendering: the first steps</h3>
<p>The client&apos;s renderer was pure fixed-function OpenGL from around 2005. It isn&apos;t anymore: bone skinning moved to the GPU instead of transforming vertices on the CPU (which alone was a 15-20x win on the CPU side), the client asks for a Core Profile context, and there&apos;s a small render hardware interface and uniform buffer objects behind it now.</p>
<p>Getting rid of the fixed function pipeline is a big step on the way to a renderer that&apos;s fully built on vertex and pixel shaders - which is where this should end up. Most of the drawing still works the way it always did, just through a different door.</p>
<p>The optimization work which followed is more tangible: ring-buffer streaming for uniform blocks instead of orphaning a buffer on every update, terrain draw calls collapsed by bucketing tiles per texture pair (~25x fewer draws), and a lot of redundant per-draw state changes removed. Measured on dev hardware, the net result of that series was avg FPS +4.4%, 1% lows +28.0% and frame time -4.1%.</p>
<h3 id="it-runs-on-linux">It runs on Linux</h3>
<p>In June 2026 the client booted into the login scene, rendered through SDL and OpenGL and connected to a server - on Linux, natively. That was mostly the work of <a href="https://github.com/Mosch0512?ref=munique.net">@Mosch0512</a>, and it surfaced exactly the kind of bugs you&apos;d expect from a 20-year-old Win32 codebase: LP64 width bugs when reading binary assets, a double <code>fclose</code>, paths split on backslashes only, and a <code>wchar_t</code> that is 4 bytes wide on Linux while the .NET side decoded UTF-16.</p>
<h3 id="and-a-lot-of-smaller-things">And a lot of smaller things</h3>
<ul>
<li>A new translation system: <code>.resx</code> files which generate C++ code, replacing<br>
<code>text.bmd</code> and the old JSON translations.</li>
<li>The master skill tree was upgraded to Season 6.</li>
<li>Inventory and vault extensions.</li>
<li>The MU Helper UI and logic.</li>
<li>An auto-reconnect system.</li>
<li>A DevEditor to tune camera, fog, render distances and debug visualisations<br>
live, without rebuilding.</li>
<li>A window which shows the chat commands the server allows you to use, instead<br>
of expecting you to know them by heart.</li>
</ul>
<h2 id="meanwhile-on-the-server">Meanwhile, on the server</h2>
<p>OpenMU didn&apos;t stand still either - 337 pull requests were merged since the beginning of 2023.</p>
<p><strong>Keeping up with .NET.</strong> We went to .NET 8 in November 2023, .NET 9 in November 2024 and .NET 10 at the end of 2025. These upgrades are boring by now, which is a good sign.</p>
<p><strong>Tools.</strong> The releases v0.8.x and v0.9.0 were mostly tools updates: the network analyzer learned a lot of new packet definitions, got better protocol detection, shows a short summary of packet values instead of raw bytes, and it supports the extended protocol of the new client. The client launcher got fixes and became more compatible with other main.exe versions.</p>
<p><strong>Gameplay.</strong> Too much to list completely, but some highlights: experience and Zen loss when a player dies, template accounts, a warning on double-login, improved item storage handling, fixes for pets, jewels, combos and the Force skills - and monsters don&apos;t walk through walls anymore.</p>
<p><strong>Castle Siege - work in progress.</strong> The biggest feature we&apos;re currently working on, and it&apos;s nowhere near finished. It&apos;s a huge one: the state machine, the registration and mark handling, the NPC lifecycle, the persistence and the Season 6 configuration around it are in place, but there&apos;s a lot left to do before it behaves like a real siege. Don&apos;t expect to run it on a live server yet.</p>
<p><strong>Bots.</strong> OpenMU can now run bots which log in, walk, fight, use skills and go shopping. They&apos;re configurable through the admin panel. Originally a testing tool, but they&apos;re also quite useful to make an empty server look alive.</p>
<p><strong>Anti-cheat as plugins.</strong> Walk and attack speedhack detection, and a configurable limit of concurrent connections per IP - both as plugins you can switch on, off and tune, instead of hardcoded behaviour.</p>
<p><strong>Admin panel.</strong> A live log viewer with search, account and character search, and the live map now shows levels and master levels.</p>
<p><strong>Refactoring.</strong> The <code>Player</code> class grew over the years into the thing every long-lived codebase has: the class that knows everything. It&apos;s being taken apart step by step - movement, persistence, summons and storages are extracted, and map changes and logout are observable through the state machine now.</p>
<h2 id="about-ai">About AI</h2>
<p>I might as well be honest about it, since anyone browsing the repositories will notice the branch names: a considerable part of the work in the last few months was done with AI coding agents.</p>
<p>Both repositories have an <code>AGENTS.md</code> and a <code>docs/CODING_RULES.md</code> which apply to humans and agents alike, and larger changes start with a written plan that gets reviewed before a single line is changed. It works well for the kind of mechanical, wide-reaching work these codebases are full of - extracting classes, or grinding through a renderer regression series with a milestone list. It works a lot less well where the invariants only exist in someone&apos;s head, which is why writing them down became part of the workflow.</p>
<p>This deserves its own post, including the parts that didn&apos;t work.</p>
<h2 id="thanks">Thanks</h2>
<p>Both projects live from the people who send pull requests. Since 2023 that&apos;s 337 merged PRs in OpenMU and 347 in MuMain, from far more people than I can list here. Thank you - and if you filed one of the issues that made me stop and rethink something: that counts too.</p>
<h2 id="outlook">Outlook</h2>
<p>The same disclaimer as always applies: this is a hobby project and my time is limited. Still, this is what I&apos;m aiming at:</p>
<ul>
<li>Get Castle Siege into a state where you can actually play it.</li>
<li>Get the client feature complete for Season 6 Episode 3 - while keeping full<br>
support for the original Season 6 client on the server side.</li>
<li>Finish the Player refactoring on the server side.</li>
<li>Keep going on the client&apos;s renderer. Fixed function is gone, a real shader<br>
pipeline is not there yet.</li>
<li>Write more, and shorter, posts here. I have a list, starting with how a .NET<br>
Native AOT library ended up inside a C++ game client.</li>
</ul>
<p>If you want to follow along or help out, the repositories are<br>
<a href="https://github.com/MUnique/OpenMU?ref=munique.net">MUnique/OpenMU</a> and<br>
<a href="https://github.com/sven-n/MuMain?ref=munique.net">sven-n/MuMain</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Optimized pathfinding]]></title><description><![CDATA[<p>After some tests of the community, we found that many instances of monsters on a map will cause a lot of memory usage. As I found out, the root of the cause was how we do the pathfinding for the monsters. Because of that, I took this as a challenge</p>]]></description><link>https://munique.net/optimizing-pathfinding/</link><guid isPermaLink="false">6633f2d02fa90259941c3fc8</guid><category><![CDATA[C#]]></category><category><![CDATA[OpenMU]]></category><category><![CDATA[MU Online]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Sun, 18 Dec 2022 12:04:59 GMT</pubDate><media:content url="https://munique.net/content/images/2022/12/_texture-g5aa66f0f7_1920.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://munique.net/content/images/2022/12/_texture-g5aa66f0f7_1920.jpg" alt="Optimized pathfinding"><p>After some tests of the community, we found that many instances of monsters on a map will cause a lot of memory usage. As I found out, the root of the cause was how we do the pathfinding for the monsters. Because of that, I took this as a challenge to optimize memory and cpu usage.</p><h2 id="reducing-memory-usage-1">Reducing memory usage (1)</h2><p>The major flaw was using one instance of a pathfinder per monster. Every instance of a pathfinder had one instance of a &quot;<em>GridNetwork</em>&quot;, which had an array of 65536 possible nodes. These nodes were created by demand, but kept in memory to be reused, until the server closed.</p><p>So, the first thing I did was pooling these pathfinder objects per map. A handful of them could easily handle all monsters of a map, because a monster would only search for a path every few seconds, and only if a player is around. This has probably already solved the intial issue, but I wanted more.</p><h2 id="reducing-cpu-usage">Reducing CPU usage</h2><p>I observed that the preparing of the path finder between each request was pretty inefficient. It still had to reset all the nodes - up to 65k as mentioned above. Additionally, searching for a path in some maps could take longer than expected. Take the <em>Lost Tower</em> map as example. There you have a lot of walls, where the monster would see a player in their range, but could never find a path in a reasonable number of steps. For example, the limit for the walking network packet is 16 steps. The line of sight distance limit of a walk target (MonsterDefinition.ViewRange), is much lower than that (at most: 10).</p><p>So, I had the idea of limiting the search of the path to a small segment of the map. To accomplish that, I implemented a &quot;<em>ScopedGridNetwork</em>&quot;, which would use a dynamically sized segment, of 8x8 or 16x16 coordinates. To define the segment, we calculate the point in the middle of the start and end coordinates and then check if both coordinates fit into a 8x8 or 16x16 segment.</p><p>The smaller segment means less work to do: First, we don&apos;t have to prepare up to 65k nodes anymore. Second, we are not dedicating a lot of computation to hopeless requests anymore - we fail much earlier.</p><h2 id="reducing-memory-usage-2">Reducing memory usage (2)</h2><p>So, because we&apos;re now using map segments, we can reduce the size of the node array in the network to at most 256 (16x16) elements, instead of 65k.</p><p>Now, because the CPU usage was greatly reduced, why not handle all monsters of all maps with a handful of pooled Pathfinding objects? Well, I implemented this as well, of course :-)</p><h2 id="summary">Summary</h2><p>Thanks to this <a href="https://github.com/MUnique/OpenMU/pull/276?ref=munique.net">changes</a>, we have now a very efficient pathfinding implementation in OpenMU. To give you an idea: One pathfinding request takes an average of 0.025 ms on my machine (Ryzen 9 5900X). That means, it can handle about 40k pathfinding requests per second, per cpu core.</p><p>Antes de revisar opciones de camisetas, es recomendable tener claras las medidas personales y el tipo de uso previsto. Dentro de esa comparaci&#xF3;n, <a href="https://www.camisetatienda.com/categoria-producto/camisetas-de-futbol-de-clubes/ligue-1/camiseta-del-paris-saint-germain/?ref=munique.net">camisetas del Paris Saint-Germain online</a> mantiene la consulta centrada en la prenda indicada por el propio enlace. As&#xED;, la elecci&#xF3;n queda vinculada al uso real de la camiseta y no a una afirmaci&#xF3;n promocional.</p>]]></content:encoded></item><item><title><![CDATA[The power of the Attribute System]]></title><description><![CDATA[I want to show a nice powerful feature of the OpenMU Project - the Attribute System.]]></description><link>https://munique.net/power-of-the-attribute-system/</link><guid isPermaLink="false">6633f2d02fa90259941c3fc7</guid><category><![CDATA[OpenMU]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Mon, 27 Apr 2020 19:00:00 GMT</pubDate><media:content url="https://munique.net/content/images/2020/04/table-music-power-sound-63703--1-.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://munique.net/content/images/2020/04/table-music-power-sound-63703--1-.jpg" alt="The power of the Attribute System"><p>I want to show a nice powerful feature of the OpenMU Project - the Attribute System.</p><p>First, I want to describe some terms:</p><ul><li><strong>Attribute Definition</strong>: Describes the attribute itself. For example, we have definitions for each possible attribute, such as <em>Strength</em>, <em>Agility</em>, <em>Character Level</em>.</li><li><strong>Stat Attribute</strong>: It&apos;s an attribute which is saved individually for a character. A stat attribute can be increasable by the player (e.g. <em>Strength</em>, <em>Agility</em>, etc.) or just increasable by the program code (e.g. <em>Level</em>, <em>Current Health</em>, etc.).</li><li><strong>Attribute (Value)</strong>: The value of an attribute of a game object, e.g. Character A has &apos;50 <em>Strength</em>&apos;. Values are all handled as 32 bit floats.</li><li><strong>Attribute Relationship</strong>: It&apos;s possible to define relationships between attributes. For example, &quot;1 <em>Strength </em>-&gt; +0.25 <em>Maximum Damage</em>&quot;.</li><li><strong>Attribute System</strong>: An object which holds and manages all attributes of a game object (e.g. <em>Player</em>, <em>Monster</em>). Every game object which attacks or can be attacked has an Attribute System object.</li><li><strong>Item Option</strong>: An item option holds a so-called &quot;PowerUpDefinition&quot; which defines which target attribute is increased by which value. The value can consist of a constant value and/or attribute relationships. For example, you could define an option which increases the base damage by the character level.</li><li><strong>(Passive) Skills</strong>: The master skill system of MU Online has some skills which give some power-ups when they&apos;re learned. These power-ups are also described with a &quot;PowerUpDefinition&quot;, just like item options.</li><li><strong>Attribute Requirements</strong>: It&apos;s possible to define attribute requirements for Items, Skills and Maps. E.g. &apos;Sword X needs 100 <em>Strength </em>to be equipped&apos; or &apos;Skill Y uses 20 <em>Mana&apos; </em>or &apos;Map Icarus requires the attribute &quot;CanFly&quot; greater than 0&apos; (which is given by Wings or a Dinorant).</li></ul><p>The attribute definitions, relationships, base values, requirements and item options are all defined as data in the database. So, as you can see, the system as a whole allows you to tune every tiny bit of your in-game formulas and to customize a lot without changing a line of code.</p><h2 id="how-to-use">How to use</h2><h3 id="attribute-relationships">Attribute Relationships</h3><p>One of the most important things are the attribute relationships. You can find these in the <em>CharacterClass </em>configurations of each character class. In the example above, 1 <em>Strength </em>is multiplied with 0.25 to get the additional value for the maximum damage.</p><p>Instead of multiplying, there are other operators available, too. You can do simple additions or even exponentiate the source attribute with a value. There is one limitation for attribute relationships, though: Stat Attributes can not be defined as targets. For <em>Strength</em>, etc. there are two attributes: <em>BaseStrength </em>(stat attribute) and <em>TotalStrength </em>(can be a target); There is actually a relationship which adds the <em>BaseStrength </em>to the <em>TotalStrength</em>.</p><p><strong>Example &apos;Dynamical experience rate&apos;</strong></p><p>By dynamical, I mean calculating the rate based on some other attribute, such as the character level. Usually, game servers are configured with a fixed experience rate, for example 1x or 1000x which is applied in the same way to every character.</p><p>Since the &apos;experience rate&apos; is an attribute of a character, it&apos;s possible to adjust it by defining additional relationships. The default attribute value here is 1. However, what about defining something like &apos;Add the square root of the character level to the experience rate&apos;. For example, a level 9 character would be given an additional rate of 3, whereas an level 225 will get an additional rate of 15.</p><p>With the available <em>exponentiate </em>operator we can also calculate square roots, when we choose 1/2 as the operand.</p><p>To actually configure this, we need to navigate to the character class configuration in the Admin Panel:</p><ul><li>First, navigate to the Game Configuration</li><li>Then, scroll a bit down to the Character Classes, expand them and click the edit button for one of them, e.g. Dark Knight:</li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://munique.net/content/images/2020/04/character_classes.PNG" class="kg-image" alt="The power of the Attribute System" loading="lazy"><figcaption><span style="white-space: pre-wrap;">Game Configuration</span></figcaption></figure><ul><li>At the character class page, you&apos;ll find the list of attribute relationships (called AttributeCombinations) a &apos;Create&apos; button when expanded. When you have opened the creation dialog, fill in the following:</li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://munique.net/content/images/2020/04/exp_by_squareroot_of_level.PNG" class="kg-image" alt="The power of the Attribute System" loading="lazy"><figcaption><span style="white-space: pre-wrap;">Create a new attribute relationship</span></figcaption></figure><p>Hit Submit, Save and it&apos;s done. Well, a server restart is still required - I&apos;m working on that ;)</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://munique.net/content/images/2020/04/exp_by_squareroot_of_level_result.PNG" class="kg-image" alt="The power of the Attribute System" loading="lazy"><figcaption><span style="white-space: pre-wrap;">The result</span></figcaption></figure><p><strong>More use-case ideas</strong></p><p>So, if you not already identified some more practical use-cases I want to give you some ideas:</p><ul><li>Modify or add new item options</li><li>Giving new attributes to Items, Monsters, Character classes</li><li>Defining attribute requirements for Items, Maps</li><li>Modifying the damage calculations by changing the relationships in the character classes</li><li>Adding new attribute definitions and relationships to the game which you may use further to restrict Map access or give some new powers.</li></ul>]]></content:encoded></item><item><title><![CDATA[Project Status - Easter 2020]]></title><description><![CDATA[I want to give a short status update on the OpenMU project.]]></description><link>https://munique.net/project-status-easter-2020/</link><guid isPermaLink="false">6633f2d02fa90259941c3fc6</guid><category><![CDATA[OpenMU]]></category><category><![CDATA[MU Online]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Sun, 12 Apr 2020 21:01:57 GMT</pubDate><media:content url="https://munique.net/content/images/2020/04/cute-cuddly-toy-cartoon-costume-4142--1-.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://munique.net/content/images/2020/04/cute-cuddly-toy-cartoon-costume-4142--1-.jpg" alt="Project Status - Easter 2020"><p>I want to give a short status update on the OpenMU project.</p><h2 id="recap-of-2019">Recap of 2019</h2><p>As you may <a href="https://munique.net/recap-of-2019/">remember</a>, I set some goals for 2020.</p><h3 id="stabilizing-and-getting-the-network-nuget-out">Stabilizing and getting the Network-NuGet out</h3><p>I put some effort in solving some edge cases in the network code - primarily related to error handling and detecting disconnections. There have been some usages of obsolete System.IO.Pipeline functions, which I resolved.</p><p>Additionally, I improved the API of the network &amp; packets API. There are now simple extension methods for IConnection to send specific packets. They&apos;re automatically generated by the packet definition XML files, too.</p><p>Last but not least, I released nuget packages on nuget.org, so that other applications can make use of this API.</p><h3 id="release-of-an-updated-network-analyzer">Release of an updated Network Analyzer</h3><p>TODO</p><h3 id="completion-of-the-quest-system">Completion of the Quest System</h3><p>I completed the first part of it. The old quest system with the level 150, 220, 380 and 400 quests was implemented and tested. I hope that I can implement the other quest system without much changes to the logic, but only by adding its data. Time will tell.</p><h3 id="getting-other-game-features-done">Getting other game features done</h3><p>I started solving some gameplay issues and missing features:</p><ul><li><a href="https://github.com/MUnique/OpenMU/issues/4?ref=munique.net">Implement usage of ammunition (Arrows, Bolts) for Bows/Crossbows</a></li><li><a href="https://github.com/MUnique/OpenMU/issues/149?ref=munique.net">Automatic Health Regeneration in the Safezone</a></li><li><a href="https://github.com/MUnique/OpenMU/issues/160?ref=munique.net">Weather System</a></li><li><a href="https://github.com/MUnique/OpenMU/issues/150?ref=munique.net">Drop Level for Event Items</a></li><li><a href="https://github.com/MUnique/OpenMU/pull/173?ref=munique.net">Ancient Sets</a></li><li>Chaos Machine Combinations (in progress, about 30% done)</li><li>Fruit consumption</li></ul><p>From the community, I received some nice contributions of chat commands. Thanks for that!</p><h3 id="simplifying-modularization-of-the-server-public-api">Simplifying/Modularization of the server -&gt; Public API</h3><p>I implemented a basic public API. However, further modularizing the whole server architecture will take some more time and has not a high priority at the moment.</p><p>I think of implementing the &quot;servers&quot; as <a href="https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice?view=dotnet-plat-ext-3.1&amp;ref=munique.net">IHostedService</a> which would run on a <a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-3.0&amp;ref=munique.net">Generic Host</a>. It would give us some nice built-in goodies, such as logging and dependency injection.</p><h2 id="other-improvements">Other improvements</h2><h3 id="blazor">Blazor</h3><p>One big change was the technological switch of the AdminPanel from React/Redux to server-side Blazor. The reasons for the change were:</p><ul><li>Keeping most of the code in C#</li><li>Less and simpler code. React/Redux involves alot of boilerplate code which is sometimes hard to follow.</li><li>Being more productive. If I compare how much time I spent on react/redux and on blazor, I can say that I got more features done in blazor in less time.</li></ul><p>One big improvement is now the ability to edit the server configuration and account data on the admin panel. Not only small parts of it, but every tiny bit. The user interface is dynamically generated by reflection. Not the fastest way, but you would be surprised how fast it is anyway. If performance of that is ever a problem, there are still ways to improve that.</p><p>There is still a lot of things to do in the AdminPanel:</p><ul><li>General polishing, adding user-friendly field names and descriptions</li><li>Adding some kind of editors, also graphical ones (e.g. for gates of maps)</li><li>Adding authentication</li><li>Better error handling on generic edit pages which tells the user what went wrong.</li></ul><h3 id="documentation-generation">Documentation generation</h3><p>I implemented the automatic generation of packet documentation with a XSL transformation which takes the packet definition XMLs and transforms them to markdown files.</p><p>Additionally to that, I set up GitHub Pages with Jekyll, which takes these all documentation of the git repo folder <em>docs </em>and spits out html files. You can see a link to the documentation at the menu of this page.</p>]]></content:encoded></item><item><title><![CDATA[Demo Console Client for MU Online]]></title><description><![CDATA[<p>For a test of the new nuget packages of <em>MUnique.OpenMU.Network.Packets</em> I wrote a small demo client. </p><p>You can find the source code here: <a href="https://github.com/sven-n/MuConsoleTestClient?ref=munique.net">https://github.com/sven-n/MuConsoleTestClient</a></p><p>It encapsulates most of the packet creation and parsing for you, so you don&apos;t have to mess</p>]]></description><link>https://munique.net/demo-console-client/</link><guid isPermaLink="false">6633f2d02fa90259941c3fc5</guid><category><![CDATA[C#]]></category><category><![CDATA[OpenMU]]></category><category><![CDATA[MU Online]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Thu, 12 Mar 2020 21:33:58 GMT</pubDate><media:content url="https://munique.net/content/images/2020/03/abstract-art-blur-bright-373543.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://munique.net/content/images/2020/03/abstract-art-blur-bright-373543.jpg" alt="Demo Console Client for MU Online"><p>For a test of the new nuget packages of <em>MUnique.OpenMU.Network.Packets</em> I wrote a small demo client. </p><p>You can find the source code here: <a href="https://github.com/sven-n/MuConsoleTestClient?ref=munique.net">https://github.com/sven-n/MuConsoleTestClient</a></p><p>It encapsulates most of the packet creation and parsing for you, so you don&apos;t have to mess around with bits and bytes yourself. Recently, I also added some automatically generated extension methods to make sending packets easier than ever.</p><p>Have fun :)</p>]]></content:encoded></item><item><title><![CDATA[Recap of 2019]]></title><description><![CDATA[In this post I want to recap what was accomplished in 2019 and what will come next in 2020.]]></description><link>https://munique.net/recap-of-2019/</link><guid isPermaLink="false">6633f2d02fa90259941c3fc4</guid><category><![CDATA[OpenMU]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Thu, 09 Jan 2020 00:00:00 GMT</pubDate><media:content url="https://munique.net/content/images/2020/01/dark-1845065_960_720.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://munique.net/content/images/2020/01/dark-1845065_960_720.jpg" alt="Recap of 2019"><p>Hey folks, in this post I want to recap what was accomplished in 2019 and what will come next in 2020.</p><h2 id="all-the-way-to-net-core">All the way to .net core</h2><p>We managed to migrate the whole project to .NET Core 3 and also 3.1. This enabled some new opportunities, simplifications and optimizations in the code base. &#xA0; &#xA0; </p><h2 id="docker">Docker</h2><p>With the help of some people of the community we managed to provide a Dockerfile and a publish <a href="https://hub.docker.com/r/munique/openmu?ref=munique.net">images</a> on DockerHub. Additionally, there is a docker-compose configuration to deploy a server including the postgres database with one simple command. You can find the required information in the <a href="https://github.com/MUnique/OpenMU/blob/master/QuickStart.md?ref=munique.net">QuickStart-Guide</a>.</p><h2 id="packet-structures">Packet Structures</h2><p>In the previous posts I talked about how ref structs can be used to parse and write data into a <em>Span&lt;byte&gt;</em> and how these structs can be generated from some data on compile time. I managed to set up a XSL transformation to make that happen, changed almost all of the affected code to make use of these structs.</p><p>The first tool which benefits the most of this yet is the NetworkAnalyzer. There is no new release with it yet, however if you can compile it yourself, you&#x2018;ll be surprised how well it extracts all the available data out of the network data.</p><p>I plan to release a NuGet-Package of the Network-Assembly to make it easier to develop other network based tools for the game, too.</p><h2 id="plugin-system">Plugin System</h2><p>Another great addition this year was the introduction of a plugin system. It allows us to select the right plugins for each connected client separately, depending on the game client version. For the outgoing messages, I basically broke up all the &#x201E;Views&#x201C; &#xA0;(interfaces with a lot of methods) into smaller &quot;ViewPlugIns&quot; which mostly only have one method and send one message. On the incoming side of data, I made a &quot;PacketHandlerPlugIn&quot; out of every existing &quot;PacketHandler&quot;.</p><p>This enabled new possibilities to support other game clients than only season 6. However, the main focus is still season 6. Anyone is free to add their own plugins to support newer or older clients, though. It&#x2019;s even possible to connect to the same game server and join the same game world using two different game client versions. I managed to do that with Season 6 and one of the oldest MU Online versions which you &#xA0;can find - 0.75. At the moment, I have no more interest in extending the 0.75 stuff, because this client is buggy as hell - nobody would ever want to play it in this state. However, in the future I also want to add support for 0.97d where more stable clients are available.</p><p>Of course, there are other use cases for plugins as well. In the code you can find several plugin extension points where custom code can be plugged into.</p><h2 id="ancient-simplemodulus">Ancient SimpleModulus</h2><p>In order to make the server compatible with a game client of version 0.75, I also had to make the network encryption work. It turned out that this version used the very first variant of the SimpleModulus algorithm. After banging my head on the keyboard a few times I managed to find out that this variant uses a much bigger block size than the newer variant. With some luck, I managed to decrypt the client side encryption keys and calculated the server side ones after I adapted the key generator.</p><p>This all also helped me to better understand the algorithm, so I implemented some minor improvements to support longer keys with the same code base, too.</p><h2 id="quest-system-in-progress">Quest System - In progress</h2><p>To get some game features done, I started to begin implementing the quest system. It&#x2019;s currently still incomplete. The first thing which was done is defining all the required network message structures and implementing the corresponding View- and PacketHandler-Plugins. Then I went further and tried to make some sense out of packets I captured a decade ago on the original Global MU Online server and implemented some of the game logic.</p><p>Now, what&#x2019;s missing is the initialization data for all the available quests and a lot of testing ;)</p><h2 id="outlook-for-2020-and-beyond">Outlook for 2020 and beyond</h2><p><em>Disclaimer: I have a lot of ideas for the future, but unfortunately I lack of time. This will probably even get worse because I got a new job starting with this year which will need my full focus. I still hope to get something nice done this year.</em></p><p>My goals for this year are:</p><ul><li>Stabilizing and getting the Network-NuGet out</li><li>Release of an updated Network Analyzer</li><li>Completion of the Quest System</li><li>Getting other game features done which are lurking around in the GitHub issues</li><li>Simplifying/Modularization of the server -&gt; Public API</li></ul>]]></content:encoded></item><item><title><![CDATA[Generating message structs by data]]></title><description><![CDATA[In this post I want to explain how I'm planning to generate ref structs in OpenMU and how this fits into the big picture.]]></description><link>https://munique.net/generating-message-structs-by-data/</link><guid isPermaLink="false">6633f2d02fa90259941c3fc3</guid><category><![CDATA[C#]]></category><category><![CDATA[OpenMU]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Tue, 20 Aug 2019 02:00:00 GMT</pubDate><media:content url="https://munique.net/content/images/2019/08/markus-spiske-gcgves5H_Ac-unsplash_low-1.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://munique.net/content/images/2019/08/markus-spiske-gcgves5H_Ac-unsplash_low-1.jpg" alt="Generating message structs by data"><p>In my <a href="https://munique.net/handling-packet-structures-dotnet/">previous post</a> I have presented my new idea of handling network packets using <em>ref structs</em>. In this post I want to explain how I&apos;m planning to use them in OpenMU and how this fits into the big picture.</p><h2 id="packet-structures-as-data">Packet structures as data</h2><p>The first thing to do is building up something like a database of message struct definitions. I want to save the following stuff for each packet:</p><!--kg-card-begin: markdown--><ul>
<li>Struct Name</li>
<li>Descriptions (&apos;sent when?&apos;, &apos;caused reactions?&apos;)</li>
<li>Header type (C1 etc.)</li>
<li>Code and SubCode, if applicable</li>
<li>Direction</li>
<li>Expected length, if known</li>
<li>Fields, for each field:
<ul>
<li>Index of the field in the struct</li>
<li>Size</li>
<li>Type (Integer, String, Enum etc.)</li>
<li>Byte order (endianness)</li>
<li>Name</li>
<li>Description</li>
</ul>
</li>
<li>Enum Types, for each with their possible values, including names and descriptions. If an enum is used by more than one packet, an external definition should be possible.</li>
</ul>
<!--kg-card-end: markdown--><p>As you see, I want to define more than just what&apos;s required to generate some struct code. I also want to be able to generate a documentation of all message structs. This also means, whenever a message gets extended or needs to be handled by the server, the corresponding data must be extended beforehand.</p><p>A similar, but incomplete collection of this data is already contained within the Network Analyzer project in some XML files. These files could be replaced with the new approach, once it&apos;s complete. How the new data is stored, isn&apos;t clear yet. I have some ideas, but it&apos;s too early to talk about it.</p><h2 id="generating-and-compiling">Generating and Compiling</h2><p>The next step is generating the code out of the available information. The generated code could look like this:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">/// &lt;summary&gt;
/// Is sent when:
///   The client opened an quest NPC dialog and decided to start an available quests.
/// Causes the following actions on the server side: 
///   The server decides if the character can start the quest. A character can run
///   up to 3 concurrent quests at a time.
/// &lt;/summary&gt;
[SentWhen(&quot;The client opened an quest NPC dialog and decided to start an available quests.&quot;)]
[ReactionsOnServer(&quot;The server decides if the character can start the quest. A character can run up to 3 concurrent quests at a time.&quot;)]
[Direction(PacketDirection.ToServer)]
[Length(9)]
public ref struct QuestInitializationRequest
{
    public static byte Type =&gt; 0xC1;

    public static byte Length =&gt; 9;
    
    public static byte Code =&gt; 0xF6;
    
    public static byte SubCode =&gt; 0x0A;
    
    private static readonly byte QuestNumberIndex = 4;
    
    private static readonly byte QuestGroupIndex = 6;
    
    private static readonly byte UnknownFieldIndex = 8;

    private Span&lt;byte&gt; data;

    private QuestInitializationRequest(Span&lt;byte&gt; data)
    {
        if (data.Length &lt; Length)
        {
            throw new ArgumentException($&quot;Expected a span which is at least {Length} bytes long&quot;);
        }
        
        this.data = data;
        
        var header = this.Header;
        if (header.Type != Type)
        {
            throw new ArgumentException($&quot;Wrong header type. Expected: {Type}, Actual: {header.Type});
        }
        if (header.Code != Code)
        {
            throw new ArgumentException($&quot;Wrong header code. Expected: {Code}, Actual: {header.Code});
        }
        
        if (header.SubCode != SubCode)
        {
            throw new ArgumentException($&quot;Wrong header sub code. Expected: {SubCode}, Actual: {header.SubCode});
        }
    }
    
    /// &lt;summary&gt;
    /// Gets or sets the header of this message.
    /// &lt;/summary&gt;
    public C1HeaderWithSubCode Header
    {
        get =&gt; new C1HeaderWithSubCode(this.data); // this could be another ref struct
    }

    /// &lt;summary&gt;
    /// Gets or sets the number of the quest which should be initialized.
    /// &lt;/summary&gt;
    [FieldDescription(&quot;The number of the quest which should be initialized.&quot;)
    [BigEndian]
    public ushort QuestNumber
    {
        get =&gt; this.data.GetWordBigEndian(QuestNumberIndex);
        set =&gt; this.data.SetWordBigEndian(QuestNumberIndex, value);
    }

    /// &lt;summary&gt;
    /// Gets or sets the group of the quest which should be initialized.
    /// &lt;/summary&gt;
    [FieldDescription(&quot;The group of the quest which should be initialized.&quot;)
    [BigEndian]
    public ushort QuestGroup
    {
        get =&gt; this.data.GetWordBigEndian(QuestGroupIndex);
        set =&gt; this.data.SetWordBigEndian(QuestGroupIndex, value);
    }

    /// &lt;summary&gt;
    /// Gets or sets an unknown field.
    /// &lt;/summary&gt;
    [FieldDescription(&quot;An unknown field&quot;)
    public byte UnknownField
    {
        get =&gt; this.data[UnknownFieldIndex];
        set =&gt; this.data[UnknownFieldIndex] = value;
    }

    /// &lt;summary&gt;
    /// Performs an implicit conversion from a Span of bytes to a &lt;see cref=&quot;QuestInitializationRequest&quot; /&gt;.
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;packet&quot;&gt;The packet.&lt;/param&gt;
    /// &lt;returns&gt;
    /// The result of the conversion.
    /// &lt;/returns&gt;
    public static implicit operator QuestInitializationRequest(Span&lt;byte&gt; packet) =&gt; new QuestInitializationRequest(packet);

    /// &lt;summary&gt;
    /// Performs an implicit conversion from &lt;see cref=&quot;QuestInitializationRequest&quot; /&gt; to a span of bytes.
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;packet&quot;&gt;The packet.&lt;/param&gt;
    /// &lt;returns&gt;
    /// The result of the conversion.
    /// &lt;/returns&gt;
    public static implicit operator Span&lt;byte&gt;(QuestInitializationRequest packet) =&gt; packet.data;
    
    public static ThreadSafeWriter StartSafeWriting(IConnection connection, out QuestInitializationRequest message)
    {
        var writer = new ThreadSafeWriter(connection, Type, Length);
        var span = writer.Span;
        span[2] = Code
        span[3] = SubCode;
        message = span;
        return writer;
    }
}
</code></pre>
<!--kg-card-end: markdown--><p>Once this is compiled, we could push this to a NuGet repository. This could also be done automatically by the continuous integration build.</p><h2 id="workflow">Workflow</h2><p>So, when adding a new field or message we first have to get an updated NuGet package. The workflow would be as follows:</p><ol><li>Extend the data, push to Git</li><li>CI will build and push it to the NuGet package repository</li><li>Wait some minutes...</li><li>Update the NuGet package reference in the consuming project (e.g. GameServer, Network.Analyzer)</li><li>You&apos;re ready to use the new message structures</li></ol><h2 id="usage">Usage</h2><p>So, now we have a NuGet package which can be referenced by the GameServer and used in all of the packet handler and view plugins. I want to give a short example about how to use them.</p><h3 id="reading-messages">Reading messages</h3><!--kg-card-begin: markdown--><pre><code class="language-csharp">public class QuestInitializationRequestHandler
{
    public void HandlePacket(RemotePlayer player, Span&lt;byte&gt; packet)
    {
        QuestInitializationRequest request = packet;

        // now you can work with the fields of the request:
        Console.WriteLine(request.QuestNumber);
    }
}
</code></pre>
<!--kg-card-end: markdown--><h3 id="writing-messages">Writing messages</h3><!--kg-card-begin: markdown--><pre><code class="language-csharp">public class SomeClass
{
    public void SendRequest(IConnection connection)
    {
        using (var writer = QuestInitializationRequest.StartSafeWriting(connection, out var message))
        {
            message.QuestNumber = 1234;
            message.QuestGroup = 42;
            writer.Commit();
        }
    }
}
</code></pre>
<!--kg-card-end: markdown--><h2 id="why">Why?</h2><p>You might ask, why should I do that? Well, I see the following benefits:</p><ul><li>A precise and consistent documentation is enforced, can be automatically generated and is even available in code.</li><li>Less errors when parsing and serializing messages, assuming the data is correct</li><li>We get a reusable message library (e.g. network analyzer, client simulator)</li><li>Writing code which writes code is always fun ;-)</li></ul><p>The downsides are obviously:</p><ul><li>Additional work. However, if we want a complete documentation about all message types, this kind of work is required anyway.</li><li>Additional build step. Because we&apos;ll reference a NuGet package for the messages, whenever we extend the messages we have to wait for the build to complete and for the published NuGet package update.</li></ul><p>All in all, I think it would be worth the effort.</p><h2 id="versioning">Versioning</h2><p>As you might know, the OpenMU game server supports network protocols of multiple game client versions since a few weeks. The idea would be to create one struct definition per variant, like we also use different packet handler and view plugins in the game server.</p>]]></content:encoded></item><item><title><![CDATA[Handling packet structures in .NET - a new way?]]></title><description><![CDATA[Parsing and serializing network packets in .NET in a safe, performant and elegant way can be quite tricky. C# ref structs might be the answer.]]></description><link>https://munique.net/handling-packet-structures-dotnet/</link><guid isPermaLink="false">6633f2d02fa90259941c3fc2</guid><category><![CDATA[C#]]></category><category><![CDATA[OpenMU]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Tue, 20 Aug 2019 00:00:00 GMT</pubDate><media:content url="https://munique.net/content/images/2019/08/markus-spiske-gcgves5H_Ac-unsplash_low.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://munique.net/content/images/2019/08/markus-spiske-gcgves5H_Ac-unsplash_low.jpg" alt="Handling packet structures in .NET - a new way?"><p>As you may know, parsing and serializing network packets in .NET in a safe, performant and elegant way can be quite tricky. There are several ways of doing it, but none really satisfied me in all of these aspects.</p><p>Recently, I stumbled across <em><a href="https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref?view=netstandard-2.1&amp;ref=munique.net#ref-struct-types">ref structs</a></em> which got introduced with C# 7.2. As you might know, OpenMU already handles incoming data packets as <em><a href="https://docs.microsoft.com/en-us/dotnet/api/system.span-1?view=netstandard-2.1&amp;ref=munique.net">Span&lt;byte&gt;</a></em>, because it never allocates data on the heap for each packet and therefore reduces stress at the garbage collector. It&apos;s basically a reference to a piece of memory, e.g. a part of a buffer array which might be part of an array pool. A <em>Span&lt;byte&gt;</em> is a <em>ref struct</em> as well. I don&apos;t want to go into details here, but these <em>ref structs</em> have some constraints, such as you can&apos;t hold them in a field of a class or a regular struct. However, you can hold it in a field of another <em>ref struct</em> - that&apos;s what I&apos;m trying to do.</p><p>This allows to define <em>ref structs</em> like this:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">    public ref struct SomeMessage
    {
        private Span&lt;byte&gt; data;

        private SomeMessage(Span&lt;byte&gt; data)
        {
            // You could add length/header type checks here :)
            this.data = data;
        }

        public byte SomeField
        {
            get =&gt; this.data[2];
            set =&gt; this.data[2] = value;
        }

        public static implicit operator SomeMessage(Span&lt;byte&gt; packet)
        {
            return new SomeMessage(packet);
        }
    }
</code></pre>
<!--kg-card-end: markdown--><p>I think you know what I have in mind, don&apos;t you? ;-) Basically, this message struct encapsulates the underlying Span&lt;byte&gt; and offers a simpler access to fields. Additionally, you can now implicitly cast a Span&lt;byte&gt; into a message structure like this:</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">void HandlePacket(Span&lt;byte&gt; data)
{
    SomeMessage message = data;
    Console.WriteLine(message.SomeField);
}
</code></pre>
<!--kg-card-end: markdown--><p>So, the packet handling code doesn&apos;t have to mess around with indexes anymore and isn&apos;t allocating memory on the heap. Of course, now the struct has to know all the field indexes, how these fields are aligned and how bytes are ordered. To solve this, I have another idea which I&apos;ll describe in another blog post.</p>]]></content:encoded></item><item><title><![CDATA[SimpleModulus revisited]]></title><description><![CDATA[About SimpleModulus in very early versions of MU Online.
Bigger block size, more keys and a missing counter.]]></description><link>https://munique.net/simplemodulus-revisited/</link><guid isPermaLink="false">6633f2d02fa90259941c3fc1</guid><category><![CDATA[MU Online]]></category><category><![CDATA[OpenMU]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Wed, 12 Jun 2019 01:00:00 GMT</pubDate><content:encoded><![CDATA[<p>Recently, I was working on getting game client version 0.75 to work with OpenMU. One big obstacle was the SimpleModulus encryption which was somehow different in the earlier versions of MU Online. You can find an analysis of it <a href="https://munique.net/a-closer-look-at-the-mu-online-packet-encryption">here</a>. According to a change log in a leaked 0.65 server source, SimpleModulus was introduced with Version 0.74. Unfortunately, the server source (0.65) didn&apos;t include the source code of SimpleModulus - just a header and a lib file. As I found out later, it wasn&apos;t correct for this client anyway.</p><p>My first attempts decrypting a captured packet using the newer SimpleModulus variant failed miserably. The provided dat-files (they contain the encryption keys) of the game client were too big to make sense for me, too. After a while, I realized they contain more keys than newer variants. The key files were also encrypted (simple XOR) and of course, the encryption key for the files was longer in earlier versions. To find the longer key, I searched in the old main.exe for the first values of the known key and found the others. I had luck that Webzen just shortened the key.</p><p>After a while I managed to decrypt the captured packet and refactored my code so that it&apos;s possible to support both variants.</p><p>To make a long story short, I summarize the difference to newer versions:</p><ul><li>The block size. In 0.75 it used a block size of 32 decrypted and 38 encrypted bytes, instead of 8 decrypted and 11 encrypted. One 16 bit value needs 18 bits in encrypted state (because the multiplication result is bigger), so you can basically calculate the encrypted block size as follows:</li></ul><p>blockSize<sub>encrypted</sub> = (blockSize<sub>decrypted</sub> * 18 / 16) + 2</p><p>Cuando se busca una camiseta de club, es &#xFA;til comparar medidas, corte y detalles de confecci&#xF3;n antes de decidir. Una referencia como <a href="https://www.camisetatienda.com/categoria-producto/camisetas-de-futbol-de-clubes/laliga-ea-sports/camiseta-del-atletico-de-madrid/?ref=munique.net">comprar camiseta del Atl&#xE9;tico de Madrid</a> puede servir para revisar opciones generales sin depender de una sola caracter&#xED;stica. Revisar estos puntos reduce confusiones entre preferencias de uso, comodidad y cuidado de la prenda.</p><ul><li>More keys. Because blocks were bigger in earlier versions, they used more keys. Every 16 bit value is encrypted separately, so it uses 16 keys ( 32 decrypted bytes / 2 bytes).</li><li>The missing counter. The old variant didn&apos;t use a counter in its header. The first byte in the first encrypted block was already the content. Webzen probably has added the counter in a later version to make replay attacks harder.</li></ul><p>It&apos;s pretty interesting that Webzen actually decreased security in one aspect (key size/count) and increased it by a counter. I can just assume that they wanted to reduce the required network traffic and CPU utilization back at the time (~2002). Needless to say, this algorithm isn&apos;t secure, no matter which variant is used.</p>]]></content:encoded></item><item><title><![CDATA[Understanding the relationship between react, redux and react-redux]]></title><description><![CDATA[<p><em>react</em> and <em>redux</em> are basically independent libraries. You can use react without redux and redux without react. What glues both together is the <em>react-redux</em> library. I don&apos;t want to write a full blown tutorial about react and redux, so I&apos;ll keep it short.</p><blockquote>Disclaimer: I&apos;</blockquote>]]></description><link>https://munique.net/react-redux-relationship/</link><guid isPermaLink="false">6633f2d02fa90259941c3fbe</guid><category><![CDATA[Redux]]></category><category><![CDATA[React]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Sven]]></dc:creator><pubDate>Thu, 14 Jun 2018 01:00:00 GMT</pubDate><media:content url="https://munique.net/content/images/2018/09/logo-title-dark.png" medium="image"/><content:encoded><![CDATA[<img src="https://munique.net/content/images/2018/09/logo-title-dark.png" alt="Understanding the relationship between react, redux and react-redux"><p><em>react</em> and <em>redux</em> are basically independent libraries. You can use react without redux and redux without react. What glues both together is the <em>react-redux</em> library. I don&apos;t want to write a full blown tutorial about react and redux, so I&apos;ll keep it short.</p><blockquote>Disclaimer: I&apos;m not an expert. My explanations could be na&#xEF;ve ;)</blockquote><h5 id="react">react</h5><p><em>react</em> is obviously the library which renders the components to the DOM.</p><h5 id="redux">redux</h5><p><em>redux</em> offers mechanisms to handle the state of your application.</p><h5 id="react-redux">react-redux</h5><p><em>react-redux</em> is used to &quot;connect&quot; your React components to the store.<br>It has the &apos;Provider&apos; component which makes your store available in its contained components (your app). It offers the &quot;connect&quot; method which has knowledge of this provided store. In your components you basically define all of your application state and actions in your props and don&apos;t access the store directly.</p><p>Antes de revisar opciones de camisetas, es recomendable tener claras las medidas personales y el tipo de uso previsto. Quienes comparan alternativas pueden usar <a href="https://www.camisetatienda.com/categoria-producto/camisetas-de-futbol-de-clubes/ligue-1/camiseta-del-paris-saint-germain/?ref=munique.net">camisetas deportivas del Paris Saint-Germain</a> como punto de partida para observar tipos de camiseta y detalles b&#xE1;sicos. As&#xED;, la elecci&#xF3;n queda vinculada al uso real de la camiseta y no a una afirmaci&#xF3;n promocional.</p><h6 id="presentational-and-container-components">presentational and container components</h6><p>In React it&apos;s usually a good pattern to separate your components into <a href="https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0?ref=munique.net">Presentational- and Container-Components</a> - this can be achieved easily with Redux.<br>Your presentational component defines as props just what it&apos;s showing and not the source for a data aggregation.<br>An example would be the <a href="https://github.com/MUnique/OpenMU/blob/d116008aab236110a06431174b066d6c2d5fd3f4/src/AdminPanel/content/js/components/LogNotifier.tsx?ref=munique.net">LogNotifier component</a>:</p><pre><code class="language-tsx">interface LogNotifierProps {
    showError: boolean;
    subscribe: (subscriber: any) =&gt; void;
    unsubscribe: (subscriber: any) =&gt; void;
}

class LogNotifier extends React.Component&lt;LogNotifierProps, {}&gt; { &#x2026; }
</code></pre><p>As you can see, the component itself doesn&apos;t define all log entries in the props to be able to figure out itself, if it should show a sign or not. Instead, it just defines <em>showError</em>. The <em>mapStateToProps</em> function which is used by the connect function of Redux, determines <em>showError</em>.</p><pre><code>const mapStateToProps = (state: ApplicationState) =&gt; {
    return {
        showError: anyEntry(state.logTableState, &quot;ERROR&quot;),
    };
}

const mapDispatchToProps = (dispatch: any) =&gt; {
    return {
        subscribe: (subscriber: any) =&gt; dispatch(logSubscribe(subscriber)),
        unsubscribe: (subscriber: any) =&gt; dispatch(logUnsubscribe(subscriber)),
    };
}

const anyEntry = (state: LogTableState, logLevel: string): boolean =&gt; {
    for (var i = state.entries.length - 1; i &gt;= 0; i--) {
        var entry = state.entries[i];

        if (logLevel.localeCompare(entry.Level.Name) === 0) {
            return true;
        }
    }

    return false;
}

export default connect(mapStateToProps, mapDispatchToProps)(LogNotifier);
</code></pre>]]></content:encoded></item></channel></rss>