[0.4.5] update, 2026-08-29 
(A purely technical bugfix update, no new features at this time)
Streaming: transient loads were never retired
FStreamableHandle::IsActive() is !bCanceled && !bReleased. A handle that has
finished loading is still active, so the prune predicate never matched one.
RequestAsyncLoad()
|
v
+----------+ load finishes +-----------+ last ref drops +----------+
| loading | --------------> | completed | ---------------> | released |
+----------+ +-----------+ +----------+
loading completed released
IsLoadingInProgress() true false false
IsActive() true true (*) false
(*) still active once the load finishes - which is why a prune written
as !IsActive() never retired a completed request
- Fixed: the prune now tests
IsLoadingInProgress(). Previously every sound,
attenuation, concurrency, VFX and decal streamed for a transient event stayed
resident for the life of the world, and IsTickable() never returned false again
once any event had streamed - a permanent per-frame cost with automatic reverb off.
before handles: 1 2 3 4 5 6 ... grows for the life of the world
after handles: 1 2 1 0 1 0 ... settles back to zero
- Warmed visual assets moved to a bounded 32-entry retention window. Pruning alone
would have made the warm-up pointless (the assets would be collected before reuse),
so a small recent window keeps repeated events fast without unbounded retention.
PreloadEvent remains the guarantee for first-use VFX.
Networking: server-originated events fix
The autonomous-proxy early-out that stops a client double-hearing its own predicted
event was also swallowing events the server raised on that client’s pawn, because
both paths funnelled into the same multicast.
Who hears a footstep raised on Player A pawn?
| Player A | Player B | listen
| (owner, | (sees a | server
| autonom.) | sim proxy) |
------------------------------+------------+-------------+--------
A predicts locally, server | muted | hears | hears
echoes it back | (correct) | |
------------------------------+------------+-------------+--------
Server raises it directly
before (0.3.0) | SILENT ! | hears | hears
after (0.4.0) | hears | hears | hears
- Fixed: the multicast RPCs carry
bOwnerAlreadyPlayed. Only a client’s own echo
is suppressed. Authority-originated audio now reaches the pawn’s owner.
- The rule moved into the pure decision model as
ShouldPlayReplicatedEvent, so it is
covered by an automation test rather than only reachable through a live session.
Surfaces: exact overrides were unreachable
before (0.3.0) after (0.4.0)
-------------------------- ----------------------------
1. SurfaceTypeMap <-+ 1. PhysicalMaterialOverrides
2. PhysicalMaterialOver. | 2. MaterialOverrides
3. MaterialOverrides ---+ 3. SurfaceTypeMap
4. keyword match swap 4. keyword match
5. DefaultSurface 5. DefaultSurface
- Fixed: exact soft overrides are consulted before the canonical map. The setup
wizard seeds SurfaceTypeMap[SurfaceType_Default], so any physical material left on
the default surface was captured at step 1 and its own override at step 2 could
never run. Documented in README.md.
- The material-decision cache (
SmartCache) is now capped at 512 entries and drops
entries whose FObjectKey no longer resolves. Content that mints materials at
runtime - the plugin’s own endless-runner showcases do exactly this - grew it
without limit.
Automatic reverb: clearing a requirement made it stricter
Confidence was scored against a fixed ceiling-inclusive maximum, so removing the
ceiling requirement lowered the achievable score without lowering the threshold.
Sample: 3 walls including an opposing pair, no ceiling. Threshold 0.75.
evidence ceiling 0.25 | horizontal 3 x 0.125 | opposing 0.25
scored ---- 0.000 ---|-------- 0.375 ---------|---- 0.250 ---- = 0.625
bRequireCeiling = true 0.625 / 1.00 = 0.62 fails (correct - no ceiling)
bRequireCeiling = false
before 0.625 / 1.00 = 0.62 fails <-- stricter!
after 0.625 / 0.75 = 0.83 passes <-- attainable evidence
- Fixed: confidence is normalized against the evidence the current configuration
can actually gather. Clearing bRequireCeiling now relaxes detection instead of
silently demanding all four walls. A ceiling found while not required still counts
and simply saturates the score.
Atmosphere volumes
USonantVolume binds its overlap delegates in BeginPlay, which runs after
overlaps are generated at registration. A pawn standing inside a level-streamed
volume - or spawned inside a placed one - was never seen. It now sweeps existing
overlaps on begin play. (ASonantBSPVolume was unaffected: it overrides the
always-live NotifyActorBeginOverlap virtual.)
- The atmosphere refcount saturates instead of overflowing;
Count + 1 was signed
overflow at the limit, before FMath::Min could clamp it.
Spatial components
USonantFoleyComponent and USonantSplineAudioComponent name their audio component
uniquely. A fixed NewObject name collided when one actor carried two of them.
- Both honour
UpdateInterval changes made after BeginPlay; the property is
BlueprintReadWrite but was only read once.
USonantSplineAudioComponent warns when its owner has no spline, instead of
silently doing nothing forever.
USonantProjectileTracker disables its tick once it has fired, until ResetWhizby.
Editor
- The setup wizard writes
Config/DefaultGame.ini via TryUpdateDefaultConfigFile().
SaveConfig() targets the per-user Saved/Config/<Platform>/Game.ini, so the
assignment the confirmation dialog promised was neither source-controlled nor
carried into a packaged build.
- No widget binds to the module instance any more - the status getters and button
handlers are statics. A nomad tab outlives ShutdownModule (and a Live Coding
reload), and the previous CreateRaw(this, ...) bindings dangled.
ExtendMenu result is null-checked.
Demo and showcases
ASonantGodRayShowcase, per generation step:
spawned floor 1 + pillars 2 + meteor 0.25 (every 4th) = 3.25
retired -------------- one per call ---------------------- = 1.00
net = +2.25
before actors _.-~=#@ climbs forever (physics bodies and
shadow-casting 50k-intensity lights)
after actors ====== flat at the cap
- Fixed unbounded actor growth in
ASonantGodRayShowcase, ASonantAutoShowcase
and ASonantDemoGenerator. Each retired exactly one actor per spawn call while
adding more than one. Cleanup now drains to the cap.
ASonantSpectacle checked TileMesh after SpawnActor, leaving three untracked,
never-destroyed actors behind on every row when it was unset. The check moved ahead
of the spawn, and the actor now ships engine-content defaults like its siblings.
- Fixed a null-material dereference in
ASonantGodRayShowcase
(UMaterialInstanceDynamic::Create returns null for a null parent).
- The showcases re-acquire the player pawn instead of latching a single null in
BeginPlay. Actor begin-play can precede the game mode spawning the pawn, which
left them permanently inert.
ASonantGodRayShowcase no longer overrides world exposure, bloom and volumetric fog
by default. It is opt-in via bOverrideWorldPostProcess, and auto-exposure is a
range again rather than a locked min == max == 1.0 that blinded any host level.
- The 1-5 keys now route by
EPhysicalSurface through the previously unused
TestSurfaces array. They built a FHitResult with no component and no physical
material, so all five resolved to DefaultSurface and sounded identical.
ImpactTestForce drives the impact tests instead of being ignored.
- The spline, whiz-by and occlusion exhibits take assignable sounds and say so on
their signs when unset. They were silent by construction, and the occlusion wing had
no audio source at all - its own source comment admitted it.
- The demo projectile assigns its mesh before enabling physics (there was no body to
simulate or receive the impulse), and its destroy timer holds a TWeakObjectPtr
rather than a raw actor pointer.
PrintCurrentSurfaceInfo traces with bReturnPhysicalMaterial and reports it.
Build correctness
- Added includes that only
-StrictIncludes (non-unity, IWYU) exposes:
Engine/World.h in SonantFeatureDemo.cpp, and Materials/MaterialInterface.h
plus PhysicalMaterials/PhysicalMaterial.h in SonantDemoCharacter.cpp. These were
pre-existing latent failures - an ordinary editor build does not catch them.
Tests
Now 11 automation tests, all passing.
Sonant.Decision.Acoustics.OpenTerrain
Sonant.Decision.Acoustics.EnclosedRoom
Sonant.Decision.Acoustics.Overhang
Sonant.Decision.Acoustics.CeilingNotRequired (new)
Sonant.Decision.Impacts.TierSelection
Sonant.Decision.Impacts.TierEdgeCases (new)
Sonant.Decision.Surfaces.DeterministicKeyword
Sonant.Decision.Surfaces.KeywordEdgeCases (new)
Sonant.Decision.Network.Boundaries
Sonant.Decision.Network.EchoSuppression (new)
Sonant.Integration.ConfigAssetLoads
Run them with:
Automation RunTests Sonant
- Extracted the replication echo rule into the pure decision model so the
“server-originated events reach the owner” fix has a regression guard.
- Added coverage for enclosure confidence when no ceiling is required, keyword edge
cases and determinism, impact-tier boundaries and fallback, and rate-limit
burst/idle behaviour.