Star Wars: Force Commander
September 23, 2026 · View on GitHub
Static recompilation of Star Wars: Force Commander (LucasArts, 2000) from its shipping Win32 binary to native C.
Built on the pcrecomp toolchain, and next to xwa — same publisher, same year, same studio's tooling.
Project Status: It is in game. The campaign's briefing room, in 3D.
build/focom.exe game/Focom.exe --run \
--nocond 25 3 83 --varat 182 40 12 1 \
--mousescale 1.25 --clickat 120000 --clickgap 15000 \
--click 227 145 # Single Player
--click 577 427 # forward -> SINGLE PLAYER page
--click 227 145 # Campaign
--click 577 427 # forward -> the Imperial hangar, in 3D
The last click does not open another page. Every UI rectangle disappears
except the cursor and one arrow, and the frame becomes a fully 3D interior:
curved hangar walls, a ramp, and a holographic briefing table with a blue ring
and a green tactical display on its console. 1,331,273 primitives rasterised
by present #24150, 306,510 of 307,200 pixels non-black, holding and fading
in over hundreds of frames. That is 0007 - EmpireHangar /
stateEmpireHangar out of the exe's own 63-entry state table at 0x008595E0 --
where a Force Commander campaign begins.
Two flags in that command are cheats, and both say so in their own comments.
--nocond steps over a disc check for a disc that is present; --varat
answers a name check for a name the front end will not accept typed input for.
They stand in for two bugs that are located to the point of proof and written
up in docs/STARTUP.md: the font sheets are stored upside
down, and a keystroke reaches the window procedure but not the name field.
No screenshot of it here: a rendered frame of the game's own artwork is retail content, and this repository does not carry any.
Before that: the splash screen, drawn by lifted code.
The first thing it ever drew. 640×480, pixel-exact, and every pixel is produced
by sub_00401770 — Force Commander's own splash dialog procedure, lifted to C —
running on a host that owns nothing but the window. The title text is the
game's too: CreateFontA sized from the window rect, then TextOutA twice,
black then yellow one pixel up and left, for the drop shadow. (A capture of it
used to live here; it is the game's own splash art, so it went.)
$ build/focom game/Focom.exe --splash
import bridges: 307
IAT slots self-patched: 307
mapped game/Focom.exe: 0x00400000 + 5632000 bytes
WM_INITDIALOG -> lifted 0x00401770
[shim] LoadBitmapA(102): 640x480 8bpp, 256 colors, 308264 bytes
[shim] StretchBlt dst=0,0 640x480 src=0,0 640x480 rop=00CC0020 -> 1
real shims installed: 71
Two functions lifted to get there (the dialog proc and __chkstk), 71 of
307 imports with real bodies. Getting in game was a different order of work —
see The road to in-game.
| recovered function starts | 34,674 |
byte coverage of .text | 98.8% |
| classes recovered from RTTI | 567 |
| vtables / virtual methods | 1,121 / 5,436 |
| methods attributed to one class | 4,832 |
| functions attributed to a source file | 738 |
| game-specific classes | 6 |
| lifted, whole binary | 39,038 functions, 10.7M lines of C, 0 errors |
| real import bodies | all 307 bridged; DirectDraw 7, Direct3D 7, DirectInput, DirectSound, DirectPlay, SMUSH as COM/DLL shims |
Read docs/RECON.md first. It is the primary P1 document:
the RE3D/Ronin/GEAPI/DX7 namespace map, the renderer's interface seam,
and the decoded .rpk, .znm/SMUSH and .M3D formats. This README covers P0,
P2 and what was added afterwards; it does not repeat RECON.md.
P0: what the binaries are
| Binary | Size | .text | Built | Role |
|---|---|---|---|---|
Focom.exe | 4,943,872 | 3,936,882 | 2000-03-03 | the game |
Force.exe | 106,496 | — | 2000-02-29 | launcher |
Smush.dll | 147,456 | — | 1999-02-27 | LucasArts SMUSH video (4 exports) |
mss32.dll | 328,704 | — | 1999-01-04 | Miles Sound System |
FocomSetup.dll | 843,776 | — | 2000-02-29 | installer |
MSVC 6.0, MSVCP60.dll with 82 imports — STL-heavy C++, and both the CRT and
the standard library are dynamic, which is a smaller classification problem
than a statically linked CRT (cf. Monster Truck Madness).
No DRM, no .reloc, fixed base 0x00400000. 3.94 MB of .text makes this
the second-largest target in the collection; only Rise of Legends (13.25 MB) is
bigger, and that one is explicitly a stress test rather than a project. This is
roughly 1.5× X-Wing Alliance and 4× Crimson Skies.
The import table understates the runtime surface
307 imports across 11 DLLs, and not one of them is DirectX:
KERNEL32 63 MSVCP60 82 MSVCRT 67 USER32 44 mss32 22
WINMM 8 GDI32 11 ADVAPI32 5 ole32 3 SHELL32 1 DINPUT 1
DDRAW.DLL, SMUSH.DLL, FEELIT.DLL and DINPUT.DLL are LoadLibrary'd;
DirectPlay, DirectMusic and the video path arrive through CoCreateInstance.
RECON.md makes this point from the GUIDs in .rdata; the import table confirms
it from the other side.
The consequence for bring-up, as written at P0: runtime/compat/win32_compat.h
sorts 275 imported Win32 APIs into keep/shim/SDL2/stub, and that machinery
does not reach an interface pointer called through a vtable. Budget for
interface shims. That is what happened: src/runtime/ddraw_shims.c builds the
DirectDraw 7, Direct3D 7 (all 49 device methods), DirectInput, DirectSound and
DirectPlay objects inside the target's address space, and raster.c is the
software rasteriser behind them.
P2: the classification is free, because RTTI was left on
567 classes out of one tools/cpp/rtti.py run. For scale: Black & White has
569 types and they were recovered by hand — that project is what all of
tools/cpp/ was built for. Same class count here, with names, vtables and
inheritance chains, in seconds, because LucasArts shipped a release build with
RTTI enabled.
RECON.md maps the four engine namespaces. What it left as "plus Subsystem,
GamePPMultiplayer, GamePPVisBase and friends" is 237 of the 567 classes, and
they sort cleanly by name prefix:
| Family | Classes | vtable slots | What it is |
|---|---|---|---|
GamePPGlobal* | 145 | 4,274 | Platform/game layer — resources, events, managers, and the script AST |
GamePPSys* | 67 | 1,650 | Framework core: Object, Process, Event, Message, Resource, Library, Variable, State, Undo |
GamePPVis* | 25 | 1,053 | A visual editor. Shipped in the retail binary. |
GamePPMultiplayer* | 20 | 655 | DirectPlay |
Focom* | 6 | 161 | The entire game-specific surface |
GamePPProd* / App / Log | 3 | 247 | Startup and logging |
82% of all 567 classes have a recovered base class. (Namespace counts here are
by outer scope after stripping template arguments; they run a little higher than
RECON.md's for RE3D and Ronin, which is a counting-method difference rather
than a disagreement — RECON.md's table is the one to quote.)
Six classes are game-specific. Six, out of 567.
FocomStartup FocomEmitterObjectCallback
FocomPassCallback GamePPFocomSystemManager
GamePPFocomLandscapeResourceData GamePPFocomLandscapeResourceType
Plus a handful of global-scope render-pass callbacks — SkyObjectCallback,
ShadowObjectCallback, RadarObjectCallback, CFOWOverlayCallback
(fog of war), GlobalWeatherObjectCallback, CSmushMemRenderCallback.
This is the Gunman Chronicles shape — 78% of the binary is the SDK, and only
499 of 3,990 functions need real work — except Gunman needed a four-pass
classifier to prove which functions were SDK, and that classifier is all of
tools/classify/. Here the answer is in the type names, and classify/ does
not need to run at all.
So this is a thin game on a large unreleased in-house engine, and the recompilation is mostly an engine recompilation. The payoff is bigger than one title: anything else built on Ronin/GamePP gets cheap afterwards.
Mission logic is a visual script, not native code
38 GamePPGlobal* classes are AST nodes with their own vtables:
If Else ElseIf For EndFor While EndWhile Switch Case CaseOr CaseRange
CaseOrRange DefaultCase Do Loop LoopForever LoopVar EndLoop EndIf And
Or Assert Comment NOOP Stop CallParent EnumDef ArrayTypeDef ConstTypeDef
MessageTypeDef ResourceTypeDef EventFunctionTypeDef Argument{Expr,VarDec,…}
With GamePPSysCodeBlock, GamePPSysCodeContainer, GamePPSysLibraryProc,
GamePPVisCodeBlock, GamePPVisCodeClipboard, the info.pro / code.bin
filenames in .rdata, and Subsystems as the first asset category in the
.rpk string table, that is a complete in-house visual scripting language
and its editor, compiled into the shipped game.
Force Commander therefore lands on the same shelf as Encarta 97, the Magic
School Bus and Prodigy: the interpreter is the code and the behaviour is
data. Recompiling the binary gets the VM; code.bin and the Subsystems
members of the .rpk are the other half of the project.
Audio has two managers, GamePPGlobalSysMilesSoundManager and
GamePPGlobalSysiMuseSoundManager — iMuse, LucasArts' interactive music
system, alongside Miles. And GamePPGlobalSysFEELitMouseManager goes with the
FEELIT.DLL string: Immersion force-feedback. Genuinely obscure.
Correction: RTTI is a symbol source, not a recovery pass
This was asserted the wrong way round here and is worth recording properly.
tools/cpp/rtti.py's docstring says the method addresses are proof of function
entry points, which is why this is worth running before disassembly rather than
after, and the obvious inference is that seeding a 3.94 MB C++ binary with
1,121 vtables should find functions a branch scan cannot. Measured, it finds
essentially none:
| RTTI virtual methods | 5,436 |
| already in the disassembler's catalog | 5,427 |
not found by disasm32 | 9 |
| …of those, new functions rather than alternate entry points | 0 |
All nine land inside a body the sweep had already decoded, against 34,674
recovered starts at 98.8% byte coverage. vtable_scan.py measured against RTTI
as truth found 91.6% of its methods and proposed 688 more, of which 109 were
absent from the catalog — so the more generous of the two passes contributes at
most 109 addresses out of 34,674.
That is now measured on two binaries that differ in nearly everything that should matter (Trespasser: 7.8 MB, has a linker map; this: 3.9 MB, none) with the same answer both times. E9 seeding plus the fixpoint is what finds the functions. See pcrecomp docs/CONSOLIDATION.md items 5 and 6 for the full write-up.
What RTTI is worth here is names, and on a stripped retail binary with no
symbols of any kind that is the whole point: 4,832 methods attributed to one of
567 classes plus 738 functions attributed to a source file means 5,570 of
34,674 functions (16%) stop being sub_004A1C30. That is what makes lifted
code readable and a crash stack worth reading.
analysis/rtti_seeds.json, analysis/vtable_seeds.json and their union in
analysis/seeds_union.json (6,124 addresses) are kept as a names/methods index,
not as a seeding input.
The road to in-game
The splash screen was reachable because it is a closed loop: one dialog procedure, two GDI calls, a resource already inside the exe. Nothing else in this binary is like that. The inventory below was written when only that loop worked; the status column is what happened since.
| # | Work | Status |
|---|---|---|
| 1 | The function catalog. Needed before any closure can be computed. | done — 38,908 entries |
| 2 | Lift the startup closure and walk the failures. | done, and then some — the whole binary lifts, 10.7M lines of C, 0 errors |
| 3 | 58 __thiscall MSVCP60 shims — "the wall". | done — real basic_string with the MSVC 6 COW layout. What is left of MSVCP60 is data symbols (vtables, npos, _Nullstr), not code |
| 4 | DirectDraw 7 over the DIB. | done — COM object model in the target address space, 640×480 16bpp primary surface, device enumeration, GetCaps |
| 5 | The .rpk reader. 274 MB, and nothing loads without it. | done — tools/rpk.py; 9,554 members tile the archive with no gap and no overlap |
| 6 | Stubs that must not lie: DirectInput, Miles (22), SMUSH, DirectPlay. | partly — Miles and WINMM are still stubs; nothing has needed them yet |
| 7 | Whatever the lifter gets wrong across 34,674 functions. | two found, two fixed — see below |
Item 3 was called the real gate and item 7 the real risk. That was the wrong way round. The STL came out in a day; the lifter bugs were the expensive part, and both of them truncated function bodies silently, so the generated C compiled, ran, and quietly did less than the original:
- a fixed 512-byte window in the recursive descent, which cut every body with a
longer straight run — WinMain lost everything past 0x004012AE and fell off
its own end into the CRT, which then stored the result through a clobbered
ebp; - the tail-call guard being given the catalog's 4,234
aliasentries, so ajmpto a second entry point of the same function ended the descent, and the arm reachable only through it became an unresolvedITAILat runtime.
A third was chased and turned out not to exist. 4,025 bodies appear to end on a
lea, which cannot end a function, and that looked like the catalog's end
being too small — but an indirect tail call (jmp [eax+0x18], which is every
vtable thunk in this binary) emits as an ITAIL dispatch block, so the last
instruction comment in the body is the one before the jump. Measured properly,
14 of 38,908 bodies never reach a terminator, all one-block fragments at
false starts, and raising the bound for them changes the generated C by nothing.
The catalog's end is trustworthy. run_lift.py now reports the number and
leaves it alone.
The other finding is that not one of the sixteen blockers between the splash
screen and here was a defect in the lifted code. Every one was an infidelity in
a hand-written Windows or CRT reimplementation. docs/STL-GATE.md argues from
that to a 32-bit host, and the argument has only got stronger.
Where it actually stops
The game runs its own startup, driven by its own script -- see
docs/STARTUP.md, which is the other thing that had to be
recovered: Focom.ini is not a settings file but a directive list, and the disc
ships it as zero bytes because the installer writes it.
CheckAppMutex FORCE -> CreateMutexA
CheckCD <installdir> -> GetVolumeInformationA, label FOCOM_1
LoadAppFileName ... -> reads Resource/appname.ini
ShowLoadingPanel -> CreateDialogParamA(101), dialog procedure
0x00401770 -- lifted -- and it paints
InitBase -> builds GamePPProdBase, creates its registry key
RPKDir / Workspace -> opens forcecommand.rpk, 288,585 reads,
compiles 1,713 object templates
Movies / Music / GameFiles / Players
Run 2 1 6 -> starts the "Trasse - Day" section as a process
on a Ronin worker thread
HideLoadingPanel
CreateThread is honoured -- one thread runs lifted code at a time, with the switch points at the blocking shims -- and the section's script really runs.
Then RE3D comes up: a 640x480 16bpp mode, a flipping primary with a back
buffer, an attached Z buffer, a Direct3D 7 device, texture formats enumerated,
textures uploaded, render state and materials set. All 49 IDirect3DDevice7
methods are implemented, with the purge count of each taken from the headers.
Nothing is rasterised yet -- Clear really clears the render target and the
DrawPrimitive family accepts and counts its vertices.
Getting that far took one discovery and four corrections, all of the same
shape, and docs/STARTUP.md has them: the game's screen gate reads
dwDeviceRenderBitDepth at a hardcoded device + 0x454, which is the HAL
slot of the four device descriptions it keeps inline, so offering only the RGB
software device made it skip the entire screen and renderer setup and run its
front end for 61 frames with nothing to draw on.
And then it draws. Every reading of the stall pointed at the boot
script -- one context on Wait Forever, one on a Wait If that never cleared
-- and the script was fine. Its thread was being shot, by one shim:
u32_MsgWaitForMultipleObjects passed nCount = 0 with no handle array, and
read its timeout from ARG(2), which is bWaitAll. The return value of that
function is a position in the handle array, so with nCount = 0 "a message
arrived" comes back as WAIT_OBJECT_0 + 0 -- byte-for-byte what "handle 0
signalled" looks like. Handle 0, for every Ronin thread, is its stop event, and
a boot process lives exactly as long as its boot thread. The first stray mouse
message ended the game's first section; the frame count that varied run to run
(61, 68, 164) was just how long it took one message to arrive.
With the handles passed through, BeginScene, SetTransform, SetViewport,
Clear, EndScene and Flip all run, CDD7FSScreen::Present is reached, and
the window shows frames:
[present] #1 surface 0x10D62090 640x480: 307200 of 307200 pixels non-black
[present] #3 surface 0x10D62090 640x480: 0 of 307200 pixels non-black
[present] #4 surface 0x10D62090 640x480: 307200 of 307200 pixels non-black
And then it renders. src/runtime/raster.c is a software rasteriser for the
DrawPrimitive family -- half-space edge functions, gouraud diffuse, one
modulated texture stage, alpha blending, the alpha test, a 16-bit depth test --
and with it, plus two things that had to be right first, the game boots into
its front end and draws its title screen, presenting around 3,000 frames a
run.
The two: 130 vtable slots were never lifted, because MSVC's
virtual-inheritance adjustor thunks are eight bytes that nothing calls and
nothing falls into, so RECOMP_ICALL answered the slot with eax = 0 and a
renderer took that null for a render stage. And
IDirect3DDevice7::Load was a no-op, which is how every texture stayed black:
the game locks a system-memory surface, writes the image, and calls Load to
move it into the texture it draws with.
Two more were found by looking at the frames rather than the code. A bit depth
is not a pixel format -- the font atlas is ARGB1555 and decoding it as 565
painted a solid red panel over the game's own title -- and Clear ignored
D3DCLEAR_ZBUFFER.
And it is a menu. One value explains why it was not: the front end draws its
menu with a vertex diffuse of 0x00FC0000 -- a real red, and an alpha of zero --
against ALPHAOP = MODULATE, ALPHAARG1 = TEXTURE, ALPHAARG2 = CURRENT.
Reading that literally multiplies every glyph by zero, and with the alpha test
the game also sets, 6 of 500 UI draws painted anything. Taking alpha from the
texture alone puts Single Player / Multiplayer / View Installation / Show
Credits on the screen.
And it draws the scene behind the menu. The frame used to be a menu panel
on black, and the black had a name: BeginStateBlock/EndStateBlock/
ApplyStateBlock were no-ops. Direct3D RECORDS state between Begin and End
rather than applying it, so with recording unimplemented every state in every
block was applied the instant it was recorded and never applied again -- and
the live state at every draw was whatever the last block to be BUILT had
wanted. The 3D backdrop was therefore drawn with the 2D front end's state:
blending on, depth writes off, and an alpha test it never asked for. With
state blocks implemented, the AT-ATs walk, the stormtroopers advance and the
explosions burn behind the menu.
And it responds to a click. That took finding out that there were two
windows -- visibly two, a fullscreen white one with "Force Commander" in
yellow at the bottom (the game's own, with its GDI loading panel) and a
smaller one beside it with the frames. The game's is the one Windows delivers
input to. There is one now: host_present blits into the game's window and
the host's is never shown when the game is going to make its own. An earlier
note blamed cross-thread GDI for making that too slow to use, at 39 presents a
run; the real culprit was ShowWindow, which SENDS its message to a thread
that never pumps. Presenting into the game's window measures 4,400 presents
in 130 s.
--uimap prints the hot rectangles so click coordinates are read off the
draws instead of guessed:
140,130 175x 30 centre 227,145 Single Player
538,388 78x 78 centre 577,427 forward
The mouse turned out to be DirectInput only -- the game's own window
procedure dispatches messages 7..0x100, 0x101 and 0x102..0x112 and throws
everything else, so WM_MOUSEMOVE and WM_LBUTTONDOWN reach it and are discarded
-- and the cursor is driven by relative deltas divided by a sensitivity, which
--uimap calibrated by reading the cursor's own 51x51 quad back:
--mousescale 1.25 puts an aim at 227,145 on the pixel.
With that, Show Credits works: sixteen script blocks that had never run start and the panel fills with a scrolling credits roll. View Introduction works, and so does exit, which brings up a confirmation page with a button each side.
Single Player and Multiplayer do not. The menu is a 217-line script block
-- a Switch on the row with a Case per item -- and Single Player's Case runs
its eleven set-up calls to completion: it sets a Bool, an Int and an Enum
and posts a Message. The front end's 309-line page controller then reacts,
running ten lines it had never run. And the page does not change: click Single
Player, then the exit arrow, and the quit confirmation comes up instead of a
return to the menu, which is what settles it. docs/STARTUP.md has the trace
and the five things this is not -- including one earlier reading of the same
trace that --switchtrace proved wrong.
What it waits on is now located to a line. --scripttracefrom MS --scripttracefor MS bounds the script trace to the seconds around a click,
and tools/stepdecode.py collapses 30,000 [step] lines into the sequence
each block ran with a class name per line. A 25-line block that had been
parked on a Wait If for the whole run wakes up on the click and enters a
While ... Wait ... EndWhile that never exits. --nodedump read that
While's condition straight out of the bytecode --
GamePPGlobalSysWhile::Execute keeps the operand count in [args], the jump
target in [args+8] and the operands at args+0xC -- and it is one operand,
variable slot 83.
And then everything got its name. --threadlist scans the heap for thread
records and finds 4,011 script threads, 197 of them in the front end's own
container, so the whole front end is a map: Opening Screen 111 lines
running, Cursor, Click, Wait, Monitor running, every other page a
state marker with an empty body and an Enable X event function behind it --
and the thread that spins is For CD. tools/gtxvars.py reads the
script's declaration table out of Trasse - Night/Opening.gtx and names slot
83 "Min CD Number". Resource/appname.ini, all 108 bytes, is three
strings, and the third is "Please, insert the Force Commander CD to proceed".
So it was never a player profile and never a page that fails to draw. The front end reaches its disc check.
And the check can be stepped over. GamePPGlobalSysWhile::Execute
initialises its result to zero before evaluating, so writing 3 into the
six-bit operand-count field of any control-flow line forces its condition
false -- no operands touched, and no need for the operand grammar that had
been in the way. --nocond 25 3 83 does it to that one line, identified by
its block's line count, its line number and the variable its first operand
reads, because a script line's address is different every run and several
25-line blocks exist.
With it, the front end navigates: SELECT PLAYER NAME, then ENTER PLAYER
NAME, then the player page's forward arrow -- which fails a name check and
draws "No Name Selected". That check's result is variable slot 12, assigned by
a 7-line block called Check That Name Exists and read at line 40 of the
182-line handler, so --varat 182 40 12 1 answers it at that one line and
nowhere else. (--varpoke 12 1, which pins the slot everywhere, segfaults the
game in sixty-three writes: the whole front end shares it.)
Then SINGLE PLAYER, then Campaign, and the next click is not a page at
all. Every UI rectangle disappears and the frame becomes a fully 3D interior:
curved hangar walls, a ramp, a holographic briefing table with a blue ring and
a green tactical display. 1,331,273 primitives a frame, 306,510 of 307,200
pixels lit, holding and fading in over hundreds of frames. 0007 - EmpireHangar out of the exe's own state table -- where a Force Commander
campaign begins.
Two things were missing along the way and neither was the cause. --nolib
proves no line the click takes names an unregistered subsystem. And the
install was short 260 MB: Resource\Music and Resource\Movies had never
been copied off the disc, and the music file names turn out to be the exact
state names in the exe's own 63-entry state table -- 1202 - MasterScreen.imu
is stateSinglePlayerScreen. tools/iso_extract.py copies them out with
their real names, which means reading the image's Joliet tree rather than its
truncated 8.3 one. timeSetEvent delivers its callback now too, on a host
thread with its own target stack and machine state, because iMUSE runs on that
timer.
Three other things had to be right for that, and docs/STARTUP.md has them:
IDirect3DVertexBuffer7 (the game locks one on its first rendered frame),
host_present blitting straight to the window rather than through
UpdateWindow (which blocks on another thread's message pump, and hung the
process on the first Flip it ever issued), and a fourth lifter bug -- a body
can continue past an int3 when something jumps over it, and four dropped
leaders came out as tail transfers to VAs nobody lifted.
SMUSH.DLL is shimmed too, so the intro movie is skipped rather than silently
disabling the module: the exe asks GetProcAddress for four exports and the
loader answers one NULL by FreeLibrary-ing the whole thing.
Where it goes next
In order, and the first two are the ones that matter:
- Walk the hangar to a battle. The campaign's briefing room renders; the
rest is the same click-at-a-
--uimap-rectangle work the front end took. - The font sheets, stored upside down -- which is what makes that work
slow, because the screen cannot be read.
--vtxdumpproves the glyph quads are right (x-span/u-span is 3.75 on every glyph, matching y-span/v-span), so it is the texture content, and a sheet's alpha channel dumps as ASCII running bottom to top with every glyph inverted. The font files are 8-bit BMPs and the model textures 24-bit through the same fill function,sub_00774640. - A keystroke that reaches the name field. The messages arrive --
DispatchMessageAshows WM_KEYDOWN, both WM_CHARs and WM_KEYUP with the right scan code on the game's own pumping window -- and ten typed characters still leave the field empty, so the break is inside the game's key-to-script plumbing. Fixing it retires--varat. - The alpha, underneath that.
--drawprobesettled what the forced opaque diffuse costs: the panel is FVF 0x112, which has no diffuse at all, so it looks the same either way, and the TEXT is FVF 0x142 with a vertex diffuse alpha of zero in every batch sampled across a run. Both readings are wrong; the honest fix is a real texture-stage evaluator. - Miles (21 entries). Still stubs, and the install now has its music, so there is something for them to play.
- Stub
GamePPVis*entirely. 25 classes and 1,053 vtable slots of editor that a first playable does not need. Free. - A 32-bit host.
docs/STL-GATE.mdmakes the case for loading the real 32-bit DLLs instead of reimplementing them, since every blocker so far was a reimplementation infidelity. - DirectPlay is a dead service. Whatever replaces it is a design decision, not a recompilation one.
- Disc 2 has not been unpacked.
Smush.dll needs no reverse engineering — RECON.md has the container decoded
and ScummVM has implemented SMUSH for twenty years.
Building and running it
You need your own copy of the game: both discs, or an installed copy. Nothing
of it is in this repository. pcrecomp
must be checked out next to this one as ../tools -- the lifter, the
disassembler and the 32-bit runtime (runtime/recomp32/) all come from there.
Python needs capstone; the host builds with MinGW-w64 GCC and Ninja.
# 1. Assemble an install under game/ (see docs/STARTUP.md for the layout):
# Focom.exe and its DLLs from your disc or installed copy, then Resource.
# Resource\Music and Resource\Movies are ~260 MB and easy to miss.
py -3 tools/iso_extract.py original/fc1.iso RESOURCE game/Resource
py -3 tools/make_focom_ini.py <absolute path to game> > game/Focom.ini
# 2. Catalog, import bridge, lift (the lift is ~10.7M lines; it is not committed)
py -3 ../tools/tools/disasm/disasm32.py game/Focom.exe -o analysis/functions.json
py -3 gen_imports.py
py -3 run_lift.py --all
# 3. Build, and run from the project root -- not from game/
cmake -B build -G Ninja && cmake --build build
build/focom.exe game/Focom.exe --run --watchdog 180
The command at the top of this page is the one that reaches the hangar.
docs/STARTUP.md ("Running it") has the diagnostic flags and the two ways the
build fails silently on a mixed MSYS2/Windows PATH.
Layout
forcecommander/
run_lift.py lift driver over pcrecomp's tools/lift/generate.py
gen_imports.py generates src/runtime/imports_gen.c (the 307-import bridge)
src/runtime/ the host: image mapping, threads, shims, COM objects,
the software rasteriser -- all hand-written
src/recomp/gen/ lifted C, generated and gitignored
tools/ rpk.py, iso_extract.py, make_focom_ini.py, gtxvars.py,
stepdecode.py, scriptmap.py, whoslot.py, com_purge_check.py
analysis/ catalog, sections, imports, rtti.json, vtables.json, seeds
original/, game/ your discs and your install (gitignored)
docs/
RECON.md what is inside Focom.exe: namespaces, renderer seam, formats
STARTUP.md startup, Focom.ini, the install layout, every bring-up fix,
the front end's script, and how to run it
STL-GATE.md the MSVCP60 gate, and the case for a 32-bit host
Credits
Star Wars: Force Commander © 2000 LucasArts Entertainment Company. This project neither contains nor distributes any part of it. The generated C is a derivative of the game's binary and is produced from your own copy, never committed; see LICENSE.