PeakSafeOptimizer
Performance mod focused on 8-player lobbies. 33 of 39 patches on by default, each equivalence-proven or a vanilla crash fix, per-patch fail-closed validation, opt-in switches for the ones that trade fidelity for frames.Changelog
1.4.1
The game shipped another update (Assembly-CSharp MVID c88cc526-… -> e053d265-…). Only
Assembly-CSharp.dll changed - every other game assembly is byte-identical - and no method this mod
patches was rewritten. So this release contains no adaptation work. What it contains is three
long-standing accounting errors, one of which was a real regression.
IKItemGuard reproduced the wrong hand (real regression, fixed)
This patch exists because CharacterAnimations.ConfigureIK raises a NullReferenceException every
physics frame while you hold an item whose prefab lacks a Hand_L or Hand_R child. The two cases
are not symmetric: whichever hand the method evaluates first throws with no writes landed, and the
other hand has already been written twice by the time it throws, so the patch has to reproduce those
writes before skipping. Getting that assignment wrong freezes one IK hand target or performs writes
vanilla never reaches.
It was wrong. The patch was written against the 1.20 body, which evaluated the right hand first;
from c88cc526 onward the game evaluates the left hand first. The bookkeeping was never flipped
with it, so for two releases a missing Hand_L froze the left IK target and a missing Hand_R
performed two right-hand writes the original does not reach. Verified at the IL level
(GetItemPosLeft at IL_0063, the rotation setter at IL_009f, SetPositionAndRotation at IL_00a5,
then GetItemPosRight at IL_00da) and fixed: the patch now reproduces the left-hand pair and treats
a missing Hand_L as the zero-write case. The two repair reasons swapped names to match.
Two patches removed
PocketBehaviorGuard. It claimed vanilla dereferencedcharacter.player.itemSlotsunguarded. The game addedcharacter?.player == nullplus anitemSlotsnull/length check inc88cc526- the release this was last audited against - so the guard has been redundant since 1.4.0. Because its prefix returned true and let the original run, it was pure per-character per-frame overhead. This is the same class of mistake as the three patches 1.4.0 removed; this one was missed.AnimatorValuesHash. A census of every asset file in the shipped game (all.assets, everylevel*, everysharedassets*,globalgamemanagers, 810 MonoBehaviour classes with instances) finds zeroAnimatorValuesinstances - against 23UIPlayerNames, 1Snowballand 3ExplosionEffect, all of which the same sweep resolves. Nothing in the game or in any installed mod adds the component in code either. Its target method can never run, which makes it the same situation as theParticleCountCullerWritepatch deleted in 1.2.0.
Counts: 39 patches / 35 switches, 33 on by default, down from 41 / 37 / 35.
Three comments whose conclusions had gone stale
No behaviour change, but they were load-bearing arguments and are now correct:
ExplosionScalejustified leaving nine prefab fields unasserted partly on the grounds thatrequireCurrentMvidwould skip the whole patch on an unrecognised build. 1.4.0 deliberately removed that gate, so the backstop it leaned on no longer exists. The comment now says plainly that those nine fields are protected only by re-auditing a fresh decompile after a game update.SleepingZombieScancalled its squared-distance replacement "provably equivalent". It is not bit-exact:t*trounds once in float and the comparison moves onto a different quantum, leaving a band at most one ULP wide where the two disagree (roughly half of plausible thresholds have such a band, and the patch errs slightly permissive). The probability of a real zombie landing in it is around 1e-10 per test, so the behaviour is fine, but the comment now says "within one ULP" instead of "identical".RemoteRagdollLodAnimatordescribed a vanillaDebug.LogWarningabout swapped animator controllers. The current build deleted that log line and kept the check, so the comment now describes the check without the log.
Documentation
README.md and TESTING.md are gone. Their per-patch tables and test checklists duplicated what the
patch docstrings already state, and every code change needed them rewritten, which is a worse use of
attention than the code itself. The source docstrings are the specification. This changelog and the
Thunderstore README cover only what a user needs.
1.4.0
The game shipped a low-level update: it now does a batch of its own performance work, plus a console port. Assembly-CSharp's MVID moved, and three of this mod's patches were made redundant by the game itself. 41 patches and 37 switches, 35 on by default, down from 44 / 40 / 38.
The MVID gate is gone, and that is the headline
Until 1.3.0 the installer had this, ahead of every patch's own validation:
if (definition.RequiresCertifiedBuild && !fingerprint.IsCurrent) return;
43 of the 44 patch modules set that flag. So one game update silently skipped almost the entire mod - before a single patch got to check whether the members it actually reads had changed shape. That is the opposite of what the per-patch structural validation exists for, and it turned every game update into a release event. Removed.
The only gate now is each patch's own Initialize assertions plus its target signature check: shapes
match and it installs, shapes differ and that one patch reports SkippedStructureMismatch. The MVID
is demoted to a log label and CompatibilityLevel is demoted to a re-audit priority.
SkippedUncertifiedBuild is no longer produced by anything.
The cost, stated plainly: structural validation proves that member shapes are unchanged, not that
method bodies still mean the same thing. Only re-reading a fresh decompile catches the second kind, so
structural-validation in the log should be read as "nobody has re-read this build yet - start with
the Fingerprint and StrictIl patches".
Three patches removed, because the game now does their job
- GenericOptimizerRange.
AnyCharacterWithinRangenow squares the range, indexes the list and hoiststransform.positionout of the loop - exactly the three things this patch did. Worse, the game gaveGenericOptimizera private cached transform property, which this assembly cannot bind to, so C# member lookup falls through toComponent.transform: the patch became one extra native call per instance per frame in exchange for nothing. There are 2133 instances. - StatusEmitterScan.
StatusEmitternow caches its transform and computes the distance once;Update()does not even callInRange()any more. Counted call for call, the replacement makes the same number of native calls and the same one square root as vanilla, and adds a Harmony wrapper frame, three delegate null checks, a try/catch region and two FieldRef invocations. Net negative. - RagdollPhysicsMats.
CharacterRagdoll.FixedUpdatenow guardsSetPhysicsMats()withif (character.IsLocal), so this redundancy filter can only save one character's worth of native material writes instead of every character's. It was the most complex patch in its lane (a ConditionalWeakTable, a two-phase pending/promote signature and a 300-frame / 5-second material audit); that trade no longer holds.
Adapted to the new build
Four real behavioural deviations and one logic gap:
- RemoteClusterAnimationThrottle called
SetPhysicsMatson every tick, evaluating or not. Vanilla no longer calls it for remote characters, and remote characters are the only ones this patch touches, so continuing to call it would be adding per-step nativecol.sharedMaterialwrites the game no longer performs. Both call sites, the accessor, the delegate and theset-physics-mats-faileddiagnostic are gone. - IsLookedAtScan.
UIPlayerNames.Initchanged from returning anintto taking anAction<int>callback - the one assertion in the whole plugin that would now fail, which after the gate removal means the patch would be skipped rather than silently wrong.Update()also hoists the blind/struggling test to the top and returns early, so the replacement follows, and the camera null check moved below that gate: otherwise a null camera while blind would hand the method back, where vanilla never touches the camera and cannot throw. - BarAfflictionLayout. The game added
size == 0f -> width = 0fand anApproximatelysnap. The old predicate only forecast theLerp, so it would skip a write that should have snappedwidthtosize. Rewritten branch for branch, and the float equality is now a bit comparison, because-0f == +0fwould drop a real write. - GenericOptimizerRange (confirmed on the way out): the patch's
candidate == null -> continuewas itself a deviation. Both the old and the new vanilla loop dereferencecharacter.dataunguarded, so a null element throws in both. - RemoteRagdollLod gained
data.carrierCondor != nullin its never-downgrade list. That field is new: a character being carried by a condor keepscurrentRagdollControllat 1, so it passes the control threshold, while the condor takes it far past 60m - and none of the three existing carry conditions could see it. Such a character would have gone limp mid-flight.
Verified as harmless: Bodypart's SetPositionAndRotation merges and the dropped isKinematic guard
in FollowRotation_Rotation (that guard was dead code - its only caller returns early when kinematic);
CharacterMovement.FixedUpdate hoisting four currentRagdollControll multiplications out of the loop;
the new AdjustDrag (AdjustDrag(1f) == 1f exactly, and its preimage is unique, so
BodypartDragIdentity's identity test is unaffected); ConfigureIK evaluating the left hand first now
(so IKItemGuard's two per-side reasons swap roles); the ItemPhysicsSyncer.FixedUpdate rewrite; and
the squared-distance rewrites in TumbleWeed.GetTarget and GenericOptimizer.
Every remaining patch's Initialize assertions were checked against the shipped DLL with
System.Reflection.Metadata rather than against decompiled text. Apart from that one Init signature,
all of them still pass.
Comment convention change
There were roughly 450 File.cs:NNN references in the comments. A game update invalidates all of them
at once, and re-anchoring them had grown more expensive than the code fixes. New convention: name the
type and the method, not the line. Existing line references are left as historical coordinates rather
than re-anchored one by one.
None of the above is verified in game. See TESTING.md; the 1.4.0 acceptance points are at the top of its final section.
1.3.0
Still 44 patches and 40 switches, 38 on by default. Seven concurrent read-only audits over the whole plugin, then a repair pass over everything they turned up that could be settled by reading code rather than by playing. One behaviour regression, three real defects in the group installer, one threshold that was a magic number and is now derived from the assets, and a long list of comments that were confidently wrong.
The regression, first
IKItemGuard froze the right IK hand target on any held item whose prefab has a Hand_R child but
no Hand_L. C# evaluates a.position = f() receiver-first, so on such an item vanilla completes
both right-hand writes (CharacterAnimations.cs:397-398) and only then raises at :399. The guard
skipped the whole item branch, so the right hand stopped tracking the item - every physics frame, for
as long as it was held. It now reproduces those two writes through delegates bound to the two
internal helpers before skipping, and books the two cases separately
(right-hand-node-missing vs left-hand-node-missing). This is the same class of mistake as the
1.1.0 emote-wheel regression: skipping the writes vanilla performs before it throws.
Group installs were not all-or-nothing
Three defects, all in the machinery that makes one switch install several patches as a unit.
- HarmonyX registers a prefix into the patch info before generating the wrapper
(
PatchProcessor.Patch), so when wrapper generation throws,GetPatchInfoalready lists our method. The installer recorded that as an installed patch,IsInstalledreturned true for a patch that had failed, and the group rollback was skipped entirely - leaving a half-installed switch with nothing in the log to say so. A record in that state is now flagged separately: never live, never reinstalled (that would attach the prefix twice), still cleaned up on uninstall. - Rolling a group back claimed members had been withdrawn cleanly when their
Resetthrew. It asked a question that excludes exactly that case, took the success branch, and discarded the real reason. It now reports which of the two failure shapes happened. - A member that was already live before a failed install kept running, unreported. It is now named as a deliberately half-installed switch.
Also: the failed-startup rollback shut the engine-global lanes down after detaching the patches,
the opposite order from a normal shutdown, leaving a window where Physics.reuseCollisionCallbacks
was still set with the Snowball guards already gone.
The cluster radius is no longer a magic number
ExplosionScale used a fixed 3.5m to decide whether an explosion overlaps a recent one. That is
smaller than the main dynamite's own fireball radius, so two detonations 5m apart - visually almost
fully overlapped, which is precisely the fill this patch exists to cut - counted as isolated and each
kept five full-size orbs. The threshold is now derived from the orb geometry the assets carry: the orb
root's localScale (4.4401164) times the Icosphere mesh AABB extent (1.372998), i.e.
baseScale * 6.0962 - about 6.10m for the main dynamite and 3.05m for the Small/Fae variants. A NaN,
zero, negative or infinite baseScale falls back to the old constant. The test uses the current
explosion's radius, because what is being decided is whether this fireball lands on screen that is
already covered.
Two more things in that patch:
- The scene-handle debounce is gone. It guarded against last scene's positions being counted as
neighbours, but the 2.5s window already does that: every scene load in this game is
LoadSceneMode.Singlebehind a loading screen that also waits for the local character to spawn, and segment changes areSetActivewithin one scene. Its only observable behaviour was a falsescene-handle-thrashreport, so that reason no longer exists. - The light tier's caps were
Math.Min(pointCap, 2)andMath.Min(subPointCap, 1)against a 2/1 base - identity - while the comment claimed it cut counts from one neighbour on. Those caps are now named constants of their own, so the no-op is explicit and giving that tier a real reduction is a one-constant edit.
Diagnostics that were measuring the wrong thing
The repair counters are cited as evidence of vanilla faults, so a counter that fires when nothing is wrong is worse than no counter.
WeightRefreshCoalescingcounted a repair reason on the path where the patch succeeds.same-frame-duplicate-skippedis removed.PocketBehaviorGuardcounted one per bot per frame.Character.playeris null for bots by design, so skipping the call is right and the accounting was not - the same exclusionParachutealready had.AnimatedMouthMaterialtested the renderer and the texture array before the audio source, but vanilla's guard short-circuits on the audio source and runs no body at all, so a mouth with no audio source was reported as a fault every frame.RemoteClusterAnimationThrottlesilently swallowed a null rig builder and an invalid animation graph and reported success. Vanilla raises there every tick. The guards stay - reintroducing a per-step exception would be worse - but a malformed rig is now visible asrig-builder-unavailable/animator-graph-invalid.
Hot paths
LightVolumeSampleCacheallocated a carrier object on every cache miss, the last per-call allocation in the plugin. The Postfix now recomputes the voxel key from the same argument through the same pure function, so nothing is carried across the call.RemoteRagdollLodread a static property behind a native alive check before its per-frame decision cache - useless on the cache-hit path, which is where the roughly twenty thousand calls a second land.AnimatedMouthMaterialread two material properties by string name every frame; the ids are resolved once at install.RemoteItemInterpolationThresholdpaid three native calls before four managed checks that could reject the frame. Single-player and non-synced items now cost none of them.DetailBodypartThrottlecomputed the physics tick twice per detail part per step, and never checkedTime.fixedDeltaTime: a zero step would freeze the tick and permanently skip some parts, the one direction in which a stuck tick is not safe.PlayerNameUiWritesresolved the same references two to four times each.ItemScaleRedundantWritedid three redundant destroyed-object checks per call.
Arguments and assertions that were wrong
StatusEmitterScancallsScreenVFX.StartFX(), whose0.5fdefault is compiled into this DLL. Nothing asserted the overlay chain at all; all four members are asserted now, plus that the default is still0.5f- which no signature check can see.RopeSegmentGuardjustified skipping a frame with "idempotent copy, recomputed next frame". That value is written once per grab and is constant for the climb, so the copy only ever matters on the first successful frame. The real argument is that the rotation it feeds is restored within the same physics step.BoneWeightCapclaimed URP rewritesantiAliasingevery frame; that write is in the pipeline constructor. Nothing in the game or URP writesskinWeightsand nothing changes quality level, so the periodic reassert is kept only as insurance against writers that cannot be seen from here, and now says so. Its restore also cleared its bookkeeping after the write-back rather than in afinally, so a throwing restore left a stale snapshot that the next disable would write over the player's newer value.RemoteRagdollLodAnimatorlisted an accepted cosmetic cost - a stale water flag - that does not exist, because the replacement does write the parameter that flag reads.RemoteRagdollLodPhysicsrejected re-bounding the friction-audit window on the grounds that it could not be shown whether a delegate call is seen by a Harmony detour. MonoMod's own startup selftest does exactly that and asserts the hook answered, so BepInEx could not start if it were not. The real reason is stronger:Collider.sharedMaterialhas exactly four writers in the whole game and all four are inside the method being skipped, so in vanilla there is nothing for the missed audits to correct.RemoteRagdollLodclaimed the cluster lane validates the fixed timestep for both lanes. It validates it for itself.Acquire()sat before two throwing statements while the comment asserted nothing below could throw.
Subtraction
DetailBodypartThrottle kept its own copy of the eligibility list and now calls the shared one.
RemoteRagdollLodAnimator emitted two dynamic methods to reach Animator members that are public in
an assembly this project already references. Nine assertions on fields ExplosionScale neither reads
nor writes are dropped - a shape check cannot protect an argument that depends on serialized
values, which is what those arguments depend on. Plus: two config/report methods whose docstrings
claimed hot reload used them (it does not), an unused module-cleanup channel, three unused emit
helpers, and twelve unused using directives.
Documentation
The README claimed 13 orbs per explosion in one paragraph and 10 in another, and that the patch only
cuts counts (it cuts counts and shrinks them). Collision.contacts is evaluated six times, not seven
or four - four places said three different numbers. TumbleWeedTargetScan is now a declared
exception to the default-on rule with its cost written out. Five section subtotals in the test guide
disagreed with their own tables, the results template omitted five items, and the README's
per-section directory labels were wrong three times out of five, because the sections are organised by
the kind of optimisation and the directories are not.
None of the above is verified in game. See TESTING.md; its acceptance order is unchanged.
1.2.0
44 patches and 40 switches, down one from 1.1.0: ParticleCountCullerWrite was removed. A third
audit pass, this time with the shipped game assets decoded rather than only the decompiled code,
which corrected several claims this mod had been making since 1.0.0 - including the one that made
that patch pointless. One of the fixes is a behaviour regression 1.1.0 introduced; the rest are
correctness, blast-radius and honesty repairs.
Removed
ParticleCountCullerWrite. Decoding every.assetsfile and every level shows the component it patches,ParticleCountCuller, has zero instances in the shipped game - the class is compiled into the assembly but nothing carries it. Its target method is therefore never called, so the patch could only ever cost a detour. It followed the rule the reserved config keys andJointProjectionwere dropped under: a patch whose win is provably zero does not ship.
The regression, first
EmoteWheelGuardcould leave the emote wheel stuck open. Vanilla short-circuits in two places -canEmotereturns early for a dead character without readingrefs.stats, andcanEmote && input.emoteIsPressednever touchesinputwhencanEmoteis false - so on those frames vanilla does not throw at all; it falls through and closes an open wheel. The guard was skipping the whole method there, suppressing that close and latchingusingEmoteWheel, which also latches the pause-menu gate, the cursor lock and a movement gate. The guard now mirrors both short circuits and only skips a frame where vanilla would genuinely dereference a null. Introduced by theinputcheck added in 1.1.0.
What the assets said
- An explosion was never 13 orbs. The
4 / 2defaults inExplosionEffectare overridden by the serialized values on the three prefabs that actually carry the component: the main dynamite is3 / 2, i.e. 10 orbs, and the small and fae variants are3 / 0, i.e. 4. The old cap of3 / 1therefore did nothing at all to the first number. The cap is now2 / 1: 5 orbs on the main dynamite, 3 on the variants. ExplosionScaledoes not cover every explosion. Reverse-resolving everyexplosionPrefabreference in the asset library finds exactly threeExplosionEffectinstances, all dynamite. The rocket, rocket-pack and ghost-ball explosions instantiate prefabs whose cost is in particle systems and which carry noExplosionEffectat all, so they are untouched. Said plainly in the README now instead of being listed as covered.- Fewer orbs also means a softer camera shake. Every orb prefab carries two self-firing
AddScreenshakechildren, and the game stacks those rather than replacing them, so orb count and shake strength move together. That is a second visual cost this mod was not declaring. It is declared now; the switch stays on by default, because the explosion case is the one where the default-off rule would leave the worst stutter in the mod unaddressed. - The claim that shrinking the orbs is where most of the win comes from is withdrawn, and this one is now settled rather than merely unproven: both particle systems inside an orb are authored with a local scaling mode, so the root scale the shrink writes never reaches them. Their particle sizes, burst counts and lifetimes are unaffected. The shrink only reduces the orb's sphere mesh, so the count reduction is what cuts the particle work, the mesh work and the screen shake together.
Two more asset corrections, no code change
- The spore volumes really do run unthrottled: 1571
StatusEmitterplus 108WindAffectedStatusEmitterinstances exist and nothing distance-gates them. The game's own script disabler,GenericOptimizer, is only on arrow shooters and venus fly traps. The spore clouds do carry aParticleCulleron their particle child, but it only starts and stops the particles - it cannot touch the damage volume, and the manager that drives it visits three cullers per frame. GenericOptimizerRangeis worth more than its own comment claimed, not less. All 2133GenericOptimizerinstances override the range with a finite value (82m or 60m); thefloat.PositiveInfinityin the class is only the field default, and serialized values win.
Declared, not changed
TumbleWeedTargetScanis now written down as the second declared exception to the default-on rule, alongsideExplosionScale. It has shipped on by default since 1.0.0 while not being behaviourally equivalent, and that was never said out loud. What it gives up, exactly: the target solve runs once every 5 physics ticks (0.1s) instead of every tick, so a newly appearing closer or better-angled character is adopted up to 0.1s late. The force is still applied every tick, and its direction is recomputed from the cached target's current centre, so the lag only affects which character is chased, never the tracking of the one already chosen; a target that died or was destroyed forces an immediate rescan. Exposure is narrow - tumbleweeds only exist in the desert / mesa biome. The switch stays on by default, because that is the map the framerate complaint came from. No code changed.
Correctness
StatusEmitterScanrestored vanilla's ordering in the warning-end branch. It cleared the in-zone flag after calling the screen effect, so an effect that threw left the flag set and the branch re-entered - and re-threw - every following frame, where vanilla's single throw is self-limiting.StatusEmitterScannow reads__runOriginal. It is a full replacement with side effects - it applies spore damage and mutates a static dictionary - and without that parameter another mod's decision to suppress the original was silently ignored.StatusEmitterScanno longer reaches its static overlap dictionary through reflection on the hot path, and its two deferral paths were reworked: an unavailable ragdoll centre is now handled rather than handed back for the original to throw on again, and the afflictions check moved to the point where vanilla actually dereferences it, which is a damage tick rather than every frame.- The distance tier of
RemoteRagdollLodmeasures from whoever you are watching, not from your own character. While spectating or after a warp those are different points, and the old basis could freeze the pose of someone standing right in front of the camera. - A downgraded remote character no longer keeps a stale crouch or sprint height. The two target heights are written only by the method the tier skips, and they feed the standing force and the ground-check ray length, so a pose change now returns the character to full behaviour.
- The warm-up before a first downgrade counts physics steps instead of rendered frames. It exists to guarantee the animation targets have been written at least once, and those are produced per physics step, so at a high frame rate the old count could elapse before a single one had run.
- The physics-step index is derived from
Time.fixedTimeAsDouble. The float it used accumulates from process start and eventually stops being able to separate two adjacent steps. - The phase that staggers throttled characters no longer uses the low bits of the Unity instance id. Objects created in one batch get consecutive ids, so with an interval of two every remote character could land on the same tick - turning the flat cost the stagger exists to spread back into a spike.
- A failing replacement in
RemoteClusterAnimationThrottlenow disables itself for that one character rather than for the whole session, with a process-wide cap for genuinely systemic failures.
Blast radius
- A switch that covers several patches is now installed all-or-nothing. If one member cannot install, the ones that did are withdrawn and reported, because those sets exist precisely because a half-installed combination is a state nothing was reasoned about.
- Turning off the Snowball guard no longer leaves
Physics.reuseCollisionCallbackson for a frame. The flag is lowered before any patch toggle and re-armed after, instead of waiting for the next frame's poll - that window ran vanilla Snowball against exactly the reused instance the flag's safety argument depends on it not seeing. - Both engine-level lanes now restore only their own write, and only while it is still the live value, and they re-sample on every enable. Previously the collision flag was written back unconditionally, which would clobber another mod that had set it, and both lanes could restore a stale snapshot after an off/on cycle.
- The two Snowball patches finally register their
Reset; it had been dead code. - A patch whose
Resetthrew is no longer reported as installed forever. It used to answer "already installed" to every later enable while its Harmony methods were long gone. - A failed startup now shuts the engine-level lanes down even when the Harmony uninstall did not
fully succeed. The component keeps receiving
Updateeither way. BinocularSunGuardalso guards the ragdoll centre the method dereferences past its own guard clause, and it now reproduces vanilla's short-circuit before checking anything behind it, so a non-Mesa or night-time frame no longer books repair reasons vanilla never earned. Those counters are quoted as evidence of vanilla defects, so a false positive costs more than the branch.
Documentation corrections
CharacterMovement.OnCollisionreads the allocatingCollision.contactsproperty six times, not seven:CharacterMovement.cs:928,:948,:953twice and:955twice. The docs and the patch's own comment had claimed seven since 1.0.0. The patch itself is unchanged - it still readsGetContact(0)once - only the count quoted for the vanilla method was wrong.IKItemGuardnow books its repair reason separately for a missing right-hand node and a missing left-hand node, so the log says which side is absent instead of only that one of them is.WeightRefreshCoalescingno longer books a diagnostic reason on its success path. Booking one there broke the project rule that repair counters only ever record repairs and fallbacks, which is what makes a non-zero counter evidence of a defect.
None of the above is verified in game yet. See TESTING.md.
1.1.0
Same 45 patches and 41 switches as 1.0.0, no additions and no removals. This release is a second audit pass that corrects four equivalence defects, widens two guards, and fixes ExplosionScale, whose cluster reduction turned out not to trigger in the case it was written for.
Equivalence repairs
StatusEmitterScan- the replacement returned early from the warning branch, where vanilla falls through into the tail that clears the tick timer. A poison volume you walked out of therefore kept its accumulated tick time instead of resetting it, so re-entering could deal a tick sooner than vanilla. The branch now falls through exactly as the audited method does, and the warning FX is resolved before any field is written so a deferral leaves the emitter untouched.StatusEmitterScan- the range test is back to a real square root. Comparing squared magnitudes is not exact: the two forms disagree inside the rounding band around the threshold, and the in-range side of that band is a live damage tick. The saving that remains is computing the distance once per call instead of the two to four times vanilla computes it, plus oneTryGetValuein place of aContainsKeyand three string-keyed indexer lookups.ParticleCountCullerWrite- the redundant-write filter remembered the last value this patch wrote. That is only equivalent while nothing else writes the same emission module, and the game does write one elsewhere (the bee swarm decrements its own emission multiplier every frame while dispersing). The filter now compares against the live value on the particle system, which stays exact no matter who else writes it.RemoteClusterAnimationThrottle- the explicitRigBuilder.SyncLayers()call was removed on the grounds thatEvaluateperforms it. It does, but only when the builder owns a valid graph of its own, and PEAK builds the rig into the animator's graph instead, which leaves that property unset.Evaluatewas therefore a no-op and the IK constraint data stopped being pushed into the jobs. Both calls are restored, as in the audited method.
Wider guards
BinocularSunGuard- also guardsMainCamera.instanceand the achievement manager singleton, which the same method dereferences unguarded past its own guard clause.EmoteWheelGuard- also guardsCharacter.input, the one remaining unguarded link in the chain the method walks, and reportslocalCharacterseparately from the rest.
Behaviour on failure
RemoteClusterAnimationThrottle- a replacement that fails on the same physics tick it was handed no longer yields that tick to the original, because doing so would runRotateCharactertwice on one tick and leave the rig root rotated. The tick stays skipped; every later tick goes to the original.
ExplosionScale now attacks the right cost
Reported: ten sticks of dynamite detonating inside one metre still ran at about five frames per second. Confirmed as a design error rather than a failure to apply.
- The cluster tier could not trigger in that scenario. It needed two prior explosions within 1.2s, but a single explosion keeps spawning orbs for a full second after it starts, and its neighbours are what the new orbs draw over - so the window was shorter than the overlap it was meant to detect. The window is now 2.5s and the light tier starts at one neighbour instead of two, so every explosion after the first in a pile takes a reduction.
- Reducing the orb count was never going to fix it on its own. An orb is an animated transparent sphere roughly ten metres across, so an observer inside the pile has every surviving orb covering the whole screen; the frame cost is orb count times screen area and only the count was being reduced. The cluster tiers now also shrink the orbs, to 85% and 60% of their authored size, which is where most of the saving comes from - screen area goes with the square of the radius.
- A lone explosion is unaffected: it sees no neighbours, so its size is exactly what the prefab authored. The size field is only ever lowered, feeds no damage path, and is read by exactly one line in the whole assembly.
None of the above is verified in game yet. See TESTING.md, steps A6, A8, A9, B13, E1 and E2.
1.0.0
First public release. 45 patches, 41 switches, 39 patches enabled by default.
Vanilla fault repairs
Eight guards that remove per-frame exception floods. Each skips only the frame whose data is missing and defers to the untouched original otherwise.
RunBasedValues- null run-data dictionaries during serializationRopeSegmentGuard- empty rope segment list while climbing (184 exceptions in one session)IKItemGuard- item prefab missing hand nodes during an equip (135)EmoteWheelGuard- incomplete local character while spectating, respawning or quitting (2315)PocketBehaviorGuard- unlinkedCharacter.playerduring the join window (14)BinocularSunGuard- two unguarded singletons in the astronomy-badge check, one throw per frame while binoculars are raised (1544)ItemOptimizerDeregisterGuard- vanilla passes a null key toDictionary.Removefrom its own disable-on-null path (67)Parachute- conservative cache clear on incomplete character/slot/backpack state
Equivalence-proven optimizations
Same result as vanilla, less work. Squared distances, hashed Animator parameters, dictionary lookups, merged scans, and identity filters that skip a write only when the write provably changes nothing.
Campfire, CampfireProtectionMerge, HeatEmissionScan, RagdollPhysicsMats, ItemCollisionMode, GenericOptimizerRange, ItemDatabaseNameLookup, IsLookedAtScan, ItemAudioManagerHash, BarAfflictionLayout, AnimatedMouthMaterial, CollisionCharacterLookup, AnimatorValuesHash, PlayerNameUiWrites, BodypartDragIdentity, BodypartMovementForceIdentity, LightVolumeSampleCache, CollisionContactsNoAlloc, SnowballContactGuard, ZombieScanRange, ZombieSpawnScanRange, SleepingZombieScan, WeightRefreshCoalescing, ItemScaleRedundantWrite, RemoteItemInterpolationThreshold, EyeLookComponentCache, TumbleWeedTargetScan, StatusEmitterScan, ParticleCountCullerWrite
Visual reduction, on by default
ExplosionScale- 13 orbs per explosion down to 7, with further reduction for clustered and distant explosions. Explosions are visibly smaller; this is the difference between a playable and an unplayable moment when a lobby throws dynamite together.
Opt-in, off by default
RemoteClusterAnimationThrottle- remote animation graph evaluated every other physics tick (25Hz stepping, unchanged speed)DetailBodypartThrottle- remote fingers, jiggle bones, toes and jaw at ~16HzRemoteRagdollLod- remote players past 60m become passive ragdolls (one switch, four patches)CollisionCallbackReuse- engine stops allocating aCollisionplusContactPoint[]per reported collisionGraphics.CapBoneWeights- two bone weights per vertex
Notes on what was removed before release
- The only Transpiler was dropped. IL rewriting proved unreliable alongside IL preprocessors, and its win only existed while someone stood on a bridge.
- A shadow distance and cascade cap was dropped: it silently overrode the in-game Shadow Distance setting, and forcing a single cascade made distant shadow edges shift while walking.
- An MSAA override was dropped: turning anti-aliasing off is a visual decision that belongs to the player.
- A Mesa heat-haze suppressor was dropped: the value it targeted measured 0.016 at runtime, i.e. effectively already off.
- Every numeric config entry was replaced by a switch. A number in a config file cannot be validated by the person editing it, and a wrong one fails quietly.
