Elemental Fracture 6.2.0 – Patch Notes – “Do you wanna make some MONEY?!”

Hey, everyone! Doobs here. I’m pleased to announce the next round of additions I’ve been working on: Elemental Fracture 6.2.0, read as balance.mod.bump.

6.2.0 does two things that I think are nothing short of miraculous. It brings back a whole game mode that Proletariat cut, Clash, the Chapter 2 team mode that got deleted out of the menu when Chapter 3 shipped. And it brings back progression: Mage Rank, Class Mastery, the post-match XP screen, the rank-up cards, the MASTERY tab, the Gold shop, cosmetic ownership, reward unlocks. All of it. Every one of those systems has been dead since the shutdown, because every one of them lived on a service that no longer answers the phone.

They answer now.

And that’s not the whole release. Blocked Players and Report Player work natively. The dedicated servers keep a record of cheat commands, and that record files its own reports. Dominion’s end screen doesn’t yank the server out from under you anymore. There are public Hi-scores on the website, built as intuitively and as straightforward in mind. There’s a Cosmetics Manager that lets you either grind for your collection or just pick what you want to wear, your call. Marla got a report inbox, a cheat-flag feed, and the ability to hand out cosmetics by name. And under all of it, a stability pass that touched every single mod we ship, including a crash safety net that catches access violations mid-flight and keeps your game alive.

I’m going to tell you how all of it works.

That’s the thing I want to say up front about these notes. They’re long, and they’re long on purpose, and this time I went further than I ever have: there are sections marked “How we did it” scattered through every part, and they get genuinely deep. Calling conventions. Vtable slots. Garbage collector write barriers. The exact reason a stack pointer where a heap handle belongs took me four separate bug hunts to find. A lot of you have asked how any of this is even possible on a game whose studio doesn’t exist anymore, and I think the honest answer is more interesting than “magic, don’t worry about it.”

So skip those sections if you just want to know what’s new. Read them if you want to know what it actually looks like to resurrect a dead online game one function call at a time.

Here’s the map:


Part 1 of 7: Clash Is Back

Let me be completely clear about what this is, because it’s the single biggest thing in the release and I don’t want it buried under a bullet point.

Clash is a real Spellbreak game mode that you cannot play in Spellbreak. It shipped in Chapter 2. It’s team elimination on the Hollow Lands: you drop in through the portals with your squad, the circle closes like it does in battle royale, but you respawn, you fight over a team score, and the team that puts the other team away wins. Then Chapter 3 arrived, Clash didn’t make the cut, and Proletariat pulled it out of the menu. The build every single one of us runs is the Chapter 3 build. If you own Spellbreak, or you played it on the day it went down, you could not queue this mode. It was gone.

It isn’t gone here. You can queue it right now. It’s in the GAME MODE carousel, sitting between Dominion and Practice, with its own card. You press Play, you pick your class, you drop in with your party on your team, you fight, you respawn, and at the end you get a VICTORY or DEFEAT board with the team scores on it.

That’s a mode nobody has legitimately played since 2021.

What’s actually in the box

How we did it: waking up a cut mode

This one goes deep. Buckle up.

Step one: the mode is still in the build, the menu just can’t see it.

Spellbreak’s mode cards aren’t hardcoded UI. Each one is a data asset, and the menu builds its carousel by enumerating a list of asset IDs. That list, in the Chapter 3 client, contains six entries: the battle royale variants and friends. Clash’s data asset is still in the game files. Its game mode class is still compiled into both the client and the dedicated server. The only thing missing is its name on that list.

So the first move was to inject the card in the card builder. When the native code goes to create the Practice category, our mod builds the Clash card first and hands it over, which is also why Clash lands in the carousel slot right before Practice instead of getting stapled onto the end. That got a card on screen.

Clicking it did nothing, because the click path looks the mode up by category ID and gets back null for a mode the enumerator never registered. That needed its own injection so the click resolves to the real mode info.

One thing worth knowing about the internals: Clash’s internal name is Rumble. The mode string is ?game=Rumble, the map is Rumble_Squad_C, the game mode class is AGRumbleGameMode, the game state is a Blueprint called BP_Rumble_GameState_C. There is no AGRumbleGameMode in the marketing material and there’s no “Clash” in the code. Every time you see Rumble below, that’s Clash.

Step two: the double drop, and four failed fixes.

The first time Clash actually booted, every player dropped in twice. You’d portal in, land, and then get re-dropped from the sky a moment later like the match had started over.

I spent a long time treating this as a timing problem. Spawn a beat later. Defer the restart. Re-possess after the drop. Suppress the second drop. Four different patches, four different angles, all of them either did nothing or broke something worse.

The actual answer was in the class hierarchy, and it’s a beautiful little piece of archaeology. AGRumbleGameMode doesn’t inherit from the battle royale game mode. It inherits from AGVersusGameMode, the arena-style base. That’s fine for Versus scoring, which is exactly why Clash has a team score at all. But it means Clash inherits Versus’s spawn, loadout, and phase machinery too. So the mode ran the battle royale portal drop, because that’s what the map and the closure component do, and then Versus’s inherited restart logic looked at a freshly-landed player, decided that player needed spawning, and spawned them. Twice-dropped, every match, by design, because nobody ever ran this class through this map after the inheritance changed.

The fix wasn’t a timing patch. It was redirecting the divergent game-mode virtuals to the battle royale implementations, so Clash gets Versus scoring and BR spawning, which is what the mode always wanted to be: BR gameplay with Versus scoring. That single change made the mode playable.

Step three: standing up the squad system by hand.

Clash squads didn’t exist. The squad component on the game state was never being built for this mode, so there was no squad to put your party in, no squad HUD, and no portal arrows. Worse, touching it crashed: reading the squad component gave a pointer that faulted at a low offset because the component genuinely wasn’t there.

So we build it. On player spawn, both at post-login and again at the point the game finishes restarting a player, the server mod stands the squad system up and assigns squads itself.

There’s a trap in here that cost real time and is worth writing down because it applies to any Unreal modding: Blueprint class pointers go stale. You can resolve BP_Rumble_GameState_C once, cache the pointer, and have the garbage collector move or reclaim it out from under you. The next spawn call gets a dangling class and fails with “None is not an actor class,” which is a wonderfully unhelpful error for what’s actually a lifetime bug. Anything that resolves a Blueprint class has to re-resolve it or validate it, not trust a cached pointer.

Step four: the team ID bug, which is my favorite bug of the entire release.

Clash players couldn’t see their teams. No ally colors, no ally outlines. And worse: when you got hit, you got no feedback at all, no hurt sound, no red flash, nothing. You’d just start dying silently.

Here’s the thing. The server knew the teams. Team balancing worked. Squads were right. Friendly fire logic on the server behaved correctly. It was only clients that were blind.

Unreal has a generic team interface, and the obvious way to set a player’s team is to call its setter. Our team balancing did exactly that, and it’s not wrong, it’s just… incomplete. That setter writes a single byte that only server-side team logic reads. The value that actually replicates down to clients is a separate property on the player state, at a completely different offset. Two fields, both meaning “team,” one of them local and one of them networked, and we’d been writing the local one.

The way we caught it was by probing the client’s hit-effect gate, the code that decides whether to play damage feedback. It compares the attacker’s team to the victim’s team, and if they match, it suppresses the effect as friendly fire. The probe printed both values. Both players reported team 255. Nobody had a team, so everybody was on the same team, so every hit in the mode was friendly fire, so the game helpfully declined to tell you that you were being murdered.

Mirroring every player-state team write into the replicated property fixed teams, ally colors, ally outlines, and the missing damage feedback in one change. Four visible bugs, one root cause, one line of intent.

Step five: the damage feedback that survived the team fix.

Except the hurt feedback still didn’t work on respawn, which is a problem in the one mode that has respawns.

The character’s BeginPlay binds three delegates: the hurt audio, the red HP flash, and the low-health post-process. It binds them only if the pawn is already an autonomous proxy, engine-speak for “this pawn belongs to the local player.” In battle royale that’s always true, because you possess your pawn before BeginPlay runs. In Clash, a respawned pawn gets spawned first and possessed a moment later. BeginPlay runs, checks whether the pawn is yours, sees that it isn’t yet, and skips all three binds. Forever. Your first life was fine. Every life after that was silent.

The client mod now re-runs those three binds from the health replication callback the instant the pawn’s role flips to autonomous, gated to Clash and safe to run more than once.

Step six: the offhand.

Players were spawning without a secondary gauntlet. The inventory reset on spawn was doing a bare engine inventory wipe and then re-granting the class items, which is most of what you need and not all of it. It now calls the game mode’s own ResetPlayerInventory, which re-grants the default weapon into both the primary and secondary slots, hands out the default inventory items, and runs every game-mode component’s own reset hook. Use the game’s function instead of reimplementing 90 percent of it. Lesson repeatedly learned.

Step seven: the match-end board, and a garbage collector that hates RPCs.

Clash’s Chapter 2 match-end screen is a real widget in the files: UI_MatchEnd_Rumble, with team panels and both scores. But if you let the native match-end flow run in Clash, it runs the battle royale triumph sequence, which loads a match-end level and spawns display pawns, and in Clash that either freezes or crashes.

So we don’t use the native flow. We mount the board ourselves as a clean client overlay: create the widget, add it to the viewport, style it victory or defeat, and show it.

Two hard-won details.

The trigger is a hook on the player state’s “show match end” RPC, but you cannot do the mounting inside the RPC handler. The board’s methods are Haxe, and allocating a Haxe object requires a thread-local stack context that the RPC handler doesn’t have. Every attempt to allocate there access-violated inside the allocator, reading a null context at a small offset. The fix is to set a pending flag in the RPC and do the actual mounting from the HUD’s tick, which does have a valid Haxe context. If you take one thing from this whole document: know which of your threads has a Haxe context and which doesn’t.

And the widget has to be wrapped. Every method on that board expects a Haxe object as this, with the native Unreal object hanging off it. Pass the native pointer directly and it faults every time. Wrap it and it works. That’s the single most repeated lesson in this entire release, and it shows up again in Part 2.

The scores come off the game state, where the server keeps both team scores as adjacent integers, and get written into the board’s own score fields. Victory or defeat is decided by comparing the captured scores against your own team.

What’s still on the list. Clash isn’t finished, it’s playable, and I’d rather ship it than sit on it. The original compact Clash HUD, the little top-right panel from Chapter 2, was deleted in Chapter 3 and needs recreating, so right now you get a repurposed panel. And the live team score number on the top bar is computed by the server but doesn’t always make the trip to the client yet. The mode is fun as hell in the meantime, and I want people in it.


Part 2 of 7: Progression, Returned

Here’s the other headline.

When you finish a match now, you get the game’s real post-match screen. The MATCH STATS tiles fill in. The MAGE RANK bar sweeps up from where you were to where you are. The CLASS MASTERY bar sits next to it in your class’s color. If you crossed a level, the full-screen RANK UP card takes over the screen and waits for you. Back in the menu, the MAGE RANK header shows your level, and the MASTERY tab, which was cut out of the Community Version entirely, is back with every class ranked.

None of that is a mod drawing a fake progress bar. It’s Spellbreak’s own UI, running its own animations, reading numbers that Elemental Fracture computed from matches you actually played.

Why it was gone

Spellbreak never calculated your XP on your machine. Not once. At the end of a match the dedicated server tallied your accolades, the backend added them to your account, and the client was handed the totals purely to display them. That’s the right architecture and it’s why your level was never something you could edit.

It’s also why the shutdown killed it stone dead. With no backend, the client takes a path the original developers built for exactly this situation: it exits the match without post-game stats. No screen. No XP. No levels. The code that draws all those bars has been sitting in the binary this whole time, waiting for a data structure that never arrives.

Getting it back needed three things to be true at once.

Why it matters that it’s built this way: your level isn’t a file a mod increments. It’s a projection of your match history, rebuilt from an append-only log. If I ever change a formula, every account gets recomputed from the same record, and nobody’s history is lost or invented. That’s the difference between a number you can trust and a number that’s just there.

On the post-match screen

How we did it: three designs, one screen

This was the hardest single piece of work in the release. It went through three complete designs before one held up.

Design one: force the widgets visible. The post-match widgets exist in the client, so the first attempt hooked the moment the game would normally bail out and just forced the progression widgets to show.

It painted a bar! It also left dangling object references inside widgets that had never been initialized, and Unreal’s garbage collector, which runs on a task-graph thread, would eventually walk into one and take the client down seconds later with a stack trace pointing at nothing useful. And no animations played, because the animation state machine was never set up either. Some of you tested that build. That’s why the early crashes happened after the match instead of during it.

Design two: build the thing the widgets are waiting for. The post-match menu reads one Haxe structure off the game instance. If a valid one is sitting there, the whole native flow runs: full initialization, the reveal, the level-up animations, the rank and class sections, all of it, for free. So instead of forcing widgets, call the game’s own constructor for that structure and store it.

The calling convention ate me alive. That constructor returns its result through a hidden output pointer, and every setter on the result takes a handle to the heap object the game’s allocator made, not the address of the local buffer that received it. I passed the stack address. It looked identical in a debugger. What actually happened is that passing a stack pointer into a garbage-collected object’s setter access-violates inside the GC write barrier, the bookkeeping the collector uses to track pointer writes, and that fault got swallowed. The field stayed garbage. The menu then ran its entire setup on nonsense. That one mistake presented as four different bugs over several days before I found it.

Three more traps in that design, each worth a sentence:

Design three: let the server build it. Design two worked, but it computed XP on the client, and you should never trust a client with your progression. So the final design moves construction to the dedicated server.

At match end, the server walks every human player, resolves that player’s post-match component, and wraps the native component in its Haxe object wrapper, because the constructor demands the wrapper as this and a raw native pointer is exactly what an earlier attempt was missing when it faulted at a garbage address. Then it creates a real Haxe anonymous object with the award flags set true, so the accolade tally actually pays out, calls the game’s own constructor with the wrapper, the output slot, and the context passed by reference, and ships the result down the ClientSetPostGameStats RPC.

Your client receives the same bytes it would have received in 2021 and runs its own code on them. The XP number in that structure is what our metrics service records as the match’s XP. Server-authoritative, native rendering, no compromise.

How we did it: the animation sequencing

This deserves its own section because it’s genuinely strange.

A progress bar’s fill color is not a property you set. I assumed it was for embarrassingly long. It’s written by the bar’s own UMG material-track animations: a reveal animation paints it, a progress animation sweeps it in white, and a fade animation transitions it to the final color. Setting the color and playing the animation means the animation immediately overwrites you.

So the sequence is: write the class’s three overlay colors, pulled from the class’s default object, onto the fill material before the reveal. Play the reveal. Let the sweep paint. Then apply the class gradient at the end.

The beats advance on the widget’s real animation-finished callback, so the pacing matches the game’s own timing rather than a guess. And the beats themselves are driven from the main menu backplate’s Haxe tick component, which took two failed attempts to arrive at. A Win32 timer callback has no Haxe stack context, so anything it touches faults. The WebSocket heartbeat pump never self-sustained because the pong lands inside its own throttle window. The menu’s own tick has a valid context and fires every frame. Use the game’s heartbeat, not your own.

Small details that mattered: the full-screen level-up card has a native auto-close, which we suppress so the card waits for you, and we re-enable Continue. And there’s a “reward animations finished” byte on the card that only gets set by a callback the persistent card never reaches, so the first Continue press was being silently swallowed. We set that byte ourselves when we suppress the auto-close, so your first press does what you expect.

The MASTERY tab

How we did it: the tab was removed from the menu’s navigation, not from the game. We generalized the same injection that brought FRIENDS back in 6.1.0, re-injecting the nav entry and forcing the menu’s availability check true. Rows get re-ranked by hooking the pane’s show event through its vtable slot, so they’re populated before the first paint rather than visibly popping in. One subtle one: the re-rank at standing-adoption time threw a Haxe exception at boot when it ran before a valid world existed, so it’s guarded on the world now.

The main menu and in-match

The header widget is fed by a detour on the game’s own GetPlayerXP, which hands back the authoritative total from our push instead of the zero sitting in the local blob. That way the native level display and the native XP curve agree with the backend, rather than us fighting the UI with a second source of truth.

Where the XP comes from

How the backend keeps score: the metrics service stores every match result as an immutable event, and everything you see is a projection rebuilt from those events. Mage XP and class XP are two of those projections. Ratings, hiscores, presence and the cheat record are others. Change a curve, run one rederive, and every account rebuilds from the same log. There’s also an XP adjustments ledger, so when staff grant or remove XP they’re appending an event rather than editing a total, which means even a manual correction survives a rebuild.


Part 3 of 7: Your Collection

Progression was only half of what the dead backend owned. The other half is your collection: which outfits, emotes, trails, badges, cards and titles you have, how much Gold is in your wallet, what’s in the shop. In 6.2.0 all of that moves to the server too.

Your inventory used to be a JSON file in your Saved folder that any text editor could rewrite. Now it’s an account record that the game gets handed at login and every time it changes.

The PlayerBlob is server-authoritative

How we did it: the original backend delivered this document through an UpdatePlayerBlob message the client handles natively, so we build that document server-side out of three sources: XP from the progression projection, wallet and inventory from the account record, and whatever the client legitimately owns, like loadouts, from its upload.

The two hooks the game uses to load and save its local file are both detoured. The load renames the stale file aside so the clean template loads instead. The save doesn’t write to disk at all: it serializes the live in-memory blob through the game’s own JSON printer and sends it to the appserver as an upload. Using the game’s serializer instead of writing our own is the only reason the round trip is byte-compatible.

The server then strips every server-owned key out of that upload before merging, so nothing the client claims about its XP, Gold or inventory is ever believed. And pushes get deferred while the post-match sequence is running, because a fresh standing arriving mid-animation was clobbering the flourish’s starting point and stranding it halfway.

What you own, and why

The original game gave you cosmetics three ways: some were yours from the start, some you bought, some you earned by ranking up. We mirror that faithfully, plus one deliberate addition.

How we rebuilt the catalog, because this was genuine detective work: the full list of 1,006 cosmetics comes from the client’s own template blob. Which of them were ever sold comes from the store offer data, 924 of them. Which were earnable comes from the reward tables, 64. Subtract, and what’s left is the set nobody could obtain. Strip out event, league, tester and creator titles plus the test assets, and you land on 18 items. Those are the community grants.

Titles needed extra care. My first pass treated every title as an event reward, which was wrong: 30 of the 67 titles were store items, including all the class titles and the Vowbreaker ranks. Only the remaining 37 are actual participation rewards. The “event” tag you see on the website reflects that split exactly.

And here’s a great example of why a change that sounds trivial isn’t. Making talents pay Gold meant rewriting the reward tables that live in each class’s default object as lazily-built lists. Writing through the accessor changed a copy, and the MASTERY tree responded by dropping the rows entirely. The fix forces the lazy getters to build the real tables first, then rewrites the entries in raw memory. That took two attempts, because the primary asset ID structure is 24 bytes with a 12-byte name at offset 12, and my first version copied 16 bytes and read the name at offset 8. Result: empty asset IDs, and the game politely refusing to add the reward. Reading the real layout out of the debug symbols fixed it in one shot.

The Gold shop

What things actually cost now, and why

This is the part I want to spend a minute on, because the numbers matter more than the plumbing.

Everything is earned with coins, and I’ve rescaled the prices way, way down from what they were. Not tweaked. Rescaled. A skin that used to sit behind a 3,000 coin wall costs 120 coins here. That’s roughly three rank-ups. Three. Every price in the shop moved by that same fixed rate, so the whole catalog came down together and nothing is stranded at an old number.

The reason is simple. The original prices were built for a live-service economy with real money in it, where the coin grind existed partly to make the store look appealing. There’s no real money here and there never will be, so a price tag whose whole job was to nudge you toward a wallet has no job left. What’s left is the fun part: play, rank up, buy the thing you wanted.

This is the path for people who want to earn their cosmetics. That’s a real thing a lot of you want, and it should feel good rather than feel like a second job. If you don’t want any part of it, the Cosmetics Manager in Part 3 is right there and I’m not going to make you feel weird about using it. Turn it on, tick what you like, go play. For those players, coins stop being a gate entirely and rank-ups just become number go up, which is honestly a perfectly good reason to rank up.

And the most important rule in the whole economy:

Nothing that touches gameplay is locked behind progression. Not one thing.

Every class is unlocked at level 1. Every talent is unlocked at level 1. You are not grinding for power, you are not grinding for options, and you will never load into a match against someone who has a mechanical advantage because they’ve played more than you. Every node on the mastery tree that used to be a talent unlock now just pays you coins to spend on cosmetics instead. The tree still fills in, it still tracks what you’ve mastered, it just pays out in things you wear rather than things that hit harder.

Progression here is a record of what you’ve played and a way to earn what you look like. That’s all it is, and that’s all I want it to be.

How we did it: the store calls two backend operations, fetch offers and purchase, and the appserver answers both in the exact shapes the client parses. The client’s offer fetch needed two dead-code patches, because the shipped binary short-circuits it when no backend is configured. And the purchase refusal carries an error kind field, which the client’s own purchase closure matches against its localized modals, so a refusal shows the real dialog instead of a generic one. Answering in the game’s vocabulary instead of inventing our own is what makes it feel like the store rather than a store.

The Cosmetics Manager on the website

Some of you want to grind for your collection. Some of you had a collection already and have zero interest in re-earning it. Both of those are completely reasonable, so the website lets you choose.

How the icons got there, and this was a fun rabbit hole: 908 of the 1,006 cosmetics on that page show their real in-game icon. Getting them meant reading each cosmetic’s Blueprint out of the game’s pak files to find its display name, rarity, description and icon texture path. Then the textures themselves, which are stored in cooked UE 4.22 format that no off-the-shelf extractor will decode for this game, so we wrote a small decoder: read the cooked texture header, find the block-compressed mip, hand the BC data straight to an image library. Card art gets exported as full-width strips for the hover preview. Titles all share one generic banner in-game, so they render as the game’s own title backplate with the text on it. A community asset pack covered a handful the paks didn’t.

Why the default is progression-on: the progression system is the game, and I’m not going to bury it. But this game shut down once already, and telling someone who lost a four-year collection that they have to re-grind it is a lousy way to welcome them back. Giving both groups the honest version of what they want, with a clear label on which is which, beat picking for everyone.


Part 4 of 7: Safety

A living game needs two things: a way to walk away from someone, and a way to tell a moderator. Spellbreak had both, in the friends tab and the party menu and the match-end screen. Both were dead. They’re alive now, and the second one has teeth.

Blocked Players

How we did it: the client’s entire block system reads one field of the PlayerBlob. Emit that list from the server and list filtering, the comms gate, invite suppression and squad avoidance all light up on their own, because the code was always there. That’s the recurring joy of this project: a huge amount of Spellbreak works perfectly the second something tells it the truth.

The Block and Report buttons, though. Those took a real trick.

Both native handlers compile down to the same empty function, because the linker folded every identical empty body into a single stub, and that stub is shared with completely unrelated callers including a presence setter and a debug dump. Detour it and you fire for all of them. Useless.

So we don’t hook the function. We patch the four call sites. Friends-tab Report, friends-tab Block, the party placard Block, and the match-end report-your-killer. Each of those sites has already resolved its target player by the time it makes the call, so a small code cave at each one tail-jumps into a thunk that reads the player ID out of the register it’s already sitting in and emits the WebSocket frame. Every patch verifies the site really is a call to that folded stub before it writes a byte.

Unblock has no call site at all, so that one hooks the Blocked Players tab’s action selection instead. The logic is airtight: the tab’s only action is Unblock, and a player already on your block list can never be the target of a Block, so a selection there is unambiguous.

Then the squad rule needed its own fix on the server, and here’s the archaeology: the dedicated server has no block check compiled into it whatsoever. The symbols exist on the client and are simply absent from the server’s debug data, because the original team ran that check on the backend. Two players who’d blocked each other and queued separately would happily land on the same squad.

So the client mod splices your block list into the blob it hands the game server at login, and the server mod reads both players’ lists at the exact point the server decides whether a squad can accept a player. That decision already had a detour on it, and two detours on one address chain unpredictably, so the block test shares the single owner of that hook rather than stacking a second one. It’s also tighten-only: it can turn an acceptance into a rejection, never the reverse, and any failure degrades to allow.

Report Player

How we did it: the client never learns a match ID, because the join token doesn’t carry one, so the server resolves the shared match itself out of both players’ histories once the reported player leaves. A sweeper on the router releases held reports, pulls the match context and the recent window from metrics, and posts to Marla’s keyed receiver. A hung server can’t swallow a report either, because a hold timeout releases it with whatever telemetry exists.

The fold-instead-of-refuse behavior came out of a real usability bug: the game’s WebSocket frames for these messages carry no sequence number, which means the client never sees an error reply. A refused duplicate looked exactly like success, so a player would report twice, see nothing happen either time, and reasonably conclude the button was broken.

The cheat record

This one is new, and it’s deliberately quiet.

How we did it, including the design that failed: the first version hooked the server’s cheat manager and recorded absolutely nothing. Took a while to understand why, and it’s obvious in hindsight. A remote player’s cheat manager runs on the player’s own client, and each cheat gets relayed to the server as a Cheat_Server* RPC on the player controller or one of its components. The server’s own cheat manager never runs for a remote player at all.

So we hook those receivers. Every script-visible function in Unreal carries a pointer to its native implementation, and swapping that pointer routes every call through a thunk that logs and then chains to the original. The invoking player gets resolved by walking the class chain, controller to component owner to pawn to cheat-manager outer, until something yields a player state. And the parameters get decoded out of the script frame’s locals, which is what makes the record say what was asked for rather than just which command fired. “SetHealth” is ambiguous. “SetHealth 9999” is not.

Writes happen on a dedicated writer thread so the game thread never touches the disk, the file ships with the match result, and it’s cleared before every launch so matches can never inherit each other’s commands.

I want to be really clear about intent here. This is not surveillance of ordinary play. Nothing about your movement, your aim, or your spellcasting is recorded anywhere. What’s recorded is a short list of developer commands that a normal client never sends in its life. The record exists so that when somebody does send them, nobody has to take anyone’s word for anything.


Part 5 of 7: Dominion, the Queue, and the Party

Dominion

How that end-screen bug hid for so long: the supervisor had a grace hold written for exactly this purpose. It also had a condition that skipped the hold if a restart had been requested. And the terminal-phase handler requested the restart. So the flag that was supposed to protect the end screen was being set by the very code path that needed protecting, every single time, and the hold quietly opted itself out.

The graceful path now leaves the process running and polls the router’s live session count. When it’s been zero for three seconds the server recycles, with a 120-second cap and a delayed terminate as a deterministic fallback so a hung server can’t pin a slot forever.

The queue and readiness

One of these hid a genuinely funny regression in the test builds. The queue driver resolves the main-menu game mode by class name, and the class is GMainMenuGameMode, not MainMenuGameMode. Spellbreak drops Unreal’s A prefix but keeps its own G. With the wrong name, the lookup failed silently and Play could not reach a server at all. An hour to find, one character to fix.

Party and presence

How presence labels work: the game renders presence through a localization helper that maps a key to display text. The appserver sends the native keys the game already understands, plus a mode and a phase, and a hook on that helper composes the label. The mode and phase come from the router, which asks the server manager which server your session is on and what phase it’s in. Again: speak the game’s vocabulary and its UI does the work for you.

Equipment

How we did it: the decision lives in a function that answers “should this pickup equip immediately on use,” so a one-branch patch there sends replacements down the hold path and leaves empty slots instant. The prompt relabel took three attempts because I had the calling convention wrong: the real function takes six arguments with a hidden return pointer and by-pointer parameters, and the five-argument version left one argument as garbage, which crashed the moment a player queued up. Reading the actual member layout out of the debug symbols sorted it.


Part 6 of 7: The Website and Marla

Hi-scores

People asked for leaderboards, and I wanted them to look like leaderboards people already know how to read.

How we did it: the boards are one more projection off the metrics event log, computed over opted-in accounts and served through the public sidecar with a short cache, so a popular page never touches the metrics database directly. The website fetches the feed server-side over the private network, doesn’t follow redirects, caches board pages briefly and failures for 30 seconds, and deliberately keeps user-supplied name lookups out of its own cache since the sidecar already handles those.

The Cosmetics Manager

Covered in Part 3. One detail worth knowing: a “select everything” save is roughly a thousand checkboxes, which is more form fields than PHP accepts by default, and the failure mode is silent truncation. So the page joins your picks into a single field before submitting, and the checkboxes remain purely as the no-JavaScript fallback.

The home page

Staff tools

Data exports

The map fix is a nice little story. The supervisor was watching the server log for the engine’s “bringing world up for play” line to learn the map name, and that line never reaches the supervisor’s log reader at all. Meanwhile the match-tracker mod had been writing the world URL into the match-end file the whole time, right there, unread. Sometimes the data you need is already in your hand.

Marla


Part 7 of 7: Stability

This is the part you never see and always feel. 6.2.0 added a lot of native code surface, and most of the work between the test builds and this release went into making sure that surface can’t take your game down.

The crash safety net

This is the single most important reliability change in the release, and it’s the piece of engineering I’m proudest of.

How the safety net actually works, because it’s genuinely wild and I want it written down somewhere.

Right before a protected native call, the mod captures the CPU context. A vectored exception handler is registered first in line, ahead of everything else. It ignores every exception unless an access violation happens while a protected call is in flight on that same thread. When one does, it copies our captured context over the exception’s context record and tells Windows to continue execution.

The processor resumes at the capture point. Not after the call, at the capture point, with a “recovering” flag set, so the wrapper takes the other branch and returns its default instead of the native result. The faulted stack frames just evaporate. No unwinding, no handler code running inside a broken state, no destructors firing on garbage.

The trade-off is honest: you get a skipped hook, which might mean a stale label until the next repaint. The alternative was your game closing.

Two things make it correct in practice, and both are the kind of detail that bites you at 3am. The wrapper is marked never-inline and reads its state through volatile accesses, because a function that can return twice completely violates the assumptions of a compiler that thinks it returns once, and an optimizer will happily cache a value in a register that our context restore is about to clobber. And the handler is inert unless armed, so ordinary exceptions anywhere else in the game are untouched.

There’s a second layer for the crashes the net can’t catch, the ones that land on the garbage collector’s thread where our per-thread handler has no jurisdiction. A breadcrumb file names the last risky step the mod took on the game thread, overwritten as we go, so after any crash, even an asynchronous one, that file tells us what ran last. And every sub-step of the post-match sequence has a bisect switch, off via a marker file or an environment variable. When a crash lands somewhere invisible, the breadcrumb names the neighborhood and the switches let us disable one step at a time until it stops. That’s how the post-match screen got debugged at all.

Performance and logging

On those object-table scans, because this is the most common performance mistake in Unreal modding and I made it several times: the engine keeps every live object in one global table, and the easiest way to find a widget is to walk that table matching on class and name. It’s easy, it works, and it’s catastrophic when it runs on every repaint of a menu that repaints constantly.

The version that ships finds each widget once, caches the pointer, and validates it cheaply on use by checking the memory is readable and the vtable word still points where it should. When validation fails, it rescans. You feel the difference as menus that stay crisp while the friends list refreshes behind them.

The backend

The match result itself deserves a paragraph. The server mod writes the final scoreboard to a file at match end, atomically, via a temp file and a rename, and the supervisor consumes that file. The reason it’s a file and not a network call is that the dedicated server gets recycled immediately after a match, so the metrics post absolutely cannot depend on a process that’s about to stop existing. The supervisor prefers the file, falls back to a live read over its control socket, and falls back again to its last poll cache. Every match you finish gets recorded exactly once.

And before any of this shipped: every service change was rehearsed against a copy of the live account database. 249 real accounts, with their tokens, friendships and bans, run through the new schema setup and verified byte-identical afterward. That rehearsal caught a migration bug, an ordering mistake that would have left the reports table missing two columns until the second restart, and it got fixed before anything touched production. Then four independent code reviews went over the full diff, one each for the services, the server mods, the website and the bot, and every finding from those reviews is in this build.

Getting started

Already playing? Relaunch the launcher. It pulls 6.2.0, patches the game, updates the mods. One thing to expect: your first login after the update seeds your server-side collection, so your outfits will be the base set plus whatever the reward tables say you’ve earned, and you’ll be Mage Rank 1. That’s not a bug. That’s day one of a progression system that’s finally keeping score.

New here? Grab the launcher from elefrac.com, make an account with an email and password, sign in, press Play. Everything in this document is in the box, Clash included.

And if something in here doesn’t match what you’re seeing in-game, that mismatch is exactly the report that makes the next version better, so please tell me. Every match you play now counts toward something. Every bug you find is one fewer for the next person.

Go play Clash. Nobody’s been able to for four years.

See you in the arena. ⚡

← Все списки изменений