Skip to content

Fix #7338: SEVERE log on shutdown when skill table re-sorts after close#7589

Open
Vest wants to merge 1 commit into
PCGen:masterfrom
Vest:fix/7338-shutdown-skill-rank-null-pc
Open

Fix #7338: SEVERE log on shutdown when skill table re-sorts after close#7589
Vest wants to merge 1 commit into
PCGen:masterfrom
Vest:fix/7338-shutdown-skill-rank-null-pc

Conversation

@Vest

@Vest Vest commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #7338 — on macOS Cmd-Q shutdown (and any path that calls closeAllCharacters while the Skill tab is mounted), SkillRankControl.getTotalRank is invoked with a null PlayerCharacter, producing a noisy SEVERE log with stack trace. The shutdown completes correctly; the user just sees a scary trace in the log.

Root cause

CharacterFacadeImpl.closeCharacter already documents the relevant invariant explicitly:

/*
 * Unfortunately, a dummy rather than null is necessary because the UI
 * does model swaps and such that do not pause events in the UI so that
 * it is trying to update things that do not exist
 */
theCharacter = DUMMY_PC;

CharacterLevelsFacadeImpl.closeCharacter did not follow the same pattern — it assigned theCharacter = null. During teardown, dataSet.detachDelegates() fires list-change events that propagate up to the skill tree's row comparator (RowComparator.compareSkillTreeViewModel.getDataCharacterLevelsFacadeImpl.getSkillBreakdownSkillRankControl.getTotalRank(null, skill)). The null guard at SkillRankControl.java:64-67 swallows the NPE but emits a SEVERE record with a new Throwable() for diagnostics — that's the ticket symptom.

Fix

Mirror the parent facade's pattern: introduce a static placeholder CLOSED_PC and assign it instead of null. Empty-but-valid PC state lets mid-teardown UI events read sane defaults without triggering downstream null-guard logging.

Two-line behavioral change, scoped to CharacterLevelsFacadeImpl. Does not modify CharacterFacadeImpl or any other facade.

Test plan

  • Added CharacterLevelsFacadeImplTest#testGetSkillBreakdownAfterCloseCharacterDoesNotLogError — installs a JUL Handler capturing SEVERE records, calls closeCharacter(), then getSkillBreakdown(level, skill) (the exact path on the bug's stack trace), and asserts no SEVERE record was emitted.
  • On master without the fix the test fails with the verbatim ticket stack trace (Asked to get total rank for null character ... at SkillRankControl.getTotalRank(SkillRankControl.java:66) at CharacterLevelsFacadeImpl.getSkillBreakdown(CharacterLevelsFacadeImpl.java:394)).
  • After the fix the test passes; the three pre-existing tests in the class also still pass (./gradlew :slowtest --tests 'pcgen.gui2.facade.CharacterLevelsFacadeImplTest').
  • Manual smoke test: load a character on macOS, hit ⌘Q, confirm clean shutdown log — should be done by a reviewer with macOS access (I can't reproduce the exact event ordering without GUI access in this environment, but the test reproduces the precise call chain reported).

cc @Azhrei — would you mind retrying the macOS Cmd-Q repro on this branch when you get a chance? The unit test reproduces the same getTotalRank SEVERE log via the same call chain you reported, and stops emitting it after this change.

…seCharacter (PCGen#7338)

CharacterFacadeImpl.closeCharacter notes that "a dummy rather than null
is necessary because the UI does model swaps and such that do not pause
events" and assigns DUMMY_PC. CharacterLevelsFacadeImpl.closeCharacter
ignored the same constraint and assigned theCharacter = null.

The mismatch surfaces during macOS Cmd-Q shutdown:

  CharacterFacadeImpl.closeCharacter
    1. charLevelsFacade.closeCharacter()  -> theCharacter = null
    2. dataSet.detachDelegates()          -> fires list-change events
       -> FilteredTreeViewModel.refilter -> TreeViewTableModel resort
       -> RowComparator.compare -> SkillTreeViewModel.getData
       -> CharacterLevelsFacadeImpl.getSkillBreakdown(level, skill)
       -> SkillRankControl.getTotalRank(null, skill)

getTotalRank null-guards and returns 0, but logs SEVERE with a stack
trace, producing the noise reported in the ticket.

Mirror the parent facade's pattern: assign a static placeholder
PlayerCharacter (CLOSED_PC) instead of null. Empty-but-valid state lets
mid-teardown UI events read sane values without triggering downstream
null-guard logging. Local change; doesn't touch the parent facade.

Adds CharacterLevelsFacadeImplTest#testGetSkillBreakdownAfterCloseCharacterDoesNotLogError
which captures SEVERE log records via a JUL handler around
closeCharacter + getSkillBreakdown. Fails on master with the exact
ticket stack trace; passes after the fix.
@Vest

Vest commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

Manually verified on macOS: loaded a character, hit ⌘Q. The shutdown log no longer contains the SkillRankControl.getTotalRank SEVERE trace from #7338 — only the normal Closed character … INFO line.

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.

[BUG] Exception regarding an uninitialized variable during PCGen shutdown procedure

1 participant