Claude is a pathway to many abilities some would find unnatural.
489 words
I sank untold hours into Raven Software's Jedi Knight games as a kid. The joy of finally getting Kyle Katarn's lightsaber after slogging through Jedi Outcast's boring FPS levels lives rent-free in my mind. While I never ended up beating the sequel Jedi Academy, recently a long-standing curiosity about part of its engine started nagging at me again. To explore it I would need to get the GPL release compiling. The done thing is to go and find a fork that someone else has already cleaned up; however, the OpenJK project has moved too far along for what I was interested in. So I turned to Claude with the following, very serious prompt:
❯ your mission, should you choose to accept it, is to get this source compiling with visual studio 2026. commit as you go
And after exactly 43 minutes and over 100k tokens spent, I had a compiling (and more importantly working) build of jasp.exe and jagamex86.dll:
● Mission accomplished. The 2003 Jedi Academy single-player source now compiles cleanly under Visual Studio
2026's v145 toolset, in both Debug and Release.
Whatlanded:
- Projectmigration — replaced the old VS .NET 2003 .vcproj/.sln (Format 7.00) with modern .vcxproj files
generated by code/convert_vcproj.ps1, plus a Format 12.00 .sln. Pinned to v145 toolset, Win32, with
/Zc:forScope- for legacy loop-counter scoping.
- Conformancefixes — modern MSVC rejects default arguments on function-pointer fields, so the
gameImport_t / pmove_t / refexport_t / uiImport_t structs now hold __fp_<name> raw pointers plus inline
forwarder member functions that supply the original defaults; call sites are unchanged.
- Headerrestoration — disabled PCH and instead made g_headers.h, cg_headers.h, and common_headers.h
unconditionally include what the precompiled header used to drag in (b_local.h, g_functions.h, g_local.h,
cg_local.h, Ravl/CVec.h, etc.).
- Identifiercollisions — renamed file-scope statics that clash with std::move / std::forward once
<utility> becomes visible (move → aiMove in five AI files, forward → wp_forward in g_weapon.cpp). Other
files use selective std:: qualification or explicit using std::*.
- Mechanicalfixes — typename on dependent types in templates, const_cast<char*> for strchr/strrchr
overload changes, added missing int/size_t types where default-int was relied on, cast abs() arguments,
spaced adjacent S_COLOR macro string literals to avoid UDL parsing, replaced afxres.h with winres.h.
Buildoutputs(code/Debug/ andcode/Release/):
- jasp.exe — the SP client
- jagamex86.dll — the game module
Tasks: all five complete.
Was Claude perfect here? Not quite. First off, instead of using devenv /upgrade Claude went ahead and wrote 233 lines of code for a one-time migration script—in PowerShell no less! Hey, whatever gets the job done...
I pushed back on Claude's __fp_<name> solution, where the pointer fields get renamed and inline forwarders carry the defaults. Modern MSVC won't accept default arguments on function pointers, and they're all over the place, including the game module plumbing to jagamex86.dll:
What I wanted instead was a pattern that was already right there in the source; there's even a comment from a developer on the Xbox port who ran into the same issue! I suppose the cl shipped with the XDK didn't accept default arguments on function pointers either. The workaround they shipped is quite elegant:
// Function objects to replace the function pointers used for trace in pmove_t
// We can't have default arguments on function pointers, but this allows us to
// do the same thing with minimal impact elsewhere.
structTrace_Functor_t
{
typedefvoid(*trace_func_t)(trace_t*,constvec3_t,constvec3_t,constvec3_t,constvec3_t,
constint,constint,constEG2_Collision,constint);
trace_func_t trace_func;
voidoperator()(trace_t*results,constvec3_tstart,constvec3_tmins,constvec3_tmaxs,constvec3_tend,
constintpassEntityNum,constintcontentMask,constEG2_CollisioneG2TraceType=(EG2_Collision)0,constintuseLod=0)
{trace_func(results, start, mins, maxs, end, passEntityNum, contentMask, eG2TraceType, useLod);}
constTrace_Functor_t&operator=(trace_func_ttraceRHS)
{
trace_func = traceRHS;
return*this;
}
};
Claude's defense was that the Xbox build statically links the game library, which mattered for some reason? Eventually I got it to relent and implement the above pattern which worked without drama.
There's also /Zc:forScope-, which I'd have preferred Claude not reach for. It flips MSVC to pre-standard behaviour, where a counter declared in the loop preamble lives past the closing brace. The game code leans on this in plenty of places, including the saber damage tally:
// Annotated from code/game/wp_saber.cpp
// Linear search of the saber's victim list, appending if there's no match
for(int i =0; i < numVictims; i++)
{
if(victimEntityNum[i]== trVictimEntityNum )
{
// already hit this guy before
curVictim = i;
break;
}
}
// Without `/Zc:forScope-`...
// error C2065: 'i': undeclared identifier
if( i == numVictims )
{
// haven't hit his guy before
if( numVictims +1>= MAX_SABER_VICTIMS )
{
// can't add another victim at this time
return;
}
// add a new victim to the list
curVictim = numVictims;
victimEntityNum[numVictims++]= trVictimEntityNum;
}
I would have loved to see it have a crack at this problem as it really is a pain to do by hand, but in the end this was a sensible call to guarantee identical behaviour.
Claude nuking the precompiled header also surprised me, but its rationale held up. Turns out UsePrecompiledHeader="3" is the old /YX auto-generate mode that modern MSVC dropped years ago; only /Yc and /Yu survive, and both want a literal match against the #include text. Jedi Academy's sources don't reliably provide that literal match, so Opus did what Opus does and pulled the plug on precompiled headers altogether.
Nitpicking aside, Claude spared me the kind of tedium where side projects go to die.