TwoBoneIK crashes Unreal Engine 5.8 with "array index out of bounds: 0 into an array of size 0"
Eight tables and a rulebook
Second Chance has a snooker club in it. Not a snooker-flavoured button you press to skip ahead: a room with eight full-size tables in it, a counter with a till, a cue you lift off the rail, and a frame of actual snooker played under the actual rules against a man who wants a fiver off you. GTA 6 will have minigames. This is a simulator with a town built round it, and the room it sits in is a specifically British room - the strip lights on cables, the ox-blood banquettes down the side, the scoreboards nobody has wiped.
On Friday the agent - Claude Code Opus 5, driving the editor over two MCP servers - ran from twenty-five past midnight to three minutes to midnight, on the club almost without a break. Every check it ran that day came back green.
The checking was not the problem.
Twenty lights that agreed with themselves
The morning went on the room itself. The tables, the scoreboards and the cue racks - seventeen objects - turned out to be driven by the street's brick material master, which is fine as plumbing and hopeless as mahogany, so they were reparented onto worn oak with a proper normal and roughness set, and the hall carpet's tiling was pulled back to something a human floor would actually do.

Then the lights. Twenty light actors in the hall, each given an intensity, a colour temperature and a falloff radius, and the uplights raised three metres so they could reach the roof instead of washing the floor. The script wrote each property, read each one back, and printed the pair: set 3200, read 3200. Twenty of them, all matching.
The script still reported failure, and it reported it for a reason that had nothing to do with the readbacks: no package file on disk had changed its modification time. Nought of twenty. Every property had been set on an actor living in memory, every readback had truthfully reported what was sitting in memory, and the save had quietly declined to write any of it. This project keeps a note about exactly that - Python edits to placed actors do not save themselves
- and the only instrument that catches it is the filesystem, because the object will
cheerfully agree with you all day.

The colour you hit is the colour you called
The afternoon went on the rules engine, and on a bug that had been costing points in play for two days without anybody filing it.
When a player fouls, the referee charges the value of the ball he was on. If he has nominated brown, that is four. The engine had a variable for the nominated colour and it had never once been written: it sat at minus one for every stroke of every frame anybody had ever played, so the engine fell back on valuing the ball on as the highest colour still on the table and charged seven, every single time. The same silence was scoring a potted yellow after a red as seven as well.
The fix was not an interface, which is the good part. Nomination is now deemed from the stroke - the colour you strike first is the colour you called, which is how a referee rules it in a club where nobody says anything out loud. No menu, no prompt, and it works identically for the player and for the opponent, because the opponent reads the same target list. The flat sevens are gone.
James's own house rule went in on the same change - once the reds are finished the first colour is free, then the sequence restarts from the lowest colour still on the table - and it had to go into two places at once, the game engine and the independent Python model the engine is checked against, or both would have agreed beautifully on the wrong answer. Together they came to 30,009 checks, then 7,888, then 1,529. All green.
And here is the part worth stealing. The big sweep that produced most of those numbers - forty frames, twenty-five strokes each - had never once reached the end game. Twenty-five strokes does not clear fifteen reds. Not rarely: never. The entire house rule sat behind a handful of hand-written cases while a thousand-stroke sweep scrolled past looking like coverage. A second sweep now starts frames with the reds already gone.
A dozen contacts in two seconds
The evening was sound, and the first decision was where not to put it.
The obvious place for a click is inside the physics solver, at the point two balls are resolved against each other. That function runs once per pair per substep, over 231 pairs, and balls resting in a cluster are re-resolved every substep - so the obvious build is a machine gun. It also means editing a solver that a 61-check physics gate is pinned to.
So the detection sits in the table instead, once a frame, and the signal is speed gained. Each frame, each ball, compare its speed with last frame's. A ball only speeds up when something has hit it. One rule buys three things: one click per collision instead of one per substep, the volume for free from how much speed was gained, and - the elegant one - cushions are silent without anything needing to know where a rail is, because a ball coming off a cushion can only lose speed. James had already ruled that cushions barely make a noise and should be skipped. Physics now enforces his ruling rather than a geometry check somebody would have to maintain.
It was tuned offline against the Python model, where a full-power break produces sixteen clicks across six frames and an ordinary pot produces two - the contact, and the red coming back off the top cushion into the white. Both real.
The first run of that test rig reported zero clicks in a break, which reads exactly like a dead detector. It was a dead rig. The aim was hard-coded at zero degrees, and a cue ball that racks twenty centimetres off centre sails straight past the pack, rattles round the cushions for fourteen seconds and touches nothing at all. The aim is now computed from where the target actually is, and the script raises a named error rather than printing a nought.
He was mumbling because he had been told to
Barry, the club's hustler, got a voice the same evening: 139 lines, every variation of every call, baked through ElevenLabs in a voice James picked himself off a five-side audition.
He rejected the first bake in four words. Sounds like he is mumbling. He was right, and there were two separate faults inside that one complaint.
The first is general enough to be worth writing down: a v3 audio tag that names a volume or a breath is an instruction to swallow the line, and the model obeys it. A line tagged [low, serious] came back at -29.5 dBFS against a -18.7 median for the set - eleven decibels down, inaudible on his machine. [quietly], [under his breath] and [half-laughing] all ask for the same thing without looking like they do. Eighteen tags were rewritten to name the attitude rather than the volume, which buys the same performance and can be heard.
The second was settled by an A/B/C of three settings across three lines, all peak-normalised first so that loudness could not flatter one of them, and James chose the option with the direction tags stripped out entirely. So the directions are now stage directions for the writing, not model input. A levelling pass after the bake closed the spread across the set from fifteen decibels to eight.
The first time anybody looked
Then James watched the men play, in the game, for the first time.
Everything in the snooker file up to that point had been verified by numbers. States sampled frame by frame, clip travel measured in centimetres, 29,832 checks off disk. The first eyeball found seven defects in one turn.
It also crashed the editor one frame after the strike, with an assertion - array index out of bounds: 0 into an array of size 0 - and no Blueprint frames on the stack at all. The cause was the arm correction that puts the men's hands on the cue. Both of its two-bone nodes had an empty bone reference for the elbow target, which the engine resolves whether or not the setting says it should, and an unresolved bone reference is precisely the shape of index nought into an array of nothing.
It had never fired before because the correction had never once run. It is switched off until a stroke is actually being played, so the first frame it evaluated was the first frame anybody watched it. A feature that is inert until proven is untested until proven, and those are the same sentence.
The old elbow target was the origin, in component space - which is the floor between the man's feet. He had been aiming his elbows at his own boots. With the target moved onto the forearm the bend plane goes back to being the animator's, and two strikes and a resolved shot ran with no assertion at all - and, more to the point, with 297 of 900 sampled frames showing the correction actually doing something. A quiet run with it stuck at zero would have looked identical from the outside and meant nothing.

The other six were not crashes. Both men address the ball with the cue across the body rather than along it. Two cues are visible at once, the one in his hand and the table's own. The bridge hand travels backwards through the stroke instead of staying planted. They turn all the way round at the start of a walk. The cue lifts forty-five centimetres off the table at the strike, which is not a bug at all - it is James's own instruction from four days earlier, obeyed exactly, and nobody had watched what obeying it looks like. And a man who is not playing stands where the striker wants to be, and the striker walks through him.
The hand correction, meanwhile, is now landing each hand within three centimetres of its target, and the target is forty-two centimetres off the cue's axis - almost all of it sideways. The correction was only ever written to fix height. So the arm is doing its job and the job was the wrong one: the man is stood in the wrong place, which James had said by eye - he might be bent over too close to the table - before anything measured it.

One thing on the list was not a defect. Partway through, James typed that he could hear Barry talking. Until that moment every claim about the voice in this project had been a measurement off disk - lines baked, cues bound, a dispatcher gated at twenty-seven wired pins - and not one line had ever been heard by a human being. The chain works end to end: a stroke is resolved, one line is chosen off what happened, and it comes out of the man's mouth in the room.
Rockstar would have caught the elbow target in review, on a Tuesday, in somebody else's sprint. Here it took the first person to sit down and watch. The measurement that would have caught it does not exist, and could not have, because nobody had thought to check whether a man was holding his cue the right way round.
The fault
An Unreal Engine 5.8 AnimBP crashes with "Array index out of bounds: 0 into an array of size 0" and no Blueprint frames on the stack
The assertion fires one frame after the animation node that uses it first becomes active. The script stack is empty, so the fault is native rather than Blueprint.
Cause: a TwoBoneIK node with an empty joint_target bone reference. FAnimNode_TwoBoneIK::InitializeBoneReferences resolves that target regardless of what joint_target_location_space is set to, and an unresolved bone reference produces exactly this assertion.
It will not reproduce until the node evaluates. If the node's alpha defaults to 0 and only leaves 0 under a game condition, the first frame it runs is the first frame anybody plays.
The fix
Give the node a real pole bone, and treat "never evaluated" as "never tested"
Set the elbow target to a bone that already exists in the chain rather than leaving it empty:
joint_target_location_space BCS_COMPONENT_SPACE -> BCS_BONE_SPACE
joint_target (empty) -> lowerarm_l / lowerarm_r
joint_target_location (0,0,0) -> (0,0,0)
Using the forearm means the pole is wherever the clip already put the elbow, so the bend plane is preserved and the correction stays small. The previous value - (0,0,0) in component space - is the character's own origin, i.e. the floor between his feet, which is what aimed both elbows at his boots.
The guard: prove the node ran, not merely that nothing crashed. Sample the alpha across a few hundred frames and require a meaningful count above 0.5. A run with the correction stuck at zero is indistinguishable from a clean one unless you measure it.
The fault
Python property edits on a placed actor read back correct and never reach disk
set_editor_property followed by get_editor_property returns the value just written, on every actor, for every property. The save call returns without error. Nothing is written.
Twenty light actors were edited this way and every one of forty readbacks matched. Zero of twenty package files changed their modification time.
The fix
Assert on the package file's modification time, not on the readback
Record each affected package's modification time before the edit and require it to move afterwards. A readback interrogates the object in memory and therefore cannot see a failed save at all. The filesystem is the only witness.
The fault
A rules-engine sweep reports full coverage without ever reaching the rule under test
A 1,000-stroke randomised sweep passed 30,009 assertions while the end-game rule it was supposed to exercise was never entered once: 40 frames x 25 strokes clears fifteen reds exactly never, so the sweep could not physically reach the state.
Same class of error, different tool: an audio test rig reported "0 clicks in a break" - which reads as a dead detector and was a dead rig, because a hard-coded aim of 0 degrees sends the cue ball straight past a pack it is 20 cm off-line from.
The fix
Start the sweep in the state you are testing, and make a null result raise
Seed frames directly into the end game rather than hoping a random walk arrives there. Where a harness can produce a legitimate-looking zero, compute the input from live positions instead of hard-coding it, and raise a named error on an empty result rather than printing a count.
The guard: before believing a null result, check the harness actually did the thing.
The kit
| Tool | Why this one |
|---|---|
| Unreal Engine 5.8 | where all of it lives, and the only place six of the seven defects were visible |
| A Python reference model of the rules | a second implementation written purely to be argued with. It caught the nomination bug, and it also carried the same blind spot, which is the limit of the method |
| ElevenLabs | 139 lines in one voice. The v3 tag behaviour above is the thing to know before baking a set |
| Freesound, CC BY 4.0 | the ball foley. Attribution is a shipping obligation, so the donor list is a manifest the credits file is generated from, not a note somebody has to remember |
| The filesystem | the only instrument that caught the lights |
Plain English
| Term | What it means |
|---|---|
| animation blueprint | The logic deciding which animation a character plays and how one blends into the next. |
| inverse kinematics | Working an arm or a leg backwards from where the hand or foot has to end up, instead of playing whatever the animation happened to record. It is how a character's hand can land on a moving object the animator never saw. |
| assertion | A check the engine makes about its own internal state, which stops the program dead rather than carrying on with something impossible. An assertion failure is a crash by design, and the message usually names the impossible thing. |
| PIE | "Play In Editor" - pressing play inside the editor to test the game without building it first. Fast, and it behaves subtly differently from the real build. |
| oracle | A second, simpler implementation of the same rules, written only to be argued with. The real one is run against it thousands of times, and wherever the two disagree, one of them is wrong. |
| foley | Everyday sound recorded or cut by hand and laid onto the picture afterwards - footsteps, a door, a ball hitting a ball. The word is borrowed from film. |
| sound cue | A small wrapper around one or more sound files that the game plays instead of the file itself, so the same recording can be reused under several names or mixed with others. |
| attenuation | How a sound fades with distance, and how far away it can still be heard. |
| material | The recipe describing how a surface looks: colour, roughness, shininess, transparency. |
| actor | Anything placed in the game world - a lamp post, a person, a trigger volume, a camera. If it sits in the level, it is an actor. |
| Blueprint | Unreal's visual scripting. Logic built by wiring boxes together with lines instead of typing code. This whole game is built in it. |
| negative control | The opposite of a positive control: deliberately putting the fault back to check that the test you just wrote actually notices. A test that has never been seen to fail is not evidence of anything, because it may simply be incapable of failing. |
Things we still need to fix tomorrow:
- Both men address the ball with the cue across the body instead of along it. The arm correction
is landing the hands accurately on a target that is 42 cm off the cue's axis, nearly all of it sideways, so the next thing to measure is where the man is standing.
- Two cues are drawn at once during a shot - the one in his hand and the table's own.
- The bridge hand travels backwards through the delivery. It should be planted.
- Both men turn a full circle at the start of a walk instead of turning to face where they are
going.
- A non-playing NPC stands where the striker wants to be and gets walked through. The ruling is
that the bystander yields, which is a build rather than a fix.
- The cue lifting 45 cm at the strike needs a decision, not a repair - lower it, delay it until
after the follow-through, or hide the table's cue and let him keep his own.
- The other man's clothes stretch as though they are not moving with his body. Not a snooker bug;
the same symptom has a known cause elsewhere in this project.
- Barry has been heard speaking, but nobody has checked whether the right line fires for the right
event, or whether the rationing feels right across a full frame.
- There is still no video of any of this. The club has been playable in part for days.