SCCT Reforge
Hey everyone!
Time for a proper introduction to the other thing I’ve been building. It’s called SCCT Reforge, it rebuilds the models and textures of Splinter Cell: Chaos Theory, and like the Terranigma project it turned out to be less about modding and more about reading bytes until they confess. So this is the whole story: how it started, what fought back, and what actually came out of it.
Where this started
Chaos Theory came out in 2005 and I still play it. One evening I looked at the suppressor on Sam’s rifle and noticed it was an octagon. Not a cylinder with a few segments, an actual eight-sided prism, sitting in the middle of the screen for the entire game.
That’s not a complaint about the artist. In 2005 you had a triangle budget and you spent it where people looked. It’s just that I have a computer from 2025 and the game doesn’t. So: how hard can it be to add sixteen more sides to a cylinder?
The answer is that the game stops starting, and it takes about a week to find out why.
Everything single-player in Chaos Theory sits in one file: 610,578,839 bytes, 20,252 entries, 16-byte aligned, gaps zero-filled. Inside it are Unreal Engine 2 packages, 13,385 static meshes and 12,035 images across 72 texture packages. None of that is documented. There’s no SDK, no import plugin, and the generic UE2 notes mostly don’t apply because this is a fork. So the first month wasn’t modding at all. It was writing tools that tell you what a byte means.
One rule from the start, and I’ve never regretted it: anything I claim about the format has to hold across the whole container, not on one file. A theory that works for one mesh is a coincidence. A theory that works for 13,381 out of 13,385 is a format.
The parts that fought back
The suppressor that refused to shrink
Here’s the experiment that cost a week and taught me the most.
There is a mesh called FN2000_SAM_Silencer. It has 27 vertices and 22 triangles and, measured, the game never draws it. The visible suppressor is baked into the rifle frame. Perfect guinea pig: shrink it from 27 vertices to 3 and from 22 triangles to 1, change nothing else.
The game doesn’t start any more. The same container with geometry appended to that mesh instead of removed runs perfectly.
The reason sits in a block after the vertex data that I originally labelled “rest” because I couldn’t read it. It’s a collision tree, and its 16-bit values are two different things wearing the same coat: if the top bit is set it’s a triangle number, otherwise it’s a link inside the tree. Measured across the whole game, every one of those triangle numbers is below the mesh’s own triangle count. So when the counters grow, every reference stays valid. When they shrink, they point past the end, and the game dies before it draws a single frame.
Which gives the one law everything else is built on: existing vertices are never removed and never reordered, only appended. And that quietly solves importing arbitrary models too. If the new model has fewer triangles than the slot it’s going into, you fill the rest with degenerate triangles, the same vertex three times. Zero area, never drawn, every triangle number still valid. A test cube of 8 vertices and 12 triangles dropped into the reflex sight’s slot produces an object of exactly the same 7,168 bytes as before, with all 300 other objects in the package byte-identical.
The rebuild that wouldn’t boot
For a long time a second rule stood next to the first: nothing may move. Because when I rebuilt that 610 MB container from scratch, the game wouldn’t boot, even though the rebuild was byte-exact with no changes applied at all.
That was true, and it was true for the wrong reason. I spent real time on the checksum in the footer before finding out the game never checks it. The actual cause was in the meshes: every one of them ends with a lazy array whose jump marker is an absolute position in the package file. Move the object, and the marker points at nothing.
| state | markers correct | wrong | game |
|---|---|---|---|
| original | 270 | 0 | runs |
| patched in place | 270 | 0 | runs |
| grown, markers untouched | 112 | 158 | won’t start |
| grown, markers recomputed | 270 | 0 | runs |
That last row is confirmed in the actual game. So the rule became: objects may move and grow, the jump markers move with them. And the reason patching in place had always worked was luck disguised as discipline. The object keeps its exact byte count, so its trailing block starts at the same address.
Before I found this, I built something I’m still fond of even though it’s now mostly obsolete. A ring swap: an object moves into the slot of a larger one, the larger one into a smaller one, and the chain closes. Offsets and sizes in the export table are only exchanged, never changed, so the table is necessarily the same length, no header offset moves, and the file stays the same size. It buys you 28,915 bytes where you had 7,168, with no rebuild at all. It’s still in the codebase. It’s just that once rebuilding works, you can simply let the package grow.
Sam, and the eighty-nine percent
Modernising the player character was supposed to be a side quest. It became the largest single piece of work in the project.
Sam’s head is 4,076 triangles. Subdividing it naively makes it rounder and worse, you get a smooth potato. What actually works is laying over every edge the curve that meets the stored normal at both ends. The roundness was already in the model all along, the lighting knew about it and the silhouette didn’t. It also needs no crease detection whatsoever, because at a real hard edge the normals are split and sit on the facets, the correction term goes to zero, and the midpoint stays on the straight line. Sharp edges stay sharp without anyone looking for them.
Result: 4,076 to 35,330 triangles, median crease angle from 21.8 down to 5.8 degrees. And he still looked flat.
That was the surprise, and the measurement that explains it is my favourite number in the whole project. A scheme like that leaves the original vertices where they are, so nothing can appear that wasn’t already there. But the normal map knows the slope at every point, and a slope is the derivative of a height. Reconstruct that height and you can measure exactly how much of the surface form was ever geometry:
| displacement span | slope in the mesh | share of the map |
|---|---|---|
| 0.45 (where I started) | 2.7° | 10.8 % |
| 1.75 (shipping) | 9.5° | 38.4 % |
| 3.00 | 17.4° | 72 % |
Eighty-nine percent of the surface form was painted shading. Brows, lips, suit folds, vest seams, none of it existed as geometry. That’s why Sam looked flat with forty thousand triangles. The triangles went into rounding, not into shaping.
Then it tore his face open. At a fixed displacement the mouth opened down to the teeth and the eyelid creases carved in. Measured rather than guessed: the upper lip travelled 0.57 units across a gap of a tenth, and 484 triangles flipped against their own stored normal, clustered at face, hands and calves. That’s not random, it’s the map of the original mesh. The artist built fine where the forms are small, 1.6 units per edge in the face against 7.2 at the shoulder. A displacement that reads as muscle definition on a shoulder reads as a torn-open mouth on a lip. So it scales per vertex with the local edge length now, and the face runs at about a quarter speed.
The lesson I keep coming back to: a closed gap can still be visible. In the end no mouth pair opened more than 0.09 units, and there were still wedges at the lip line, because the lip edge itself had rolled and exposed a slit that was always open. Distance metrics check distances. What you actually see is only checked by looking at it.
Three months of an eyeball
Chaos Theory’s successor, Double Agent, runs on a fork of the same engine, and its models read with my Chaos Theory reader almost unchanged. So its weapons, its goggles and its outfits can be transplanted. Transplanted is the honest word, nothing is swapped: every foreign mesh gets re-bound to Chaos Theory’s skeleton, its triangle strip re-woven, its texture sheets re-encoded and re-packed.
The re-binding is where all the difficulty lives, and the best story in this project is Sam’s eye.
The obvious approach is that every transplanted point inherits the skin weights of the nearest Chaos Theory point. That works beautifully for skin and terribly for anything rigid. An eyeball rotates, it does not deform. Inherited from the surrounding surface it got 86 different blended bindings and not a single eye bone, and in game it visibly warped when Sam looked around or squinted.
I fixed it three times. Each fix shipped, and each one broke something new. Making it rigid on the eye bone tore it at the eyelid. Scaling the weights up instead gave a wobbling mask and stiff eyes. Moving the ball onto the centre of the visible eye made it sit too low. All three were rolled back.
And the reason every single attempt missed is that I was fitting spheres to visible geometry instead of reading the skeleton, which turned out to be sitting inside the mesh the whole time. Named bone records: a compact index for the name, then 56 fixed bytes of flags, rotation, position, children and parent. Variable record length, which is why my earlier scans only ever found part of the table.
| distance from the actual bone | |
|---|---|
| Chaos Theory’s visible eye | 0.79 behind it |
| Double Agent’s eyeball | 0.80 in front of it |
Chaos Theory’s own eye doesn’t sit on its own pivot. The entire sphere-fitting premise was wrong. And the mechanism is different from what I’d assumed: the eye section has 30 points, 14 of them bound to the head and only 8 to each eye bone. It’s a small iris cap sliding across a stationary background under partially dragged eyelids. Nothing ever rotates as a whole ball, which is exactly why the bone offset never mattered in the original.
If you need a pivot, read the skeleton. Fitting a sphere to what you can see measures the shape, not the mechanism.
Learning to read bug reports
The most useful thing that came out of testing with an actual player isn’t a fix, it’s a vocabulary. Three reports arrived over a few weeks: “it tears”, “the chin comes out”, “it folds up”. I treated them as the same complaint three times. They are three different quantities, and each of my probes was blind to the other two.
- “tears” is edge stretch, a maximum.
- “comes out” is migration relative to the carrying bone.
- “folds” is compression, the exact opposite of stretch, and I had simply never counted it.
A rig fix that halves the stretch can double the compression, and my entire test suite would have reported success. There is now a probe that drives 40 real poses across 22 body regions and measures all three, plus the opening of gaps.
Related, and it took me embarrassingly long to see: short edges tear quietly. Sorting by absolute stretch, a tenfold rip on a one-unit collar edge hides underneath the perfectly healthy stretch of a long knee edge. The original models show ratios of 13 to 19 on short eyelid edges and they’re fine. Only the location gives the bug away.
The HUD that simply wasn’t there
The HUD lives outside the package system entirely, as plain image files in a separate branch of the container. 78 sheets, most of them 16 by 16 or 32 by 32 pixels, stretched onto the screen and filtered, which at 1440p is mud.
They aren’t photographs though, they’re analytic geometry. Discs, rings, arcs with gaps, wireframe renders of the actual weapon models, all of it rasterised onto a tiny grid in 2005. So you don’t upscale them. You build a parametric model, render it large, scale it back down to the original size, and push the parameters until it matches. The return path is the proof: a reconstruction you can’t scale back down and compare is a claim, not a result. 66 of the 78 now ship reconstructed, and the compass ruler returns at 0.00 percent mean deviation.
And then the new sheets weren’t in the game. Four rounds of “not visible”. Rounds one to three each found a real bug that wasn’t the cause. The actual answer is one stage behind the image loader: it reads the dimensions out of the file header correctly, and then the texture object’s setup call immediately overwrites them with the dimensions it was created with, which come from the menu layout table. A 512 pixel sheet lands in a 32 pixel surface. No crash, no log, just nothing.
A sheet may grow, but only together with its layout. Two correct pieces of evidence don’t make a proof when there’s a stage missing in between. Since then the installer verifies itself: after writing, the container is reopened, every sheet read back out and compared byte for byte, and only then does the tool say so.
A foreign suit, and filler that was never content
The most recent stretch of work was a user-built suit, hundreds of thousands of triangles with sheets baked in a modern renderer, going onto a 2005 skeleton where the whole model has to fit into a triangle strip of 65,535 indices. Four findings, all of which generalise.
The filler on a baked sheet is not content. The suit showed fanning streaks across chest, flank and thigh. Four counter-checks cleared my import path completely, and the cause was in the bake: it covered 27 percent of the sheet, the rest is edge extension. The model’s own coordinates cover 49 percent. So half the body area sits on filler. You can’t recover it, but you can regenerate it as a smooth continuation instead of a starburst. What distinguishes real bake from filler is measured, not guessed: brightness and saturation don’t separate them at all, fine-grained variance and directionality do, and either one alone fails.
What a model brings that no texture knows about is geometry. The raised web ridges are 449,560 triangles carrying no texture coordinates at all. I originally dropped them, reasoning the pattern was in the sheets anyway. That reasoning was a claim about the data, and the data disproved it: a full-resolution crop of the torso shows the fine honeycomb and no ridge anywhere. They’re baked into the normal map from the geometry now, which is the right place for them, since they only stand 0.23 units proud.
A bone has to sit inside the flesh it moves. The thumb stuck out and was rigid, and that’s one cause, not two: the thumb’s flesh ran 19.9 degrees off its bone chain, up to eight units alongside it. The hand’s weight field falls off with the fourth power of distance, so at that range thumb, palm and finger bones are all equally far away and equally weak. The thumb was hanging on an average of half the skeleton. Rotated back onto its chain it’s 3.0 degrees, and the fingertip’s travel under a bend went from 2.64 to 5.87 units, against the original Sam’s 7.34.
A seam runs through an island, not only between islands. The web lines broke along the underside of the forearm. A checkerboard render, rather than the suit texture, showed it wasn’t shading at all, it was the coordinates: the distortion measure hit 20.4 on the underside against 1.4 on top, and the worst triangles had two corners at one end of the sheet and one 470 texels away. That’s the wrap seam of the arm, and a cut tube is still topologically one island, so filtering candidates by island number never saw it. One extra condition later, the underside is at 1.9.
Things I got wrong, in public
This section exists because the mistakes that cost the most were never the hard problems.
I put a convention to a vote. Whether the vertical texture axis counts from the top or the bottom: I compared coverage against baked content, got 60 percent against 35 in favour of flipping it, and flipped it. Half a build later, 9.4 percent of the model was landing on empty atlas and there were black bands across belly, mask, forearm and boots. Correct orientation: 0.05 percent. The answer had been sitting in the code that reads the sheets the entire time. Whoever puts a convention to a vote gets a majority, not an answer.
I measured with a different tool than the one that builds. My texture budget analysis used a simple shelf packer and reported that repacking the atlas would make things worse. The build uses a proper rectangle packer. With the real one it’s a 1.78x gain, and I nearly closed the question on the strength of a tool measuring itself.
I shipped a rig change whose effect my own probes couldn’t see. It produced “half his neck disappears at the front” in game, and offline it was, and remains, undetectable. Sections unchanged, seams unchanged, renders fine, every number clean. It was removed entirely. A change whose effect your probes cannot see isn’t verified, it’s unverifiable, and it doesn’t ship.
And a guard that never fires looks exactly like a guard. Several times over: a comparison seeded at a value every real candidate fell below, so nothing ever won; a clamped check that dutifully reported zero error on every mesh in the game. Every threshold in the build has since been deliberately tripped once, just to watch it actually break the build.
What’s in it today
The tool is called SCCT Reforge, subtitled “meshes and textures, rebuilt not repacked”, because nothing is swapped. It’s a raw Win32 window drawn entirely by hand in one paint pass: black ground, phosphor green, scan lines, yellow corner brackets. That’s partly the look I wanted and partly necessity, since Windows controls can’t show selected, hovered and locked as three distinguishable states, and a greyed-out checkbox just looks broken.
- Weapons rebuilt, or transplanted from Double Agent, including an FN Five-seveN re-bound and re-atlased from scratch.
- Sam modernised, head, body and goggles, with the normal map’s detail turned into real geometry.
- Six alternative outfits from Double Agent and its online mode, plus a user-built high-poly suit.
- 66 HUD sheets reconstructed at up to 512 pixels, in a faithful and a modern variant.
- Mission images and loading screens at four times the pixels, with the old sponsor banner painted out.
- Install and full byte-identical restore, verified against the finished container rather than against my own intent.
Everything foreign ships encrypted inside the executable, and the encryption is checked against the published test vectors rather than against itself. Double Agent content unlocks only after you show the tool your own copy of the game, checked by fingerprint first and by five byte signatures second, of which three must match. A single hash would lock out most honest buyers, because a game from 2006 has patches, languages and retail variants.
There’s a free build and an early access build, always one version number apart. Locked rows stay visible with a padlock, because a feature you can’t see isn’t a preview, it’s a secret.
Where it’s going
- Polygon budget. There are 1,851 sliver triangles under 5 degrees where the original had 52. Invisible in stills, and the next real lever on quality.
- The remaining twelve HUD sheets, and closing the gap between “interpreted” and “exact” on the ones that sit just short of it.
- Maps. The level files sit loose on disk and are completely unread so far. That’s where the lighting and geometry of the actual levels live.
- Animations. Double Agent’s online skeletons read fine, their animation tracks don’t yet.
- More outfits, and getting the existing ones confirmed in game rather than only offline.
Everything lives here
Like everything else, this now lives on diemilchgebendekuh.de. That’s where development updates go, and that’s where the tool is. The free build stays free.
I’m looking for testers here too
Almost every real bug in this project was found by someone playing the game, not by me running probes. “The thumbs stick out and they’re stiff”, “he doesn’t hold the pistol right”, “half his neck disappears when he crouches”, every one of those turned into a measurement and a fix, and not one of them showed up in my offline tests first. If you own Chaos Theory and enjoy staring at a character model until something looks wrong, please get in touch.
The one thing I’d tell anyone starting this
Every single time this project went badly wrong, the shape was the same. I had a measurement, the measurement was correct, and I asked it a question it couldn’t answer. A circle fit that said “straight” about a seven-pixel arc, where a curve and a straight line are genuinely indistinguishable. A stretch probe that stayed silent about a fold. A coverage number that proved the texture was fine while the shading was visibly wrong.
A measurement that can’t separate two readings hasn’t refuted either of them. It’s telling you to ask differently. Check the method before you rebuild the code. I’ve rewritten working subsystems twice on the strength of a bad measurement, and both times had to put them back.
Thanks for reading all of this, and thank you for the support. It means a lot. ❤️
Best regards,
DieMilchgebendeKuh
Get it on Patreon here: https://www.patreon.com/DieMilchgebendeKuh/posts/scct-reforge-v1-167841183?utm_medium=clipboard_copy&utm_source=copyLink&utm_campaign=postshare_creator&utm_content=join_link


