Case Study · Software Preservation · Reverse Engineering

«Դառնալ Նախագահ», Smart Systems LLC, 2007

Becoming President, Again

How I took a dead Windows game — a Flash movie welded to a .NET brain, its assets AES-encrypted, its Armenian text trapped in a font from another era — and rebuilt it as a static, plugin-free web app that runs in any modern browser.

Play it — davidmcarati.info/president

25encrypted .dat files decrypted (AES-256-CBC)
17,282lines of legacy C# + AS2 recovered by decompilers
3,435lines of TypeScript in the reimplemented "brain"
~30lines of ActionScript injected into the original SWF
40FSCommands bridged between Flash and the new host
0plugins, installers, or servers required to play

Chapter 0 · The Artifact

A game with two brains, and neither of them working

Become President is a turn-based election-strategy game published by Smart Systems LLC in 2007, written for an Armenian audience. You run a presidential campaign against three AI opponents: courting ten voter blocs with slogans, promises and media buys, drafting a national budget, cutting coalition deals — and, if you neglect your poll watchers, losing the election to outright falsification. It shipped on CD, installed like any Windows program of its day, and today it does not start on anything.

The first clue was sitting right next to the executable, in trace.log:

Fail: Class not registered (HRESULT 0x80040154 REGDB_E_CLASSNOTREG)
  at System.Windows.Forms.AxHost.CreateInstance()
  at Become_President.Form_Main.Form_Main_Shown(...)

That error told me most of what I needed to know about the architecture before I had decompiled a single byte. This is not a Flash game, and not a .NET game — it is both, stapled together. A ~256 KB VB.NET WinForms executable holds all the game rules, data loading and cryptography, and it embeds the bundled Flash 9 ActiveX control (Flash9c.ocx) to render the actual game. DirectShow (quartz.dll) plays the intro and election-night videos. The OCX was never registered on this machine, so the Flash control can't be instantiated, and the main form dies the moment it's shown.

I started calling the design "two brains and a bridge":

You could fix the crash with regsvr32 on a period-correct Windows box. But resurrecting a dead plugin on a dying platform isn't preservation — it's life support. The goal became a real port: the original artwork and animation, running in a modern browser, forever, with nothing to install.

Chapter 1 · The Vault

Every asset was encrypted — and the key was hiding in plain sight

All of the game's content lives in 25 files under Data\ with the extension .dat — 16 MB of opaque ciphertext. No SWF headers, no XML, nothing file could identify. The content had to come from the brain, so I decompiled the brain: ilspycmd recovered the full, unobfuscated C# source of the executable — 36 files, 8,138 lines, with original class names intact.

Two files answered everything. PESymetric.cs implements the crypto: AES-256-CBC with PKCS7 padding, key and IV derived via PBKDF2 (SHA-1, salt "This is my sa1t" — note the deliberate digit — and a distinctly 2007-era iteration count of ten). And Globals.Gides() holds the password. Rather than store it as a string literal where any hex editor would find it, the developers stored a list of integers and reconstructed the password at runtime by alternately subtracting and adding one:

gides[]541061071061161211131055047
index parity−1+1−1+1−1+1−1+1−1+1
password5kjkszpj10
The runtime key reconstruction in Globals.Gides(): even indices −1, odd indices +1, yielding the AES password 5kjkszpj10. Obfuscation, not cryptography — but nineteen years ago, against casual copying, it worked.

It took me a few hours of staring at kjkszpj before it clicked: that is the GTA: San Andreas weapons cheat code. Some developer in 2007 needed a password, typed the one string his fingers already knew by heart, and wrapped it in 510. Easily the most fun discovery of the whole project — and a reminder that the human sitting behind a cipher is usually the most interesting part of it.

I wrote a standalone PowerShell decryptor (_tools/decrypt.ps1) that rebuilds the key the same way, decrypts every .dat, and names the output by sniffing magic bytes — CWS means SWF, < means XML, 30 26 B2 75 means Windows Media. All 25 files decrypted cleanly on the first run, and sorted themselves into three groups:

GroupFilesWhat came out
Flash 2 scn.swf (3.4 MB — the entire game UI) and msc.swf (763 KB — a music player). Both SWF v8, ActionScript 2.
Video 2 int.wmv and elc.wmv — the intro and election-night clips, WMV3/WMA2, 800×600.
Game data 21 Plain, human-readable XML: 3,891 rows of slogans (571), promises (1,225), quiz questions (683), offers (496), negotiation clauses (342), plus difficulty tuning, budget coefficients, media pricing…
The green light

Both SWFs are Flash Player 8 / AVM1 — the older ActionScript 2 virtual machine, which Ruffle (a Flash Player emulator compiled to WebAssembly) supports essentially completely. The moment the SWF version byte read 8, the port went from "maybe" to "planned."

One decrypted SWF also settled the rendering question for good: parsing scn.swf's tag table revealed zero bitmap tags. All of the 2007 art — the game board, seasons, characters, buttons — is pure vector: 690 shape definitions, ten embedded fonts, 28 embedded sounds, and not a single JPEG. That's a big part of why the game renders faithfully under emulation, and why it scales razor-sharp to a 4K display that didn't exist when it was drawn.

Chapter 2 · The Contract

Mapping every message that ever crossed the bridge

With assets in hand, the port needed a precise inventory of how the two brains spoke to each other. The Flash movie is unusable without the .NET host — every button ultimately fires an FSCommand and waits for the host to push new state back. So I went through the 81 KB of decompiled Form_Main.cs line by line and catalogued the entire protocol:

DirectionMechanismCall sitesPurpose
Flash → hostFSCommand(cmd, args)40 commandsEvery player action: clicks, menu choices, save/load, exit
Host → FlashSetVariable(name, value)134Push state into timeline variables the SWF reads
Host → FlashGetVariable(name)37Read player inputs back (name, options, budget sliders…)
Host → FlashCallFunction("<invoke…>")27Invoke a named AS2 function — always zero-argument
Host → FlashTSetProperty(path, 7, v)2Toggle _visible on one movie clip (property index 7)

The protocol has a distinctive shape: the host never passes arguments to functions. Instead it pushes dozens of variables — PlayerMoney, SloganText1..5, YouthPers — and then calls a no-argument function like OpenSlogans that reads them. That "write everything, then ring the bell" style turned out to matter enormously in the next chapter.

The same pass documented the save system (ten slots, the whole Game object XML-serialized and AES-encrypted with the same key), the options file, and the high-score table — everything the browser port would need to re-home in localStorage.

A sample of the 40-command FSCommand surface
FSCommandArgsHost action
PlayerLevelNumberlevel 0–2Start a new game at Beginner / Advanced / Expert
ChooseSloganindex 0–5Commit the round's slogan choice
ChoosePromisescsv of 5 flagsCommit up to five campaign promises
TryYourSkillsAnswerClickanswer 1–5Answer a quiz question for sector sympathy
DraftBudgetDoneValidate the 12-line budget (must sum to 85–115)
StartNegotationsopponentOpen coalition talks with a rival
SaveGame / LoadGameslot 1–10Persist / restore, with overwrite confirmation
NextLevelClickEnd the round once all ten steps are complete
ExitSystemClose the app (a no-op on the web)

Chapter 3 · The Pivot

Ruffle doesn't speak ActiveX — and why that only cost thirty lines

Here the plan hit its one architecture-defining obstacle. Ruffle deliberately does not implement the legacy Flash plugin/ActiveX API — SetVariable, GetVariable, CallFunction, TSetProperty are all unimplemented (ruffle#7868). That is every single host→Flash channel in the table above. The inbound direction was fine — Ruffle's addFSCommandHandler covers all 40 FSCommands — but the host had no way to talk back. For a day, the fallback plan (boot an entire Windows image with real Flash inside the v86 x86 emulator — heavy, but guaranteed) looked uncomfortably plausible.

The way out came from a detail of how Flash itself was built: the ActiveX CallFunction mechanism is the same transport that ActionScript's ExternalInterface rides on. For the original host's calls to have ever worked, the SWF must already register those function names via ExternalInterface.addCallback — and Ruffle supports ExternalInterface for AS2 movies completely. So I decompiled scn.swf with JPEXS into 1,046 ActionScript files and grepped. Every one of the 27 function names the host calls — PlayGameBoard, UpdateBoard, OpenSlogans, ShowElectionResult… — was already registered, sitting there waiting for a caller that no longer existed.

The reduction

Function channel: works unmodified via Ruffle's callExternalInterface. FSCommand channel: works unmodified. The only thing genuinely missing was the humble variable channel — so the entire bridge problem reduced to injecting three tiny callbacks into the SWF's first frame.

Using JPEXS in script-import mode, I appended a shim to frame 1 — carefully preserving the original frame's two existing lines — that registers bp_setVar, bp_getVar and bp_setVis, then reports its own health back over the one channel already known to work:

// injected into scn.swf frame 1 (after the original two lines)
var __ei = "no";
try {
   if (flash.external.ExternalInterface.available) {
      ExternalInterface.addCallback("bp_setVar", _root, function(n, v) { _root[n] = v; });
      ExternalInterface.addCallback("bp_getVar", _root, function(n)    { return _root[n]; });
      ExternalInterface.addCallback("bp_setVis", _root, function(p, vis) {
         var t = eval("_root" + p.split("/").join("."));   // "/game_board/info/old_but"
         if (t) { t._visible = vis != 0; }                 //   → _root.game_board.info.old_but
      });
      __ei = "yes";
   }
} catch (e) { __ei = "err"; }
fscommand("bp_ready", __ei);

A small favorite: bp_setVis translates the .NET-era slash path (/game_board/info/old_but) into an AS2 dot path with split("/").join(".") — thirty years of Flash path syntax bridged in one expression. The injected SWF is exactly 1,277 bytes larger than the original, and the repo keeps two full re-decompilations of the patched movie (_tools/verify_scn, verify_scn2) so the injection itself is auditable — you can diff the two iterations of the shim design.

Ruffle (WASM)scn.swf · AVM1 · the 2007 UI, unchanged*
bridge.tsfscommand in · ExternalInterface out
host.tsport of Form_Main · 40-case dispatch
game/*.tsGame · Player · engine · AI
data/*.json21 tables · 3,891 rows
storage.tslocalStorage saves
video.tsHTML5 overlay
armenian.tslegacy font encoding
The port's architecture. The original Flash movie still does all the rendering; only the .NET brain was rewritten. (*unchanged except the ~30 injected lines.)

Chapter 4 · The Rewrite

Porting the brain — bugs and all

With the bridge solved, the remaining work was a disciplined translation: 8,138 lines of decompiled C# (plus 9,144 lines of decompiled ActionScript as the reference for what the SWF expects) distilled into 3,435 lines of TypeScript. No framework, no canvas code — the TypeScript is purely the brain:

ModuleLinesRole
game/model.ts1,551Domain model: Game, Player, sectors, slogans/offers/promises, serialization
game/engine.ts689Per-turn actions, opponent AI, the election itself (72 methods)
host.ts5571:1 port of Form_Main — the FSCommand switch and all state pushes
main.ts + bridge.ts202Boot, Ruffle wiring, the interop layer
storage.ts / video.ts / types.ts308localStorage persistence, video overlay, 13 enums ported value-for-value
game/rng.ts + game/armenian.ts100VB6 randomness and legacy text encoding (below)

Numeric fidelity down to the rounding mode

A simulation game is its arithmetic. To keep the ported game feeling identical, the TypeScript reimplements Visual Basic's actual numeric behavior rather than approximating it: VB6's 24-bit linear congruential Rnd generator (seed 0x50000, multiplier 0x43FD43FD), VB's Int() floor-toward-negative-infinity, and .NET's banker's rounding (MidpointRounding.ToEven) everywhere the original rounded money to two decimals.

Fidelity also meant porting the bugs. The original's loop that deals five slogans from the pool increments its index even after removing an element — so it can skip one — and its "does this action have any positive influence" test contains a self-referential accumulator that almost certainly wasn't what the 2007 author intended. Both quirks are replicated, with comments explaining why:

"NB: the original increments the index on every iteration even after a RemoveAt (so an element can be skipped). Replicated for fidelity."

— web/src/game/model.ts

The alternative — "fixing" them — would silently change the deck-dealing distribution and the AI's spending decisions, and the port would no longer be the same game.

The game inside the data

The 21 XML tables (converted to JSON at build time) are the whole political economy: ten voter sectors with money coefficients from 8 (Businessmen) down to 0 (Youth, Unemployed, Housekeepers bring votes but no cash); three difficulty tiers where Expert stretches to nine rounds while cutting your starting purse and the opponent handicap; 14 media formats priced per sector across 140 tuning rows. And one rule I can't resist quoting from the decompiled source: if you finish below your trustee quota, GetElectionsFalsificatedOrNot() rolls Int(10 * Rnd() + 1) > 3 — a flat 70% chance the election is stolen. Fail to staff the polling stations and no amount of popularity saves you. In a 2007 game about Armenian presidential politics, that mechanic is its own commentary.

Chapter 5 · Integration

War stories from the emulator boundary

A ported brain and a bridged SWF still had to be made to behave. Every fix in this chapter came from a symptom that made no sense until it did.

The command that vanished

Ruffle invokes the FSCommand handler synchronously, from inside its own execution. Call back into the player from that stack frame — even a harmless getVar — and the call is silently dropped. The fix is one line with a long comment: every incoming command is deferred with setTimeout(0), which moves the handling outside Ruffle's call stack while preserving FIFO command order. The original COM host never had this problem; single-threaded ActiveX re-entrancy just worked. Emulator boundaries have their own physics.

The frozen money counter

Early on, the player's money never changed on screen — though saves proved the engine was computing it correctly. The decompiled SWF explained it: the board's text fields don't watch _root variables; they re-read them only inside UpdateBoard, and only where a prev_* shadow copy differs. Setting variables moves no pixels. The host now calls UpdateBoard after every push, exactly as Form_Main.UpdateBoardVariables always had.

The race that only failed the first time

The board's button tooltips read variables pushed once at boot. Click "New game" fast enough and the board opened before the push landed — blank buttons, but only on the first visit, since by the second visit boot had long finished. Classic misleading symptom; the fix is to re-push the tooltips on every board entry.

Replacing five seconds of sleep with one event

The original host's readiness strategy was brute force: try CallFunction, catch the failure, Thread.Sleep(100), repeat up to fifty times. The port replaces the sleep loop with the injected shim's fscommand("bp_ready", __ei) hello, then runs a round-trip self-test at boot — write bp_test = "123" through the bridge, read it back, log the verdict — so a broken bridge announces itself in the console instead of as mysterious downstream silence.

Dialogs that belong to the game, not the browser

window.confirm in the middle of a 2007 vector world is jarring, so the port drives the SWF's own modal instead: set _root.MessageText, call MessageBoxOK or MessageBoxYesNo (the choice comes from the same msg data table the original used), and await the answer as an FSCommand — wrapped in a promise so overwrite-confirmations sequence correctly.

One inconsistency was left in place deliberately: the SWF's quiz music emits a lowercase fscommand("stopMusic") that no host — the 2007 one included — has ever had a case for. It hits the port's default: branch and logs itself. Some bugs are heritage.

Chapter 6 · The Alphabet

Cracking an Armenian encoding from mojibake

The strangest problem in the project wasn't binary at all. The decompiled C# was full of string constants like "ÀÝïñÇñ ϳñ·³Ëáë" — Armenian text, but not in Unicode. The game predates practical Unicode-Armenian workflows: its text is single-byte ANSI whose codepoints map to glyphs in legacy Armenian fonts embedded in the SWF (ArTarumianHelvetica ExtraBold, Arial Armenian, ArmTitle — 350 KB of vector glyphs across ten fonts).

Matching the mojibake against phrases the game had to contain cracked the scheme. "ÀÝïñÇñ ϳñ·³Ëáë" had to be «Ընտրիր կարգախոս» — choose a slogan. Lining the bytes against the letters revealed an interleaved alphabet: uppercase and lowercase Armenian letters alternate byte positions —

Byte0xB20xB30xB40xB50xB60xB7
GlyphԱաԲբԳգ
Ruleuppercase letter i = 0xB2 + 2i  ·  lowercase letter i = 0xB3 + 2i

Which produced the project's most user-visible problem: a modern Armenian keyboard types real Unicode (U+0531…U+0586), the embedded font has no glyphs there, and the name-entry field renders rectangles. The fix runs on both sides of the bridge:

A final wrinkle worthy of the era: the injected placeholder text itself had to be written as String.fromCharCode(178, 221, 225, 245, 221, 185), because the decompiler's script-import would mangle raw high bytes in source. Encoding problems all the way down — solved by refusing to let any tool interpret the bytes at all.

Chapter 7 · Shipping

Video, autoplay politics, and a deliberately boring deployment

The two DirectShow clips decrypt to WMV, which no browser will touch, so they were transcoded once with ffmpeg to H.264+AAC with +faststart — and came out smaller than the originals (the intro dropped from 4.2 MB to 2.3 MB). They play in a full-screen HTML5 overlay above the Ruffle stage: the intro fires without await so the SWFs load behind it; the election clip is awaited, with both the success and error paths routing to the results screen, matching the original's catch fallback.

The best web-specific war story is autoplay. Chromium was lenient at cold boot, but Brave blocks autoplay-with-sound outright — and the failure mode compounded: the rejected video.play() shows a ▶ Play button, but the overlay's click-to-skip handler would then treat the very click meant to press Play as a skip, dismissing the intro before it started. The fix is choreography: skip is only armed once the playing event actually fires, and the Play/Skip buttons stopPropagation(). As a deliberate web-era mercy, the election clip is skippable here; the 2007 one made you watch.

Music kept its original architecture, which I find charming: msc.swf is an entire Flash movie whose stage is 2×2 pixels and whose only cargo is one 768 KB MP3. Rather than re-implement it in Web Audio, the port runs it exactly as the original host did — as a second, hidden Ruffle player driven by PlayMusic / StopMusic calls, with volume flowing through the same _root.MusicVolume variable into AS2's Sound.setVolume().

The build output is a fully static dist/ — HTML, JS, two SWFs, two MP4s, and Ruffle's two ~13–14 MB WASM cores. No server-side anything; Node exists only at build time. It deploys to bare Apache, S3, or GitHub Pages, works from a subdirectory (base: "./"), and the only operational notes that matter are a application/wasm MIME type and gzip on the cores (a ~4× transfer cut). Saves live in localStorage as plain JSON — ten slots, options, and the top-ten high-score table, seeded on first run from the decrypted 2007 defaults.

Epilogue · The Scorecard

What 2007 needed, and what 2026 needs instead

ConcernOriginal (2007)Port (2026)
RenderingFlash 9 ActiveX control in WinFormsThe same SWF, emulated by Ruffle (WASM)
Game logicVB.NET, ~8,100 lines decompiled3,435 lines of TypeScript
Host → FlashSetVariable / CallFunction (ActiveX)ExternalInterface + 3 injected shims (+1,277 bytes)
Asset protectionAES-256 with a key hidden as ±1 integersNone — the point is that it's readable
VideosDirectShow playing decrypted WMV temp filesHTML5 <video>, transcoded MP4
SavesXML → AES → Saved Games\*.savPlain JSON in localStorage, same 10 slots
Blocking the right-click menuA Win32 window-proc subclass swallowing WM_RBUTTONDOWNcontextMenu: "off" — one config flag
Waiting for Flash to loadUp to 50 × 100 ms blocking sleepsOne fscommand("bp_ready") event + a boot self-test
Armenian textLegacy single-byte font encoding, ANSI everywhereSame bytes preserved, plus a live Unicode→legacy shim for input
DistributionCD, installer, COM registrationA folder of static files on any web host

The number I keep coming back to: roughly 17,300 lines of recovered legacy source, studied and distilled into 3,435 lines of TypeScript plus about thirty lines of injected ActionScript — while the original 2007 movie, byte-for-byte except those thirty lines, still draws every frame. That's the shape of preservation I was after. Not a remake, not a video of the game, but the game — its vector art, its quiz-show music stings, its 70%-rigged elections — running natively in the one runtime that seems likely to outlive us all: the browser.