Skip to content

Fix duplicate-key crash on BBCode IsBold font resolution (#3530)#3531

Merged
vchelaru merged 2 commits into
mainfrom
3530-bbcode-bold-font-duplicate-key
Jul 7, 2026
Merged

Fix duplicate-key crash on BBCode IsBold font resolution (#3530)#3531
vchelaru merged 2 commits into
mainfrom
3530-bbcode-bold-font-duplicate-key

Conversation

@kaltinril

@kaltinril kaltinril commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #3530.

Problem

Assigning a TextRuntime.Text containing a BBCode [IsBold=true] run could crash with System.ArgumentException: An item with the same key has already been added for the bold font's FontCache .fnt key. Hit by a shipping FlatRedBall game on a wave-complete message — the "crashes on 2nd load" symptom.

Root cause

The inline-BBCode font resolver (CustomSetPropertyOnRenderable.GetAndCreateFontIfNecessary) resolves the same font once per open/close tag and dedups via LoaderManager.GetDisposable(...) as BitmapFont. That dedup is unsound: GetDisposable returns null both when the key is absent and when the key is present but holds null — it cannot tell them apart.

The BBCode path caches whatever it resolves under the font key, including its last-resort Text.DefaultBitmapFont fallback. When that fallback is itself null — as under FRB, where Gum's default bitmap font is never assigned — the first resolution caches a null under the bold key. The next resolution sees GetDisposable(...) as BitmapFont == null, treats the occupied slot as empty, falls through, and calls AddDisposable, which defaulted to ExistingContentBehavior.ThrowException and threw on the already-present key. (A non-null cached font — including a non-null DefaultBitmapFont — is recovered by the cast and never triggers this; only a cached null poisons the slot.)

The non-BBCode MonoGame path sidesteps this by never caching the fallback — it applies ?? Text.DefaultBitmapFont transiently without caching. The inline-BBCode path was the one spot that cached the fallback and then re-added under the same key unguarded.

Fix

Pass ExistingContentBehavior.Replace at both AddDisposable calls in GetAndCreateFontIfNecessary, so a re-resolution heals the occupied slot instead of crashing. Replace (rather than Dispose-then-add) is required because the occupying value can be null or the shared default font, neither of which LoaderManager.Dispose can safely handle.

Tests

MonoGameGum.Tests/Runtimes/TextRuntimeBbCodeFontCachingRegressionTests.cs poisons the exact bold cache key so the dedup guard sees an empty slot, then asserts a [IsBold=true] Text assignment no longer throws and that the slot is actually replaced with a BitmapFont (so the test can't pass vacuously if the computed key ever drifts). Two tests cover the two AddDisposable sites the fix touches: the disk/DefaultBitmapFont fallback (the crash's exact line) and in-memory font creation. Each was confirmed to fail for the right reason (the duplicate-key ArgumentException) by reverting its fix line independently; the full MonoGameGum.Tests suite is green.

Assigning a TextRuntime.Text containing a BBCode [IsBold=true] run could
crash with "An item with the same key has already been added" for the
bold font's FontCache .fnt key.

The inline-BBCode font resolver (CustomSetPropertyOnRenderable.
GetAndCreateFontIfNecessary) resolves the same font once per open/close
tag by design and dedups only via GetDisposable(...) as BitmapFont. When
an earlier resolution cached a value that cast cannot recover (a null or
shared Text.DefaultBitmapFont fallback cached because no .fnt was on disk
yet), the guard sees an empty slot, falls through, and AddDisposable
collides with the still-occupied key. AddDisposable defaulted to
ExistingContentBehavior.ThrowException, so it threw.

Pass ExistingContentBehavior.Replace at both AddDisposable calls in
GetAndCreateFontIfNecessary so a re-resolution heals the poisoned slot
instead of crashing, matching the protection already present on the
raylib and non-BBCode MonoGame font paths. Replace is used rather than
Dispose-then-add because the occupying value can be null or the shared
default font, which LoaderManager.Dispose cannot safely handle.

Adds MonoGameGum.Tests regression coverage.
Supplements the regression test: adds a second test exercising the in-memory font-creation AddDisposable site (the disk/DefaultBitmapFont fallback site was already covered), plus a heal assertion proving the poisoned slot is replaced so the test can't pass vacuously if the computed cache key drifts. Verified red-for-the-right-reason per line; full MonoGameGum.Tests suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vchelaru vchelaru enabled auto-merge (squash) July 7, 2026 13:05
@vchelaru vchelaru merged commit 95963c6 into main Jul 7, 2026
3 checks passed
@vchelaru vchelaru deleted the 3530-bbcode-bold-font-duplicate-key branch July 7, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BBCode IsBold crashes on 2nd load

2 participants