From 35a0eca34bf1470bc48d9dcd4f2019db24555d13 Mon Sep 17 00:00:00 2001 From: Vest Date: Sat, 27 Jun 2026 00:30:01 +0200 Subject: [PATCH 1/2] Fail gracefully when data sources are missing or malformed Three NPE crashes when loading a character against a partially-loaded or malformed data set are replaced with clear, actionable errors: LstObjectFileLoader.loadLstFile no longer calls .get() blindly on the Optional returned by LstFileLoader.readFromURI. readFromURI already logs and returns Optional.empty() for non-fatal IO/decode errors (e.g. a non-UTF-8 .lst), but the caller's blind .get() turned that into a NoSuchElementException that aborted the entire source load. The file is now skipped, the upstream log line remains, and loading continues. CharacterManager.openPcInternal and createNewCharacter now null-check the DataSetFacade parameter before dereferencing it. When the source load aborts, callers in PCGenFrame previously passed the unpopulated ref through, producing an NPE on dataset.getCampaigns() outside the existing catch block. The methods now log and surface a localised dialog instead. PlayerCharacter's Collection constructor now fails fast when SizeUtilities.getDefaultSizeAdjustment() returns null. The previous behaviour was to NPE 20+ frames deep in the SizeFacet wiring while loading the unselected race; users got a stack trace with no hint at the actual cause. The fix logs the missing-default-size diagnostic once at SEVERE (with concrete guidance on where size definitions live) and throws IllegalStateException, caught and shown as a dialog by CharacterManager. --- code/src/java/pcgen/core/PlayerCharacter.java | 15 +++++++++++++++ .../persistence/lst/LstObjectFileLoader.java | 11 ++++++++++- code/src/java/pcgen/system/CharacterManager.java | 16 ++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/code/src/java/pcgen/core/PlayerCharacter.java b/code/src/java/pcgen/core/PlayerCharacter.java index 54be7e75f8b..802b76a5c3b 100644 --- a/code/src/java/pcgen/core/PlayerCharacter.java +++ b/code/src/java/pcgen/core/PlayerCharacter.java @@ -228,6 +228,7 @@ import pcgen.core.analysis.ChooseActivation; import pcgen.core.analysis.DomainApplication; import pcgen.core.analysis.RaceUtilities; +import pcgen.core.analysis.SizeUtilities; import pcgen.core.analysis.SkillModifier; import pcgen.core.analysis.SkillRankControl; import pcgen.core.analysis.SpellCountCalc; @@ -542,6 +543,20 @@ private PlayerCharacter(PlayerCharacter from) public PlayerCharacter(Collection loadedCampaigns) { LoadContext context = Globals.getContext(); + // Fail fast on a half-loaded dataset: without a SizeAdjustment flagged + // ISDEFAULTSIZE:YES, damage scaling, equipment sizing and the size facet + // would all NPE deep in facet wiring. + if (SizeUtilities.getDefaultSizeAdjustment() == null) + { + String gameModeName = SettingsHandler.getGameAsProperty().get().getName(); + Logging.errorPrint("Game mode '" + gameModeName + + "' has no default size: no SizeAdjustment was loaded with ISDEFAULTSIZE:YES." + + " Sizes are typically defined in a *__sizes.lst file inside the gamemode's" + + " Core Rules data source (e.g. data/pathfinder/.../core_essentials/ce__sizes.lst)." + + " Exactly one size in that file must be flagged ISDEFAULTSIZE:YES."); + throw new IllegalStateException("Game mode '" + gameModeName + + "' has no default size. Check earlier log errors for the missing data source."); + } id = CharID.getID(context.getDataSetID()); AbstractReferenceContext refContext = context.getReferenceContext(); controller = refContext.constructNowIfNecessary(CodeControl.class, "Controller"); diff --git a/code/src/java/pcgen/persistence/lst/LstObjectFileLoader.java b/code/src/java/pcgen/persistence/lst/LstObjectFileLoader.java index d6e6b29f911..7d1ee79147d 100644 --- a/code/src/java/pcgen/persistence/lst/LstObjectFileLoader.java +++ b/code/src/java/pcgen/persistence/lst/LstObjectFileLoader.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Objects; import java.util.Observable; +import java.util.Optional; import java.util.Set; import pcgen.cdom.base.CDOMObject; @@ -281,7 +282,15 @@ protected void loadLstFile(LoadContext context, CampaignSourceEntry sourceEntry) String aString; try { - aString = LstFileLoader.readFromURI(uri).get(); + // readFromURI returns Optional.empty() when it logs a non-fatal IO/decode error + // (e.g. a non-UTF-8 .lst). Skip the file rather than aborting the whole load. + Optional read = LstFileLoader.readFromURI(uri); + if (read.isEmpty()) + { + setChanged(); + return; + } + aString = read.get(); } catch (PersistenceLayerException ple) { diff --git a/code/src/java/pcgen/system/CharacterManager.java b/code/src/java/pcgen/system/CharacterManager.java index 0232c60a5ab..7556d3a56dd 100644 --- a/code/src/java/pcgen/system/CharacterManager.java +++ b/code/src/java/pcgen/system/CharacterManager.java @@ -84,6 +84,14 @@ private CharacterManager() */ public static CharacterFacade createNewCharacter(UIDelegate delegate, DataSetFacade dataset) { + if (dataset == null) + { + Logging.errorPrint("Unable to create character: data sources failed to load (see earlier errors)"); + delegate.showErrorMessage(LanguageBundle.getString("in_cmCreateErrorTitle"), + LanguageBundle.getFormattedString("in_cmCreateErrorMessage", + "data sources failed to load - see earlier errors in the log")); + return null; + } @SuppressWarnings("rawtypes") List campaigns = ListFacades.wrap(dataset.getCampaigns()); try @@ -169,6 +177,14 @@ public static PlayerCharacter openPlayerCharacter(File file, UIDelegate delegate private static PlayerCharacter openPcInternal(File file, UIDelegate delegate, DataSetFacade dataset, boolean blockLoadedMessage) { + if (dataset == null) + { + Logging.errorPrint("Unable to load character " + file + ": data sources failed to load (see earlier errors)"); + delegate.showErrorMessage(LanguageBundle.getString("in_cmLoadErrorTitle"), + LanguageBundle.getFormattedString("in_cmLoadErrorMessage", + file, "data sources failed to load - see earlier errors in the log")); + return null; + } @SuppressWarnings("rawtypes") List campaigns = ListFacades.wrap(dataset.getCampaigns()); try From 027008eff99bae983965d33ae149b43a42040466 Mon Sep 17 00:00:00 2001 From: Vest Date: Sat, 27 Jun 2026 00:30:07 +0200 Subject: [PATCH 2/2] Make 'Could not find campaign' message actionable Globals.getCampaignKeyed previously logged just the missing campaign key, leaving users with no hint that the campaign needed to be installed somewhere or where that 'somewhere' was. The message now names the lookup mechanism (looking for a .pcc file declaring CAMPAIGN:) and points at the vendor and homebrew data dirs as the standard drop-in locations for third-party (non-OGL) data. --- code/src/java/pcgen/core/Globals.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/code/src/java/pcgen/core/Globals.java b/code/src/java/pcgen/core/Globals.java index 1a044561948..e810c63cf2b 100644 --- a/code/src/java/pcgen/core/Globals.java +++ b/code/src/java/pcgen/core/Globals.java @@ -127,7 +127,11 @@ public static Campaign getCampaignKeyed(final String aKey) final Campaign campaign = getCampaignKeyedSilently(aKey); if (campaign == null) { - Logging.errorPrint("Could not find campaign: " + aKey); + Logging.errorPrint("Could not find campaign: " + aKey + + " - no loaded .pcc file declares CAMPAIGN:" + aKey + "." + + " If this is third-party (non-OGL) data, install the .pcc into the" + + " vendor data dir (" + PCGenSettings.getVendorDataDir() + + ") or homebrew data dir (" + PCGenSettings.getHomebrewDataDir() + ")."); } return campaign;