From 90e62eb7c68a539688853d87a614a2d73c522188 Mon Sep 17 00:00:00 2001 From: Vest Date: Wed, 20 May 2026 22:07:45 +0200 Subject: [PATCH 01/34] Remove dead RandomNamePlugin GMGen wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Never packaged into any *plugins.jar, no callers — unreachable since GMGen was retired. The live name generator path (RandomNameDialog → NameGenPanel) is unaffected. --- .../plugin/doomsdaybook/RandomNamePlugin.java | 144 ------------------ code/src/java/plugin/doomsdaybook/manifest.mf | 4 - 2 files changed, 148 deletions(-) delete mode 100644 code/src/java/plugin/doomsdaybook/RandomNamePlugin.java delete mode 100644 code/src/java/plugin/doomsdaybook/manifest.mf diff --git a/code/src/java/plugin/doomsdaybook/RandomNamePlugin.java b/code/src/java/plugin/doomsdaybook/RandomNamePlugin.java deleted file mode 100644 index 28ccdec0fe8..00000000000 --- a/code/src/java/plugin/doomsdaybook/RandomNamePlugin.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2003 (C) Devon Jones - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ -package plugin.doomsdaybook; - -import java.awt.Component; -import java.io.File; - -import javax.swing.JMenuItem; -import javax.swing.JOptionPane; -import javax.swing.JTabbedPane; - -import pcgen.core.SettingsHandler; -import pcgen.gui2.doomsdaybook.NameGenPanel; -import pcgen.gui2.tools.Utility; -import pcgen.pluginmgr.InteractivePlugin; -import pcgen.pluginmgr.PCGenMessage; -import pcgen.pluginmgr.PCGenMessageHandler; -import pcgen.pluginmgr.messages.FocusOrStateChangeOccurredMessage; -import pcgen.system.LanguageBundle; - -public class RandomNamePlugin implements InteractivePlugin -{ - /** Log name */ - private static final String LOG_NAME = "Random_Name_Generator"; - - /** The plugin menu item in the tools menu. */ - private final JMenuItem nameToolsItem = new JMenuItem(); - - /** The user interface that this class will be using. */ - private NameGenPanel theView; - - /** The English name of the plugin. */ - private static final String NAME = "Random Names"; //$NON-NLS-1$ - /** Key of plugin tab. */ - private static final String IN_NAME = "in_plugin_randomname_name"; //$NON-NLS-1$ - /** Mnemonic in menu for {@link #IN_NAME} */ - private static final String IN_NAME_MN = "in_mn_plugin_randomname_name"; //$NON-NLS-1$ - - /** - * Starts the plugin, registering itself with the {@code TabAddMessage}. - */ - @Override - public void start(PCGenMessageHandler mh) - { - theView = new NameGenPanel(getDataDirectory()); - initMenus(); - } - - @Override - public void stop() - { - } - - @Override - public int getPriority() - { - return SettingsHandler.getGMGenOption(RandomNamePlugin.LOG_NAME + ".LoadOrder", 80); - } - - /** - * Accessor for name - * @return name - */ - @Override - public String getPluginName() - { - return RandomNamePlugin.NAME; - } - - private static String getLocalizedName() - { - return LanguageBundle.getString(RandomNamePlugin.IN_NAME); - } - - /** - * Gets the view that this class is using. - * @return the view. - */ - public Component getView() - { - return theView; - } - - /** - * listens to messages from the GMGen system, and handles them as needed - * @param message the source of the event from the system - */ - @Override - public void handleMessage(PCGenMessage message) - { - if (message instanceof FocusOrStateChangeOccurredMessage) - { - if (isActive()) - { - nameToolsItem.setEnabled(false); - } - else - { - nameToolsItem.setEnabled(true); - } - } - } - - /** - * Returns true if this plugin is active - * @return true if this plugin is active - */ - public boolean isActive() - { - JTabbedPane tp = Utility.getTabbedPaneFor(theView); - return (tp != null) && JOptionPane.getFrameForComponent(tp).isFocused() - && tp.getSelectedComponent().equals(theView); - } - - /** - * Initialize the menus - */ - private void initMenus() - { - nameToolsItem.setMnemonic(LanguageBundle.getMnemonic(RandomNamePlugin.IN_NAME_MN)); - nameToolsItem.setText(RandomNamePlugin.getLocalizedName()); - } - - @Override - public File getDataDirectory() - { - return new File(SettingsHandler.getGmgenPluginDir(), RandomNamePlugin.NAME); - } -} diff --git a/code/src/java/plugin/doomsdaybook/manifest.mf b/code/src/java/plugin/doomsdaybook/manifest.mf deleted file mode 100644 index 24bce91775f..00000000000 --- a/code/src/java/plugin/doomsdaybook/manifest.mf +++ /dev/null @@ -1,4 +0,0 @@ -Manifest-Version: 1.2 -Class-Path: lib/jdom.jar lib/jaxp.jar -Created-By: NetBeans IDE - From 6f8e9766416d082d81ab1bd716ddc297ab8062f6 Mon Sep 17 00:00:00 2001 From: Vest Date: Wed, 20 May 2026 22:13:25 +0200 Subject: [PATCH 02/34] Rename engine package pcgen.core.doomsdaybook to pcgen.core.namegen The 'doomsdaybook' name is a 2003 artifact of the third-party product PCGen adopted the engine from. Rename clarifies the package's purpose. --- .../{doomsdaybook => namegen}/CRRule.java | 2 +- .../{doomsdaybook => namegen}/DDList.java | 2 +- .../DataElement.java | 2 +- .../DataElementComperator.java | 2 +- .../DataSubValue.java | 2 +- .../{doomsdaybook => namegen}/DataValue.java | 2 +- .../{doomsdaybook => namegen}/HyphenRule.java | 2 +- .../{doomsdaybook => namegen}/Operation.java | 2 +- .../core/{doomsdaybook => namegen}/Rule.java | 2 +- .../{doomsdaybook => namegen}/RuleSet.java | 2 +- .../{doomsdaybook => namegen}/SpaceRule.java | 2 +- .../VariableHashMap.java | 2 +- .../WeightedDataValue.java | 2 +- .../variableException.java | 2 +- .../pcgen/gui2/doomsdaybook/NameButton.java | 2 +- .../pcgen/gui2/doomsdaybook/NameGenPanel.java | 22 +++++++++---------- 16 files changed, 26 insertions(+), 26 deletions(-) rename code/src/java/pcgen/core/{doomsdaybook => namegen}/CRRule.java (97%) rename code/src/java/pcgen/core/{doomsdaybook => namegen}/DDList.java (98%) rename code/src/java/pcgen/core/{doomsdaybook => namegen}/DataElement.java (97%) rename code/src/java/pcgen/core/{doomsdaybook => namegen}/DataElementComperator.java (97%) rename code/src/java/pcgen/core/{doomsdaybook => namegen}/DataSubValue.java (97%) rename code/src/java/pcgen/core/{doomsdaybook => namegen}/DataValue.java (97%) rename code/src/java/pcgen/core/{doomsdaybook => namegen}/HyphenRule.java (97%) rename code/src/java/pcgen/core/{doomsdaybook => namegen}/Operation.java (98%) rename code/src/java/pcgen/core/{doomsdaybook => namegen}/Rule.java (98%) rename code/src/java/pcgen/core/{doomsdaybook => namegen}/RuleSet.java (99%) rename code/src/java/pcgen/core/{doomsdaybook => namegen}/SpaceRule.java (97%) rename code/src/java/pcgen/core/{doomsdaybook => namegen}/VariableHashMap.java (99%) rename code/src/java/pcgen/core/{doomsdaybook => namegen}/WeightedDataValue.java (97%) rename code/src/java/pcgen/core/{doomsdaybook => namegen}/variableException.java (97%) diff --git a/code/src/java/pcgen/core/doomsdaybook/CRRule.java b/code/src/java/pcgen/core/namegen/CRRule.java similarity index 97% rename from code/src/java/pcgen/core/doomsdaybook/CRRule.java rename to code/src/java/pcgen/core/namegen/CRRule.java index ef5962c53df..6816fe7425d 100644 --- a/code/src/java/pcgen/core/doomsdaybook/CRRule.java +++ b/code/src/java/pcgen/core/namegen/CRRule.java @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.core.doomsdaybook; +package pcgen.core.namegen; import java.util.ArrayList; import java.util.List; diff --git a/code/src/java/pcgen/core/doomsdaybook/DDList.java b/code/src/java/pcgen/core/namegen/DDList.java similarity index 98% rename from code/src/java/pcgen/core/doomsdaybook/DDList.java rename to code/src/java/pcgen/core/namegen/DDList.java index 5405cf24622..95ca3edd9a2 100644 --- a/code/src/java/pcgen/core/doomsdaybook/DDList.java +++ b/code/src/java/pcgen/core/namegen/DDList.java @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.core.doomsdaybook; +package pcgen.core.namegen; import java.util.ArrayList; import java.util.List; diff --git a/code/src/java/pcgen/core/doomsdaybook/DataElement.java b/code/src/java/pcgen/core/namegen/DataElement.java similarity index 97% rename from code/src/java/pcgen/core/doomsdaybook/DataElement.java rename to code/src/java/pcgen/core/namegen/DataElement.java index 822fe97ede0..2d8380dfef7 100644 --- a/code/src/java/pcgen/core/doomsdaybook/DataElement.java +++ b/code/src/java/pcgen/core/namegen/DataElement.java @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.core.doomsdaybook; +package pcgen.core.namegen; import java.util.List; diff --git a/code/src/java/pcgen/core/doomsdaybook/DataElementComperator.java b/code/src/java/pcgen/core/namegen/DataElementComperator.java similarity index 97% rename from code/src/java/pcgen/core/doomsdaybook/DataElementComperator.java rename to code/src/java/pcgen/core/namegen/DataElementComperator.java index 530dab15d1e..284e25e0a90 100644 --- a/code/src/java/pcgen/core/doomsdaybook/DataElementComperator.java +++ b/code/src/java/pcgen/core/namegen/DataElementComperator.java @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.core.doomsdaybook; +package pcgen.core.namegen; import java.io.Serializable; import java.util.Comparator; diff --git a/code/src/java/pcgen/core/doomsdaybook/DataSubValue.java b/code/src/java/pcgen/core/namegen/DataSubValue.java similarity index 97% rename from code/src/java/pcgen/core/doomsdaybook/DataSubValue.java rename to code/src/java/pcgen/core/namegen/DataSubValue.java index 088c41dc29b..8db523e76a2 100644 --- a/code/src/java/pcgen/core/doomsdaybook/DataSubValue.java +++ b/code/src/java/pcgen/core/namegen/DataSubValue.java @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.core.doomsdaybook; +package pcgen.core.namegen; /** * DataSubValue diff --git a/code/src/java/pcgen/core/doomsdaybook/DataValue.java b/code/src/java/pcgen/core/namegen/DataValue.java similarity index 97% rename from code/src/java/pcgen/core/doomsdaybook/DataValue.java rename to code/src/java/pcgen/core/namegen/DataValue.java index 3a7f1ecd540..1bc075f8bb0 100644 --- a/code/src/java/pcgen/core/doomsdaybook/DataValue.java +++ b/code/src/java/pcgen/core/namegen/DataValue.java @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.core.doomsdaybook; +package pcgen.core.namegen; public class DataValue { diff --git a/code/src/java/pcgen/core/doomsdaybook/HyphenRule.java b/code/src/java/pcgen/core/namegen/HyphenRule.java similarity index 97% rename from code/src/java/pcgen/core/doomsdaybook/HyphenRule.java rename to code/src/java/pcgen/core/namegen/HyphenRule.java index 4953ca3a2a5..9fa1da4bcf4 100644 --- a/code/src/java/pcgen/core/doomsdaybook/HyphenRule.java +++ b/code/src/java/pcgen/core/namegen/HyphenRule.java @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.core.doomsdaybook; +package pcgen.core.namegen; import java.util.ArrayList; import java.util.List; diff --git a/code/src/java/pcgen/core/doomsdaybook/Operation.java b/code/src/java/pcgen/core/namegen/Operation.java similarity index 98% rename from code/src/java/pcgen/core/doomsdaybook/Operation.java rename to code/src/java/pcgen/core/namegen/Operation.java index 97d10c9f922..ab7b84f2537 100644 --- a/code/src/java/pcgen/core/doomsdaybook/Operation.java +++ b/code/src/java/pcgen/core/namegen/Operation.java @@ -16,7 +16,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.core.doomsdaybook; +package pcgen.core.namegen; /** * {@code Operation} encapsulates an action that can be performed diff --git a/code/src/java/pcgen/core/doomsdaybook/Rule.java b/code/src/java/pcgen/core/namegen/Rule.java similarity index 98% rename from code/src/java/pcgen/core/doomsdaybook/Rule.java rename to code/src/java/pcgen/core/namegen/Rule.java index 44a14f2a3a5..7976ffe3d2e 100644 --- a/code/src/java/pcgen/core/doomsdaybook/Rule.java +++ b/code/src/java/pcgen/core/namegen/Rule.java @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.core.doomsdaybook; +package pcgen.core.namegen; import java.util.ArrayList; import java.util.List; diff --git a/code/src/java/pcgen/core/doomsdaybook/RuleSet.java b/code/src/java/pcgen/core/namegen/RuleSet.java similarity index 99% rename from code/src/java/pcgen/core/doomsdaybook/RuleSet.java rename to code/src/java/pcgen/core/namegen/RuleSet.java index 4da6d2583ce..bb50fb1f274 100644 --- a/code/src/java/pcgen/core/doomsdaybook/RuleSet.java +++ b/code/src/java/pcgen/core/namegen/RuleSet.java @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.core.doomsdaybook; +package pcgen.core.namegen; import java.util.ArrayList; import java.util.List; diff --git a/code/src/java/pcgen/core/doomsdaybook/SpaceRule.java b/code/src/java/pcgen/core/namegen/SpaceRule.java similarity index 97% rename from code/src/java/pcgen/core/doomsdaybook/SpaceRule.java rename to code/src/java/pcgen/core/namegen/SpaceRule.java index de286950017..e8971cad7ac 100644 --- a/code/src/java/pcgen/core/doomsdaybook/SpaceRule.java +++ b/code/src/java/pcgen/core/namegen/SpaceRule.java @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.core.doomsdaybook; +package pcgen.core.namegen; import java.util.ArrayList; import java.util.List; diff --git a/code/src/java/pcgen/core/doomsdaybook/VariableHashMap.java b/code/src/java/pcgen/core/namegen/VariableHashMap.java similarity index 99% rename from code/src/java/pcgen/core/doomsdaybook/VariableHashMap.java rename to code/src/java/pcgen/core/namegen/VariableHashMap.java index 64664879e97..0d2719ed880 100644 --- a/code/src/java/pcgen/core/doomsdaybook/VariableHashMap.java +++ b/code/src/java/pcgen/core/namegen/VariableHashMap.java @@ -16,7 +16,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.core.doomsdaybook; +package pcgen.core.namegen; import java.util.ArrayList; import java.util.Collection; diff --git a/code/src/java/pcgen/core/doomsdaybook/WeightedDataValue.java b/code/src/java/pcgen/core/namegen/WeightedDataValue.java similarity index 97% rename from code/src/java/pcgen/core/doomsdaybook/WeightedDataValue.java rename to code/src/java/pcgen/core/namegen/WeightedDataValue.java index ef0704a4150..9863b562871 100644 --- a/code/src/java/pcgen/core/doomsdaybook/WeightedDataValue.java +++ b/code/src/java/pcgen/core/namegen/WeightedDataValue.java @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.core.doomsdaybook; +package pcgen.core.namegen; /** * {@code WeightedDataValue}. diff --git a/code/src/java/pcgen/core/doomsdaybook/variableException.java b/code/src/java/pcgen/core/namegen/variableException.java similarity index 97% rename from code/src/java/pcgen/core/doomsdaybook/variableException.java rename to code/src/java/pcgen/core/namegen/variableException.java index bdbe7fbc0be..a7d3a6d28b5 100644 --- a/code/src/java/pcgen/core/doomsdaybook/variableException.java +++ b/code/src/java/pcgen/core/namegen/variableException.java @@ -16,7 +16,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.core.doomsdaybook; +package pcgen.core.namegen; /** * {@code variableException} is an exception raised when a problem diff --git a/code/src/java/pcgen/gui2/doomsdaybook/NameButton.java b/code/src/java/pcgen/gui2/doomsdaybook/NameButton.java index 292e852bc14..55e54a3e207 100644 --- a/code/src/java/pcgen/gui2/doomsdaybook/NameButton.java +++ b/code/src/java/pcgen/gui2/doomsdaybook/NameButton.java @@ -17,7 +17,7 @@ */ package pcgen.gui2.doomsdaybook; -import pcgen.core.doomsdaybook.DataElement; +import pcgen.core.namegen.DataElement; class NameButton extends javax.swing.JButton { diff --git a/code/src/java/pcgen/gui2/doomsdaybook/NameGenPanel.java b/code/src/java/pcgen/gui2/doomsdaybook/NameGenPanel.java index e5a2b6d1c27..d0036035ea8 100644 --- a/code/src/java/pcgen/gui2/doomsdaybook/NameGenPanel.java +++ b/code/src/java/pcgen/gui2/doomsdaybook/NameGenPanel.java @@ -49,16 +49,16 @@ import javax.swing.JSeparator; import javax.swing.JTextField; -import pcgen.core.doomsdaybook.CRRule; -import pcgen.core.doomsdaybook.DataElement; -import pcgen.core.doomsdaybook.DataElementComperator; -import pcgen.core.doomsdaybook.DataValue; -import pcgen.core.doomsdaybook.HyphenRule; -import pcgen.core.doomsdaybook.Rule; -import pcgen.core.doomsdaybook.RuleSet; -import pcgen.core.doomsdaybook.SpaceRule; -import pcgen.core.doomsdaybook.VariableHashMap; -import pcgen.core.doomsdaybook.WeightedDataValue; +import pcgen.core.namegen.CRRule; +import pcgen.core.namegen.DataElement; +import pcgen.core.namegen.DataElementComperator; +import pcgen.core.namegen.DataValue; +import pcgen.core.namegen.HyphenRule; +import pcgen.core.namegen.Rule; +import pcgen.core.namegen.RuleSet; +import pcgen.core.namegen.SpaceRule; +import pcgen.core.namegen.VariableHashMap; +import pcgen.core.namegen.WeightedDataValue; import pcgen.gui2.tools.Icons; import pcgen.gui2.util.FontManipulation; import pcgen.system.LanguageBundle; @@ -798,7 +798,7 @@ private void loadFromDocument(Document nameSet) throws DataConversionException private String loadList(Element list) throws DataConversionException { - pcgen.core.doomsdaybook.DDList dataList = new pcgen.core.doomsdaybook.DDList(allVars, + pcgen.core.namegen.DDList dataList = new pcgen.core.namegen.DDList(allVars, list.getAttributeValue("title"), list.getAttributeValue("id")); java.util.List elements = list.getChildren(); From 5dabbcd14839dcc4e368c37c20ee5373d24661b6 Mon Sep 17 00:00:00 2001 From: Vest Date: Wed, 20 May 2026 22:13:56 +0200 Subject: [PATCH 03/34] Rename UI package pcgen.gui2.doomsdaybook to pcgen.gui2.namegen Mirrors the engine package rename. NameGenPanel, NameButton, and XMLFilter move; RandomNameDialog updates its single import. --- code/src/java/pcgen/gui2/dialog/RandomNameDialog.java | 2 +- .../java/pcgen/gui2/{doomsdaybook => namegen}/NameButton.java | 2 +- .../java/pcgen/gui2/{doomsdaybook => namegen}/NameGenPanel.java | 2 +- .../java/pcgen/gui2/{doomsdaybook => namegen}/XMLFilter.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename code/src/java/pcgen/gui2/{doomsdaybook => namegen}/NameButton.java (97%) rename code/src/java/pcgen/gui2/{doomsdaybook => namegen}/NameGenPanel.java (99%) rename code/src/java/pcgen/gui2/{doomsdaybook => namegen}/XMLFilter.java (97%) diff --git a/code/src/java/pcgen/gui2/dialog/RandomNameDialog.java b/code/src/java/pcgen/gui2/dialog/RandomNameDialog.java index 205d0b05538..7dbfb1483c2 100644 --- a/code/src/java/pcgen/gui2/dialog/RandomNameDialog.java +++ b/code/src/java/pcgen/gui2/dialog/RandomNameDialog.java @@ -25,7 +25,7 @@ import javax.swing.WindowConstants; import pcgen.core.SettingsHandler; -import pcgen.gui2.doomsdaybook.NameGenPanel; +import pcgen.gui2.namegen.NameGenPanel; import pcgen.gui2.tools.Utility; import pcgen.gui3.GuiUtility; import pcgen.gui3.component.OKCloseButtonBar; diff --git a/code/src/java/pcgen/gui2/doomsdaybook/NameButton.java b/code/src/java/pcgen/gui2/namegen/NameButton.java similarity index 97% rename from code/src/java/pcgen/gui2/doomsdaybook/NameButton.java rename to code/src/java/pcgen/gui2/namegen/NameButton.java index 55e54a3e207..95b60d22b85 100644 --- a/code/src/java/pcgen/gui2/doomsdaybook/NameButton.java +++ b/code/src/java/pcgen/gui2/namegen/NameButton.java @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.gui2.doomsdaybook; +package pcgen.gui2.namegen; import pcgen.core.namegen.DataElement; diff --git a/code/src/java/pcgen/gui2/doomsdaybook/NameGenPanel.java b/code/src/java/pcgen/gui2/namegen/NameGenPanel.java similarity index 99% rename from code/src/java/pcgen/gui2/doomsdaybook/NameGenPanel.java rename to code/src/java/pcgen/gui2/namegen/NameGenPanel.java index d0036035ea8..45d7f1b074b 100644 --- a/code/src/java/pcgen/gui2/doomsdaybook/NameGenPanel.java +++ b/code/src/java/pcgen/gui2/namegen/NameGenPanel.java @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.gui2.doomsdaybook; +package pcgen.gui2.namegen; import java.awt.BorderLayout; import java.awt.FlowLayout; diff --git a/code/src/java/pcgen/gui2/doomsdaybook/XMLFilter.java b/code/src/java/pcgen/gui2/namegen/XMLFilter.java similarity index 97% rename from code/src/java/pcgen/gui2/doomsdaybook/XMLFilter.java rename to code/src/java/pcgen/gui2/namegen/XMLFilter.java index ed26642906e..89cb7fe3675 100644 --- a/code/src/java/pcgen/gui2/doomsdaybook/XMLFilter.java +++ b/code/src/java/pcgen/gui2/namegen/XMLFilter.java @@ -15,7 +15,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -package pcgen.gui2.doomsdaybook; +package pcgen.gui2.namegen; import java.io.File; From 89bef7090d56c75b32b5aba262ba3bcdb826381a Mon Sep 17 00:00:00 2001 From: Vest Date: Wed, 20 May 2026 23:17:39 +0200 Subject: [PATCH 04/34] Extract headless NameGenerator from Swing panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pull XML loading, catalog/category/gender selection, and rule invocation out of pcgen.gui2.namegen.NameGenPanel into a new UI-toolkit-independent facade in pcgen.core.namegen so the engine can be reused from JavaFX, a CLI, or tests without a Swing runtime. NameGenerator exposes getCategories / getTitlesFor / getGendersFor / getCatalog / generate / generateWithRule / getRulesFor. NameGenDataLoader handles the directory scan and per-file XML parse (still JDOM2 — translation to javax.xml.parsers comes next). NameGenData is the immutable snapshot; GeneratedName carries the assembled name plus meaning and pronunciation. NameGenPanel now constructs NameGenerator and delegates; ~370 lines of loading/generation logic move out of the UI class. User-visible behavior unchanged. --- .../pcgen/core/namegen/GeneratedName.java | 26 + .../java/pcgen/core/namegen/NameGenData.java | 28 + .../pcgen/core/namegen/NameGenDataLoader.java | 231 ++++++++ .../pcgen/core/namegen/NameGenerator.java | 185 +++++++ .../java/pcgen/gui2/namegen/NameGenPanel.java | 522 +++--------------- 5 files changed, 545 insertions(+), 447 deletions(-) create mode 100644 code/src/java/pcgen/core/namegen/GeneratedName.java create mode 100644 code/src/java/pcgen/core/namegen/NameGenData.java create mode 100644 code/src/java/pcgen/core/namegen/NameGenDataLoader.java create mode 100644 code/src/java/pcgen/core/namegen/NameGenerator.java diff --git a/code/src/java/pcgen/core/namegen/GeneratedName.java b/code/src/java/pcgen/core/namegen/GeneratedName.java new file mode 100644 index 00000000000..45ee09c04e2 --- /dev/null +++ b/code/src/java/pcgen/core/namegen/GeneratedName.java @@ -0,0 +1,26 @@ +/* + * Copyright 2026 Vest + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package pcgen.core.namegen; + +import java.util.List; + +/** + * Result of a single name generation: the assembled name plus the meaning + * and pronunciation strings produced from the same data, and the {@link Rule} + * that was used (so callers can re-trigger or inspect the structure). + */ +public record GeneratedName(String name, String meaning, String pronunciation, + Rule rule, List parts) +{ +} diff --git a/code/src/java/pcgen/core/namegen/NameGenData.java b/code/src/java/pcgen/core/namegen/NameGenData.java new file mode 100644 index 00000000000..67aa7fa01da --- /dev/null +++ b/code/src/java/pcgen/core/namegen/NameGenData.java @@ -0,0 +1,28 @@ +/* + * Copyright 2026 Vest + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package pcgen.core.namegen; + +import java.util.List; +import java.util.Map; + +/** + * Immutable container for the data loaded from a directory of random-name + * XML files. {@link #allVars} holds every {@link DataElement} keyed by id + * (lists, rules, rulesets, separators); {@link #categories} is the + * category-name -> ruleset list index used to drive UI pickers. + */ +public record NameGenData(VariableHashMap allVars, + Map> categories) +{ +} diff --git a/code/src/java/pcgen/core/namegen/NameGenDataLoader.java b/code/src/java/pcgen/core/namegen/NameGenDataLoader.java new file mode 100644 index 00000000000..2c90e3daab0 --- /dev/null +++ b/code/src/java/pcgen/core/namegen/NameGenDataLoader.java @@ -0,0 +1,231 @@ +/* + * Copyright 2003 (C) Devon Jones + * Copyright 2026 Vest + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package pcgen.core.namegen; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +import org.jdom2.DataConversionException; +import org.jdom2.DocType; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.input.SAXBuilder; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; + +import pcgen.util.Logging; + +/** + * Loads random-name XML files from a directory into a {@link NameGenData} + * snapshot. The legacy loading code lived inline in + * {@code pcgen.gui2.namegen.NameGenPanel}; pulling it here decouples the + * data model from any UI toolkit. + * + *

This class still uses JDOM2; a follow-up commit replaces it with the + * built-in {@code javax.xml.parsers} API. + */ +public final class NameGenDataLoader +{ + private NameGenDataLoader() + { + } + + /** + * Load every {@code *.xml} file in the given directory. + * + * @param dataDir directory containing {@code generator.dtd} and the XML + * files to parse + * @return populated {@link NameGenData} + * @throws IOException if {@code dataDir} is not a directory + */ + public static NameGenData load(File dataDir) throws IOException + { + if (!dataDir.isDirectory()) + { + throw new IOException("Not a directory: " + dataDir); + } + File[] dataFiles = dataDir.listFiles((dir, name) -> name.endsWith(".xml")); + if (dataFiles == null) + { + throw new IOException("Cannot list files in: " + dataDir); + } + VariableHashMap allVars = new VariableHashMap(); + Map> categories = new HashMap<>(); + SAXBuilder builder = new SAXBuilder(); + builder.setEntityResolver(new GeneratorDtdResolver(dataDir)); + + for (File dataFile : dataFiles) + { + try + { + URL url = dataFile.toURI().toURL(); + Document nameSet = builder.build(url); + DocType dt = nameSet.getDocType(); + if (dt != null && "GENERATOR".equals(dt.getElementName())) + { + loadFromDocument(nameSet, allVars, categories); + } + } + catch (JDOMException | IOException e) + { + Logging.errorPrint("Failed to parse " + dataFile.getName(), e); + throw new IOException("XML error in file " + dataFile.getName(), e); + } + } + return new NameGenData(allVars, categories); + } + + private static void loadFromDocument(Document nameSet, VariableHashMap allVars, + Map> categories) throws DataConversionException + { + Element generator = nameSet.getRootElement(); + List rulesets = generator.getChildren("RULESET"); + List lists = generator.getChildren("LIST"); + + for (final Object o : lists) + { + loadList((Element) o, allVars); + } + for (final Object ruleset : rulesets) + { + RuleSet rs = loadRuleSet((Element) ruleset, allVars, categories); + allVars.addDataElement(rs); + } + } + + private static String loadList(Element list, VariableHashMap allVars) throws DataConversionException + { + DDList dataList = new DDList(allVars, + list.getAttributeValue("title"), list.getAttributeValue("id")); + for (Element child : list.getChildren("VALUE")) + { + WeightedDataValue dv = new WeightedDataValue(child.getText(), + child.getAttribute("weight").getIntValue()); + child.getChildren("SUBVALUE").forEach(sub -> + dv.addSubValue(sub.getAttributeValue("type"), sub.getText())); + dataList.add(dv); + } + allVars.addDataElement(dataList); + return dataList.getId(); + } + + private static String loadRule(Element rule, String id, VariableHashMap allVars) throws DataConversionException + { + Rule dataRule = new Rule(allVars, id, id, rule.getAttribute("weight").getIntValue()); + List elements = rule.getChildren(); + for (final Object element : elements) + { + Element child = (Element) element; + switch (child.getName()) + { + case "GETLIST" -> dataRule.add(child.getAttributeValue("idref")); + case "SPACE" -> { + SpaceRule sp = new SpaceRule(); + allVars.addDataElement(sp); + dataRule.add(sp.getId()); + } + case "HYPHEN" -> { + HyphenRule hy = new HyphenRule(); + allVars.addDataElement(hy); + dataRule.add(hy.getId()); + } + case "CR" -> { + CRRule cr = new CRRule(); + allVars.addDataElement(cr); + dataRule.add(cr.getId()); + } + case "GETRULE" -> dataRule.add(child.getAttributeValue("idref")); + default -> { /* ignore */ } + } + } + allVars.addDataElement(dataRule); + return dataRule.getId(); + } + + private static RuleSet loadRuleSet(Element ruleSet, VariableHashMap allVars, + Map> categories) throws DataConversionException + { + RuleSet rs = new RuleSet(allVars, ruleSet.getAttributeValue("title"), + ruleSet.getAttributeValue("id"), ruleSet.getAttributeValue("usage")); + List elements = ruleSet.getChildren(); + ListIterator it = elements.listIterator(); + int num = 0; + while (it.hasNext()) + { + Element child = (Element) it.next(); + String elementName = child.getName(); + if ("CATEGORY".equals(elementName)) + { + loadCategory(child, rs, categories); + } + else if ("RULE".equals(elementName)) + { + rs.add(loadRule(child, rs.getId() + num, allVars)); + } + num++; + } + return rs; + } + + private static void loadCategory(Element category, RuleSet rs, Map> categories) + { + String key = category.getAttributeValue("title"); + categories.computeIfAbsent(key, k -> new ArrayList<>()).add(rs); + } + + /** + * Resolves {@code generator.dtd} from a known directory rather than the + * network. Carried over verbatim from the old Swing panel. + */ + static final class GeneratorDtdResolver implements EntityResolver + { + private final File parent; + + GeneratorDtdResolver(File parent) + { + this.parent = parent; + } + + @Override + public InputSource resolveEntity(String publicId, String systemId) + { + if (systemId.endsWith("generator.dtd")) + { + InputStream dtdIn; + try + { + dtdIn = new FileInputStream(new File(parent, "generator.dtd")); + } + catch (FileNotFoundException e) + { + Logging.errorPrint("GeneratorDtdResolver.resolveEntity failed", e); + return null; + } + return new InputSource(dtdIn); + } + return null; + } + } +} diff --git a/code/src/java/pcgen/core/namegen/NameGenerator.java b/code/src/java/pcgen/core/namegen/NameGenerator.java new file mode 100644 index 00000000000..d3f1034c832 --- /dev/null +++ b/code/src/java/pcgen/core/namegen/NameGenerator.java @@ -0,0 +1,185 @@ +/* + * Copyright 2026 Vest + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package pcgen.core.namegen; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +/** + * UI-toolkit-independent facade for the random-name engine. Loads XML data + * once at construction and exposes operations a UI (Swing, JavaFX, CLI) + * needs to drive a category/title/gender picker and produce names. + * + *

Categories on disk are keyed by display name; gender categories use + * the {@code "Sex: Female"} / {@code "Sex: Male"} / {@code "Sex: Other"} + * convention. This facade hides that representation: callers see + * {@link #getCategories()} (no {@code Sex:} entries, no {@code All}) and + * a separate {@link #getGendersFor(String, String)} method. + */ +public final class NameGenerator +{ + private static final String SEX_PREFIX = "Sex:"; + private static final String ALL_CATEGORY = "All"; + private static final String FINAL_USAGE = "final"; + + private final NameGenData data; + + public NameGenerator(File dataDir) throws IOException + { + this.data = NameGenDataLoader.load(dataDir); + } + + /** + * Sorted display-only categories (no {@code Sex:} entries, no + * {@code All}). Suitable for direct use as a combo-box model. + */ + public List getCategories() + { + return data.categories().keySet().stream() + .filter(key -> !key.startsWith(SEX_PREFIX)) + .filter(key -> !ALL_CATEGORY.equals(key)) + .sorted() + .toList(); + } + + /** + * Sorted titles available within {@code category} (final-usage rulesets + * only). A title appearing under multiple categories will be returned + * for each of them. + */ + public List getTitlesFor(String category) + { + return data.categories().getOrDefault(category, List.of()).stream() + .filter(rs -> FINAL_USAGE.equals(rs.getUsage())) + .map(RuleSet::getTitle) + .distinct() + .sorted() + .toList(); + } + + /** + * Genders available for a given (category, title). Returned in the + * {@code [Female, Male, Other]} canonical order; entries missing from + * the data are simply absent. + */ + public List getGendersFor(String category, String title) + { + Set raw = data.categories().getOrDefault(category, List.of()).stream() + .filter(rs -> FINAL_USAGE.equals(rs.getUsage())) + .filter(rs -> rs.getTitle().equals(title)) + .flatMap(rs -> data.categories().entrySet().stream() + .filter(e -> e.getKey().startsWith(SEX_PREFIX) && e.getValue().contains(rs)) + .map(e -> e.getKey().substring(SEX_PREFIX.length()).trim())) + .collect(java.util.stream.Collectors.toSet()); + + // canonical order first, then any non-canonical genders (defensive) + Set ordered = new LinkedHashSet<>(); + Stream.of("Female", "Male", "Other").filter(raw::contains).forEach(ordered::add); + ordered.addAll(raw); + return List.copyOf(ordered); + } + + /** + * Resolve a (category, title, gender) triple to a final-usage ruleset. + * + * @return matching {@link RuleSet}, or {@code null} if no entry matches + */ + public RuleSet getCatalog(String category, String title, String gender) + { + List genderRules = data.categories().getOrDefault(SEX_PREFIX + " " + gender, List.of()); + return data.categories().getOrDefault(category, List.of()).stream() + .filter(rs -> FINAL_USAGE.equals(rs.getUsage())) + .filter(rs -> rs.getTitle().equals(title)) + .filter(genderRules::contains) + .findFirst() + .orElse(null); + } + + /** + * Pick a {@link Rule} from {@code catalog} by weighted random and + * generate a name from it. + */ + public GeneratedName generate(RuleSet catalog) throws Exception + { + Rule rule = catalog.getRule(); + List parts = rule.getData(); + return assemble(rule, parts); + } + + /** + * Generate a name forcing a specific {@link Rule} (Structure override). + */ + public GeneratedName generateWithRule(Rule forcedRule) throws Exception + { + List parts = forcedRule.getData(); + return assemble(forcedRule, parts); + } + + /** + * Returns the {@link Rule} alternatives within {@code catalog}; used by + * the Advanced "Structure" picker. + */ + public List getRulesFor(RuleSet catalog) + { + List out = new ArrayList<>(); + for (String key : catalog) + { + try + { + DataElement el = data.allVars().getDataElement(key); + if (el instanceof Rule r) + { + out.add(r); + } + } + catch (Exception ignored) + { + // missing references are skipped — the legacy panel did the same + } + } + return out; + } + + /** + * Exposes the loaded data, primarily for tests and the legacy panel + * during the transition. + */ + public NameGenData getData() + { + return data; + } + + private static GeneratedName assemble(Rule rule, List parts) + { + StringBuilder name = new StringBuilder(); + StringBuilder meaning = new StringBuilder(); + StringBuilder pron = new StringBuilder(); + parts.forEach(v -> { + name.append(v.getValue()); + + String m = v.getSubValue("meaning"); + meaning.append(m == null ? v.getValue() : m); + + String p = v.getSubValue("pronounciation"); + pron.append(p == null ? v.getValue() : p); + }); + return new GeneratedName(name.toString(), meaning.toString(), pron.toString(), + rule, List.copyOf(parts)); + } +} diff --git a/code/src/java/pcgen/gui2/namegen/NameGenPanel.java b/code/src/java/pcgen/gui2/namegen/NameGenPanel.java index 45d7f1b074b..9df545a6546 100644 --- a/code/src/java/pcgen/gui2/namegen/NameGenPanel.java +++ b/code/src/java/pcgen/gui2/namegen/NameGenPanel.java @@ -10,10 +10,6 @@ * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package pcgen.gui2.namegen; @@ -24,17 +20,10 @@ import java.awt.datatransfer.StringSelection; import java.awt.event.ActionEvent; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.net.URL; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Set; import java.util.Vector; import javax.swing.BoxLayout; @@ -49,36 +38,24 @@ import javax.swing.JSeparator; import javax.swing.JTextField; -import pcgen.core.namegen.CRRule; import pcgen.core.namegen.DataElement; import pcgen.core.namegen.DataElementComperator; -import pcgen.core.namegen.DataValue; -import pcgen.core.namegen.HyphenRule; +import pcgen.core.namegen.GeneratedName; +import pcgen.core.namegen.NameGenerator; import pcgen.core.namegen.Rule; import pcgen.core.namegen.RuleSet; -import pcgen.core.namegen.SpaceRule; -import pcgen.core.namegen.VariableHashMap; -import pcgen.core.namegen.WeightedDataValue; import pcgen.gui2.tools.Icons; import pcgen.gui2.util.FontManipulation; import pcgen.system.LanguageBundle; import pcgen.util.Logging; -import org.jdom2.DataConversionException; -import org.jdom2.DocType; -import org.jdom2.Document; -import org.jdom2.Element; -import org.jdom2.input.SAXBuilder; -import org.xml.sax.EntityResolver; -import org.xml.sax.InputSource; - /** - * Main panel of the random name generator. + * Main panel of the random name generator. Swing rendering only; all data + * loading and rule evaluation is delegated to {@link NameGenerator}. */ @SuppressWarnings({"UseOfObsoleteCollectionType", "PMD.UseArrayListInsteadOfVector"}) public class NameGenPanel extends JPanel { - private final Map> categories = new HashMap<>(); private JButton generateButton; private JButton jButton1; private JCheckBox chkStructure; @@ -115,9 +92,9 @@ public class NameGenPanel extends JPanel private JSeparator jSeparator3; private JSeparator jSeparator4; private JTextField name; - private final VariableHashMap allVars = new VariableHashMap(); - private Rule lastRule = null; + private NameGenerator nameGen; + private Rule lastRule; /** Creates new form NameGenPanel */ public NameGenPanel() @@ -127,13 +104,22 @@ public NameGenPanel() /** * Constructs a NameGenPanel given a dataPath - * + * * @param dataPath The path to the random name data files. */ public NameGenPanel(File dataPath) { initComponents(); - loadData(dataPath); + try + { + nameGen = new NameGenerator(dataPath); + loadDropdowns(); + } + catch (IOException e) + { + Logging.errorPrint(e.getMessage(), e); + JOptionPane.showMessageDialog(this, "Failed to load name data: " + e.getMessage()); + } } /** @@ -144,98 +130,33 @@ public Rule generate() { try { - Rule rule; - + RuleSet rs = (RuleSet) cbCatalog.getSelectedItem(); + if (rs == null) + { + return null; + } + GeneratedName result; if (chkStructure.isSelected()) { - RuleSet rs = (RuleSet) cbCatalog.getSelectedItem(); - rule = rs.getRule(); + result = nameGen.generate(rs); } else { - rule = (Rule) cbStructure.getSelectedItem(); + Rule rule = (Rule) cbStructure.getSelectedItem(); + result = nameGen.generateWithRule(rule); } - - List aName = rule.getData(); - setNameText(aName); - setMeaningText(aName); - setPronounciationText(aName); - - return rule; + name.setText(result.name()); + meaning.setText(result.meaning()); + pronounciation.setText(result.pronunciation()); + return result.rule(); } catch (Exception e) { Logging.errorPrint(e.getMessage(), e); - return null; } } - private void setMeaningText(String meaning) - { - this.meaning.setText(meaning); - } - - private void setMeaningText(Iterable data) - { - StringBuilder meaningBuffer = new StringBuilder(); - - for (DataValue val : data) - { - String aMeaning = val.getSubValue("meaning"); //$NON-NLS-1$ // XML attribute no translation - - if (aMeaning == null) - { - aMeaning = val.getValue(); - } - - meaningBuffer.append(aMeaning); - } - - setMeaningText(meaningBuffer.toString()); - } - - private void setNameText(String name) - { - this.name.setText(name); - } - - private void setNameText(Iterable data) - { - StringBuilder nameBuffer = new StringBuilder(); - - for (DataValue val : data) - { - nameBuffer.append(val.getValue()); - } - - setNameText(nameBuffer.toString()); - } - - private void setPronounciationText(String pronounciation) - { - this.pronounciation.setText(pronounciation); - } - - private void setPronounciationText(Iterable data) - { - StringBuilder proBuffer = new StringBuilder(); - - for (DataValue val : data) - { - String aPronounciation = val.getSubValue("pronounciation"); - - if (aPronounciation == null) - { - aPronounciation = val.getValue(); - } - - proBuffer.append(aPronounciation); - } - - setPronounciationText(proBuffer.toString()); - } - private void NameButtonActionPerformed(ActionEvent evt) { try @@ -261,11 +182,22 @@ private void NameButtonActionPerformed(ActionEvent evt) this.lastRule = rule; } - List aName = rule.getLastData(); + List aName = rule.getLastData(); - setNameText(aName); - setMeaningText(aName); - setPronounciationText(aName); + StringBuilder n = new StringBuilder(); + StringBuilder m = new StringBuilder(); + StringBuilder p = new StringBuilder(); + for (pcgen.core.namegen.DataValue v : aName) + { + n.append(v.getValue()); + String mm = v.getSubValue("meaning"); + m.append(mm == null ? v.getValue() : mm); + String pp = v.getSubValue("pronounciation"); + p.append(pp == null ? v.getValue() : pp); + } + name.setText(n.toString()); + meaning.setText(m.toString()); + pronounciation.setText(p.toString()); } catch (Exception e) { @@ -274,46 +206,36 @@ private void NameButtonActionPerformed(ActionEvent evt) } private void cbCatalogActionPerformed(ActionEvent evt) - { //GEN-FIRST:event_cbCatalogActionPerformed + { loadStructureDD(); this.clearButtons(); } - //GEN-LAST:event_cbCatalogActionPerformed - private void cbStructureActionPerformed(ActionEvent evt) - { //GEN-FIRST:event_cbStructureActionPerformed + { this.clearButtons(); } - //GEN-LAST:event_cbStructureActionPerformed - private void cbCategoryActionPerformed(ActionEvent evt) - { //GEN-FIRST:event_cbCategoryActionPerformed + { this.loadGenderDD(); loadCatalogDD(); loadStructureDD(); this.clearButtons(); } - //GEN-LAST:event_cbCategoryActionPerformed - private void cbGenderActionPerformed(ActionEvent evt) - { //GEN-FIRST:event_cbGenderActionPerformed + { loadCatalogDD(); loadStructureDD(); this.clearButtons(); } - //GEN-LAST:event_cbGenderActionPerformed - private void chkStructureActionPerformed(ActionEvent evt) - { //GEN-FIRST:event_chkStructureActionPerformed + { loadStructureDD(); } - //GEN-LAST:event_chkStructureActionPerformed - private void clearButtons() { buttonPanel.removeAll(); @@ -328,7 +250,7 @@ private void displayButtons(Rule rule) { try { - DataElement ele = allVars.getDataElement(key); + DataElement ele = nameGen.getData().allVars().getDataElement(key); if (ele.getTitle() != null) { @@ -347,8 +269,7 @@ private void displayButtons(Rule rule) } private void generateButtonActionPerformed(ActionEvent evt) - { //GEN-FIRST:event_generateButtonActionPerformed - + { try { this.lastRule = generate(); @@ -360,8 +281,6 @@ private void generateButtonActionPerformed(ActionEvent evt) } } - //GEN-LAST:event_generateButtonActionPerformed - /** * This method is called from within the constructor to * initialize the form. @@ -558,17 +477,13 @@ private void initComponents() add(topPanel, BorderLayout.NORTH); } - //GEN-END:initComponents - private void jButton1ActionPerformed(ActionEvent evt) - { //GEN-FIRST:event_jButton1ActionPerformed + { Clipboard cb = getToolkit().getSystemClipboard(); StringSelection ss = new StringSelection(name.getText()); cb.setContents(ss, ss); } - //GEN-LAST:event_jButton1ActionPerformed - private void loadCatalogDD() { try @@ -583,8 +498,8 @@ private void loadCatalogDD() catalogKey = oldRS.getTitle(); } - List cats = categories.get(catKey); - List genders = categories.get("Sex: " + genderKey); + List cats = nameGen.getData().categories().getOrDefault(catKey, List.of()); + List genders = nameGen.getData().categories().getOrDefault("Sex: " + genderKey, List.of()); List join = new ArrayList<>(cats); join.retainAll(genders); join.sort(new DataElementComperator()); @@ -621,284 +536,49 @@ private void loadCatalogDD() } } - // Get a list of all the gender categories in the category map - private Vector getGenderCategoryNames() - { - Vector genders = new Vector<>(); - Set keySet = categories.keySet(); - - // Loop through the keys in the categories - for (final String key : keySet) - { - // if the key starts with "Sex" then save it - if (key.startsWith("Sex:")) - { - genders.add(key.substring(5)); - } - } - - // Return all the found gender types - return genders; - } - // Load the gender drop dowd private void loadGenderDD() { - List genders = getGenderCategoryNames(); - Vector selectable = new Vector<>(); String gender = (String) cbGender.getSelectedItem(); - - // Get the selected category name String category = (String) cbCategory.getSelectedItem(); - // Get the set of rules for selected category - List categoryRules = categories.get(category); - - // we need to determine if the selected category is supported by the - // available genders - // loop through the available genders - for (String genderString : genders) + // Build "available genders for this category" by scanning Sex: keys + List selectable = new ArrayList<>(); + List categoryRules = nameGen.getData().categories().getOrDefault(category, List.of()); + for (var entry : nameGen.getData().categories().entrySet()) { - // Get the list of rules for the current gender - List genderRules = categories.get("Sex: " + genderString); - - // now loop through all the rules from the selected category + if (!entry.getKey().startsWith("Sex:")) + { + continue; + } + List genderRules = entry.getValue(); for (RuleSet categoryRule : categoryRules) { - // if the category rule is in the list of gender rules - // add the current gender to the selectable gender list - // we can stop processing the list once we find a match if (genderRules.contains(categoryRule)) { - selectable.add(genderString); + selectable.add(entry.getKey().substring(5).trim()); break; } } } - - // Sort the genders Collections.sort(selectable); - // Create a new model for the combobox and set it - cbGender.setModel(new DefaultComboBoxModel<>(selectable)); + cbGender.setModel(new DefaultComboBoxModel<>(new Vector<>(selectable))); if (gender != null && selectable.contains(gender)) { cbGender.setSelectedItem(gender); } } - private void loadCategory(Element category, RuleSet rs) - { - List cat = categories.get(category.getAttributeValue("title")); - List thiscat; - - if (cat == null) - { - thiscat = new ArrayList<>(); - categories.put(category.getAttributeValue("title"), thiscat); - } - else - { - thiscat = cat; - } - - thiscat.add(rs); - } - - private void loadData(File path) - { - if (path.isDirectory()) - { - File[] dataFiles = path.listFiles(new XMLFilter()); - SAXBuilder builder = new SAXBuilder(); - GeneratorDtdResolver resolver = new GeneratorDtdResolver(path); - builder.setEntityResolver(resolver); - - for (File dataFile : dataFiles) - { - try - { - URL url = dataFile.toURI().toURL(); - Document nameSet = builder.build(url); - DocType dt = nameSet.getDocType(); - - if (dt.getElementName().equals("GENERATOR")) - { - loadFromDocument(nameSet); - } - } - catch (Exception e) - { - Logging.errorPrint(e.getMessage(), e); - JOptionPane.showMessageDialog(this, "XML Error with file " + dataFile.getName()); - } - } - - loadDropdowns(); - } - else - { - JOptionPane.showMessageDialog(this, "No data files in directory " + path.getPath()); - } - } - - // Get a list of category names from the categories map - private Vector getCategoryNames() - { - Vector cats = new Vector<>(); - Set keySet = categories.keySet(); - - for (final String key : keySet) - { - // Ignore any category that starts with this - if (key.startsWith("Sex:")) - { - continue; - } - - cats.add(key); - } - - // Sor the selected categories before returning it - Collections.sort(cats); - - return cats; - } - private void loadDropdowns() { - // This method now just loads the category dropdown from the list of - // category names - Vector cats = this.getCategoryNames(); + Vector cats = new Vector<>(nameGen.getCategories()); cbCategory.setModel(new DefaultComboBoxModel<>(cats)); this.loadGenderDD(); this.loadCatalogDD(); } - private void loadFromDocument(Document nameSet) throws DataConversionException - { - Element generator = nameSet.getRootElement(); - java.util.List rulesets = generator.getChildren("RULESET"); - java.util.List lists = generator.getChildren("LIST"); - - for (final Object o : lists) - { - Element list = (Element) o; - loadList(list); - } - - for (final Object ruleset : rulesets) - { - Element ruleSet = (Element) ruleset; - RuleSet rs = loadRuleSet(ruleSet); - allVars.addDataElement(rs); - } - } - - private String loadList(Element list) throws DataConversionException - { - pcgen.core.namegen.DDList dataList = new pcgen.core.namegen.DDList(allVars, - list.getAttributeValue("title"), list.getAttributeValue("id")); - java.util.List elements = list.getChildren(); - - for (final Object element : elements) - { - Element child = (Element) element; - String elementName = child.getName(); - - if (elementName.equals("VALUE")) - { - WeightedDataValue dv = - new WeightedDataValue(child.getText(), child.getAttribute("weight").getIntValue()); - List subElements = child.getChildren("SUBVALUE"); - - for (final Object subElement1 : subElements) - { - Element subElement = (Element) subElement1; - dv.addSubValue(subElement.getAttributeValue("type"), subElement.getText()); - } - - dataList.add(dv); - } - } - - allVars.addDataElement(dataList); - - return dataList.getId(); - } - - private String loadRule(Element rule, String id) throws DataConversionException - { - Rule dataRule = new Rule(allVars, id, id, rule.getAttribute("weight").getIntValue()); - java.util.List elements = rule.getChildren(); - - for (final Object element : elements) - { - Element child = (Element) element; - String elementName = child.getName(); - - switch (elementName) - { - case "GETLIST" -> { - String listId = child.getAttributeValue("idref"); - dataRule.add(listId); - } - case "SPACE" -> { - SpaceRule sp = new SpaceRule(); - allVars.addDataElement(sp); - dataRule.add(sp.getId()); - } - case "HYPHEN" -> { - HyphenRule hy = new HyphenRule(); - allVars.addDataElement(hy); - dataRule.add(hy.getId()); - } - case "CR" -> { - CRRule cr = new CRRule(); - allVars.addDataElement(cr); - dataRule.add(cr.getId()); - } - case "GETRULE" -> { - String ruleId = child.getAttributeValue("idref"); - dataRule.add(ruleId); - } - } - } - - allVars.addDataElement(dataRule); - - return dataRule.getId(); - } - - private RuleSet loadRuleSet(Element ruleSet) throws DataConversionException - { - RuleSet rs = new RuleSet(allVars, ruleSet.getAttributeValue("title"), ruleSet.getAttributeValue("id"), - ruleSet.getAttributeValue("usage")); - java.util.List elements = ruleSet.getChildren(); - ListIterator elementsIterator = elements.listIterator(); - int num = 0; - - while (elementsIterator.hasNext()) - { - Element child = (Element) elementsIterator.next(); - String elementName = child.getName(); - - if (elementName.equals("CATEGORY")) - { - loadCategory(child, rs); - } - else if (elementName.equals("RULE")) - { - rs.add(loadRule(child, rs.getId() + num)); - } - - num++; - } - - return rs; - } - private void loadStructureDD() { if (chkStructure.isSelected()) @@ -908,20 +588,16 @@ private void loadStructureDD() } else { + RuleSet selected = (RuleSet) cbCatalog.getSelectedItem(); + if (selected == null) + { + return; + } Vector struct = new Vector<>(); - - for (String key : ((RuleSet) cbCatalog.getSelectedItem())) + for (Rule r : nameGen.getRulesFor(selected)) { - try - { - struct.add(allVars.getDataElement(key)); - } - catch (Exception e) - { - Logging.errorPrint(e.getMessage(), e); - } + struct.add(r); } - DefaultComboBoxModel structModel = new DefaultComboBoxModel<>(struct); cbStructure.setModel(structModel); cbStructure.setEnabled(true); @@ -954,52 +630,4 @@ public void setGender(String gender) cbGender.setSelectedItem(gender); } } - - /** - * The Class {@code GeneratorDtdResolver} is an EntityResolver implementation - * for use with a SAX parser. It forces the generator.dtd to be read from a - * known location. - */ - public static class GeneratorDtdResolver implements EntityResolver - { - - private final File parent; - - /** - * Create a new instance of GeneratorDtdResolver to read the - * generator.dtd from a specific directory. - * @param parent The parent directory holding generator.dtd - */ - GeneratorDtdResolver(File parent) - { - this.parent = parent; - - } - - @Override - public InputSource resolveEntity(String publicId, String systemId) - { - if (systemId.endsWith("generator.dtd")) - { - // return a special input source - InputStream dtdIn; - try - { - dtdIn = new FileInputStream(new File(parent, "generator.dtd")); - } - catch (FileNotFoundException e) - { - Logging.errorPrint("GeneratorDtdResolver.resolveEntity failed", e); - return null; - - } - return new InputSource(dtdIn); - } - else - { - // use the default behaviour - return null; - } - } - } } From 30140d3e1e8b839021ffc938d10906aa68b6339d Mon Sep 17 00:00:00 2001 From: Vest Date: Wed, 20 May 2026 23:20:29 +0200 Subject: [PATCH 05/34] Add unit tests for NameGenerator and NameGenDataLoader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin NameGenerator's facade behaviour and the loader's handling of the bundled plugins/Random Names dataset before the JDOM2 → javax.xml.parsers swap. Loader tests cover the happy path, the internal Sex:/All buckets, non-directory inputs, broken XML, and empty directories. Generator tests cover category/title/gender filtering, canonical gender ordering, catalog resolution, 100 repeated generations producing non-blank names (schema-based — Dice isn't seedable), and that generateWithRule honours the forced rule. --- .../core/namegen/NameGenDataLoaderTest.java | 87 ++++++++++++ .../pcgen/core/namegen/NameGeneratorTest.java | 129 ++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 code/src/utest/pcgen/core/namegen/NameGenDataLoaderTest.java create mode 100644 code/src/utest/pcgen/core/namegen/NameGeneratorTest.java diff --git a/code/src/utest/pcgen/core/namegen/NameGenDataLoaderTest.java b/code/src/utest/pcgen/core/namegen/NameGenDataLoaderTest.java new file mode 100644 index 00000000000..04794a72426 --- /dev/null +++ b/code/src/utest/pcgen/core/namegen/NameGenDataLoaderTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2026 Vest + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package pcgen.core.namegen; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Smoke tests for {@link NameGenDataLoader} against the bundled + * {@code plugins/Random Names} dataset. Pinned ahead of the JDOM2 → + * javax.xml.parsers swap so the rewrite has a regression net. + */ +public class NameGenDataLoaderTest +{ + private static final File DATA_DIR = + new File(System.getProperty("user.dir"), "plugins/Random Names"); + + @Test + public void loadsBundledDatasetWithoutThrowing() throws IOException + { + NameGenData data = NameGenDataLoader.load(DATA_DIR); + assertNotNull(data); + assertNotNull(data.allVars()); + assertFalse(data.categories().isEmpty(), "no categories were loaded"); + } + + @Test + public void categoriesIncludeSexBucketsAndAll() throws IOException + { + // Loader-level view: raw categories include the Sex: buckets and + // the All pseudo-category. The NameGenerator facade hides them; + // here we just verify the loader sees them. + NameGenData data = NameGenDataLoader.load(DATA_DIR); + assertTrue(data.categories().containsKey("All"), + "loader should expose the 'All' pseudo-category"); + assertTrue(data.categories().keySet().stream().anyMatch(k -> k.startsWith("Sex:")), + "loader should expose at least one 'Sex:' bucket"); + } + + @Test + public void rejectsNonDirectory() throws IOException + { + File notADir = File.createTempFile("namegen", ".tmp"); + notADir.deleteOnExit(); + assertThrows(IOException.class, () -> NameGenDataLoader.load(notADir)); + } + + @Test + public void surfacesParseErrorsAsIoException(@TempDir Path tempDir) throws IOException + { + // generator.dtd must exist for the resolver, but the actual XML is broken. + Files.copy(new File(DATA_DIR, "generator.dtd").toPath(), + tempDir.resolve("generator.dtd")); + Files.writeString(tempDir.resolve("broken.xml"), + "" + + ""); + assertThrows(IOException.class, () -> NameGenDataLoader.load(tempDir.toFile())); + } + + @Test + public void emptyDirectoryYieldsEmptyData(@TempDir Path tempDir) throws IOException + { + NameGenData data = NameGenDataLoader.load(tempDir.toFile()); + assertTrue(data.categories().isEmpty()); + } +} diff --git a/code/src/utest/pcgen/core/namegen/NameGeneratorTest.java b/code/src/utest/pcgen/core/namegen/NameGeneratorTest.java new file mode 100644 index 00000000000..b0b132f9ac4 --- /dev/null +++ b/code/src/utest/pcgen/core/namegen/NameGeneratorTest.java @@ -0,0 +1,129 @@ +/* + * Copyright 2026 Vest + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package pcgen.core.namegen; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Behavioural tests for {@link NameGenerator} driven by the bundled + * {@code plugins/Random Names} dataset. + */ +public class NameGeneratorTest +{ + private static final File DATA_DIR = + new File(System.getProperty("user.dir"), "plugins/Random Names"); + + private static NameGenerator generator; + + @BeforeAll + public static void loadOnce() throws IOException + { + generator = new NameGenerator(DATA_DIR); + } + + @Test + public void getCategoriesHidesInternalBuckets() + { + List categories = generator.getCategories(); + assertFalse(categories.isEmpty()); + assertTrue(categories.stream().noneMatch(c -> c.startsWith("Sex:")), + "Sex: buckets must not leak through the facade"); + assertTrue(categories.stream().noneMatch("All"::equals), + "the 'All' pseudo-category must not leak through"); + assertEquals(categories.stream().sorted().toList(), categories, + "categories must be sorted"); + } + + @Test + public void getTitlesForUnknownCategoryReturnsEmpty() + { + assertTrue(generator.getTitlesFor("definitely-not-a-category").isEmpty()); + } + + @Test + public void getTitlesForFantasyElvenIsNonEmptyAndSorted() + { + List titles = generator.getTitlesFor("Fantasy: Elven"); + assertFalse(titles.isEmpty(), "Fantasy: Elven should have titles"); + assertEquals(titles.stream().sorted().toList(), titles, "titles must be sorted"); + } + + @Test + public void getGendersForCanonicalOrder() + { + List genders = generator.getGendersFor("Fantasy: Elven", "Middle Earth Elf"); + assertFalse(genders.isEmpty()); + // Whatever subset shows up must appear in canonical order. + List canonical = List.of("Female", "Male", "Other"); + List filteredCanonical = canonical.stream().filter(genders::contains).toList(); + assertEquals(filteredCanonical, genders.subList(0, filteredCanonical.size()), + "canonical genders must come first in canonical order"); + } + + @Test + public void getCatalogResolvesKnownTriple() + { + RuleSet rs = generator.getCatalog("Fantasy: Elven", "Middle Earth Elf", "Male"); + assertNotNull(rs, "Fantasy: Elven / Middle Earth Elf / Male should resolve"); + assertEquals("final", rs.getUsage()); + assertEquals("Middle Earth Elf", rs.getTitle()); + } + + @Test + public void getCatalogReturnsNullForUnknownTriple() + { + assertNull(generator.getCatalog("Fantasy: Elven", "Middle Earth Elf", "Nonexistent")); + assertNull(generator.getCatalog("Fantasy: Elven", "no-such-title", "Male")); + assertNull(generator.getCatalog("no-such-category", "Middle Earth Elf", "Male")); + } + + @Test + public void generateProducesNonEmptyNameRepeatedly() throws Exception + { + RuleSet rs = generator.getCatalog("Fantasy: Elven", "Middle Earth Elf", "Male"); + assertNotNull(rs); + for (int i = 0; i < 100; i++) + { + GeneratedName generated = generator.generate(rs); + assertNotNull(generated.rule()); + assertFalse(generated.name().isBlank(), "iteration " + i + " produced blank name"); + } + } + + @Test + public void generateWithRuleAlwaysUsesGivenRule() throws Exception + { + RuleSet rs = generator.getCatalog("Fantasy: Elven", "Middle Earth Elf", "Male"); + List rules = generator.getRulesFor(rs); + assertFalse(rules.isEmpty()); + Rule chosen = rules.get(0); + for (int i = 0; i < 20; i++) + { + GeneratedName generated = generator.generateWithRule(chosen); + assertEquals(chosen, generated.rule()); + } + } + +} From 5789db954e73b90b721848ad1b69595455f2d5d3 Mon Sep 17 00:00:00 2001 From: Vest Date: Wed, 20 May 2026 23:25:04 +0200 Subject: [PATCH 06/34] Replace JDOM2 with javax.xml.parsers in NameGenDataLoader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical translation: SAXBuilder → DocumentBuilder, Element methods to their org.w3c.dom counterparts, and a small childElements helper to filter NodeList down to elements (since the DOM API exposes whitespace text nodes between elements that JDOM2 hid). Integer parsing moves from getIntValue (checked DataConversionException) to Integer.parseInt; the loader catches NumberFormatException alongside SAXException and rethrows as IOException so callers see the same failure shape as before. Mixed-content foo... elements in gaelic.xml exposed a real divergence: getTextContent concatenates descendant text, while JDOM2's getText returned only direct child text. Added a directText helper that walks direct TEXT_NODE / CDATA_SECTION_NODE children, plus a regression test pinning the "Donn / brown, brown-haired" entry. JDOM2 is still on the classpath; the dependency drop is the next commit. --- .../pcgen/core/namegen/NameGenDataLoader.java | 150 ++++++++++++------ .../pcgen/core/namegen/NameGeneratorTest.java | 18 +++ 2 files changed, 116 insertions(+), 52 deletions(-) diff --git a/code/src/java/pcgen/core/namegen/NameGenDataLoader.java b/code/src/java/pcgen/core/namegen/NameGenDataLoader.java index 2c90e3daab0..37e87c28529 100644 --- a/code/src/java/pcgen/core/namegen/NameGenDataLoader.java +++ b/code/src/java/pcgen/core/namegen/NameGenDataLoader.java @@ -19,21 +19,24 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.ListIterator; import java.util.Map; +import java.util.stream.IntStream; -import org.jdom2.DataConversionException; -import org.jdom2.DocType; -import org.jdom2.Document; -import org.jdom2.Element; -import org.jdom2.JDOMException; -import org.jdom2.input.SAXBuilder; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.w3c.dom.DocumentType; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; +import org.xml.sax.SAXException; import pcgen.util.Logging; @@ -43,8 +46,8 @@ * {@code pcgen.gui2.namegen.NameGenPanel}; pulling it here decouples the * data model from any UI toolkit. * - *

This class still uses JDOM2; a follow-up commit replaces it with the - * built-in {@code javax.xml.parsers} API. + *

Uses the JDK-bundled {@code javax.xml.parsers} API rather than a + * third-party XML library so the project doesn't need to ship JDOM2. */ public final class NameGenDataLoader { @@ -58,7 +61,8 @@ private NameGenDataLoader() * @param dataDir directory containing {@code generator.dtd} and the XML * files to parse * @return populated {@link NameGenData} - * @throws IOException if {@code dataDir} is not a directory + * @throws IOException if {@code dataDir} is not a directory or any file + * fails to parse */ public static NameGenData load(File dataDir) throws IOException { @@ -73,22 +77,21 @@ public static NameGenData load(File dataDir) throws IOException } VariableHashMap allVars = new VariableHashMap(); Map> categories = new HashMap<>(); - SAXBuilder builder = new SAXBuilder(); + DocumentBuilder builder = newDocumentBuilder(); builder.setEntityResolver(new GeneratorDtdResolver(dataDir)); for (File dataFile : dataFiles) { try { - URL url = dataFile.toURI().toURL(); - Document nameSet = builder.build(url); - DocType dt = nameSet.getDocType(); - if (dt != null && "GENERATOR".equals(dt.getElementName())) + Document nameSet = builder.parse(dataFile); + DocumentType dt = nameSet.getDoctype(); + if (dt != null && "GENERATOR".equals(dt.getName())) { loadFromDocument(nameSet, allVars, categories); } } - catch (JDOMException | IOException e) + catch (SAXException | NumberFormatException e) { Logging.errorPrint("Failed to parse " + dataFile.getName(), e); throw new IOException("XML error in file " + dataFile.getName(), e); @@ -97,50 +100,54 @@ public static NameGenData load(File dataDir) throws IOException return new NameGenData(allVars, categories); } - private static void loadFromDocument(Document nameSet, VariableHashMap allVars, - Map> categories) throws DataConversionException + private static DocumentBuilder newDocumentBuilder() throws IOException { - Element generator = nameSet.getRootElement(); - List rulesets = generator.getChildren("RULESET"); - List lists = generator.getChildren("LIST"); - - for (final Object o : lists) + try { - loadList((Element) o, allVars); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + return factory.newDocumentBuilder(); } - for (final Object ruleset : rulesets) + catch (ParserConfigurationException e) { - RuleSet rs = loadRuleSet((Element) ruleset, allVars, categories); - allVars.addDataElement(rs); + throw new IOException("Cannot create XML parser", e); } } - private static String loadList(Element list, VariableHashMap allVars) throws DataConversionException + private static void loadFromDocument(Document nameSet, VariableHashMap allVars, + Map> categories) + { + Element generator = nameSet.getDocumentElement(); + childElements(generator, "LIST").forEach(list -> loadList(list, allVars)); + childElements(generator, "RULESET").forEach(ruleset -> { + RuleSet rs = loadRuleSet(ruleset, allVars, categories); + allVars.addDataElement(rs); + }); + } + + private static String loadList(Element list, VariableHashMap allVars) { DDList dataList = new DDList(allVars, - list.getAttributeValue("title"), list.getAttributeValue("id")); - for (Element child : list.getChildren("VALUE")) + list.getAttribute("title"), list.getAttribute("id")); + for (Element child : childElements(list, "VALUE")) { - WeightedDataValue dv = new WeightedDataValue(child.getText(), - child.getAttribute("weight").getIntValue()); - child.getChildren("SUBVALUE").forEach(sub -> - dv.addSubValue(sub.getAttributeValue("type"), sub.getText())); + WeightedDataValue dv = new WeightedDataValue(directText(child), + Integer.parseInt(child.getAttribute("weight"))); + childElements(child, "SUBVALUE").forEach(sub -> + dv.addSubValue(sub.getAttribute("type"), directText(sub))); dataList.add(dv); } allVars.addDataElement(dataList); return dataList.getId(); } - private static String loadRule(Element rule, String id, VariableHashMap allVars) throws DataConversionException + private static String loadRule(Element rule, String id, VariableHashMap allVars) { - Rule dataRule = new Rule(allVars, id, id, rule.getAttribute("weight").getIntValue()); - List elements = rule.getChildren(); - for (final Object element : elements) + Rule dataRule = new Rule(allVars, id, id, Integer.parseInt(rule.getAttribute("weight"))); + for (Element child : childElements(rule)) { - Element child = (Element) element; - switch (child.getName()) + switch (child.getTagName()) { - case "GETLIST" -> dataRule.add(child.getAttributeValue("idref")); + case "GETLIST" -> dataRule.add(child.getAttribute("idref")); case "SPACE" -> { SpaceRule sp = new SpaceRule(); allVars.addDataElement(sp); @@ -156,7 +163,7 @@ private static String loadRule(Element rule, String id, VariableHashMap allVars) allVars.addDataElement(cr); dataRule.add(cr.getId()); } - case "GETRULE" -> dataRule.add(child.getAttributeValue("idref")); + case "GETRULE" -> dataRule.add(child.getAttribute("idref")); default -> { /* ignore */ } } } @@ -165,17 +172,17 @@ private static String loadRule(Element rule, String id, VariableHashMap allVars) } private static RuleSet loadRuleSet(Element ruleSet, VariableHashMap allVars, - Map> categories) throws DataConversionException + Map> categories) { - RuleSet rs = new RuleSet(allVars, ruleSet.getAttributeValue("title"), - ruleSet.getAttributeValue("id"), ruleSet.getAttributeValue("usage")); - List elements = ruleSet.getChildren(); - ListIterator it = elements.listIterator(); + RuleSet rs = new RuleSet(allVars, ruleSet.getAttribute("title"), + ruleSet.getAttribute("id"), ruleSet.getAttribute("usage")); + List children = childElements(ruleSet); + // Index counter is preserved across all child element types — it's + // part of the generated rule id and must match the legacy numbering. int num = 0; - while (it.hasNext()) + for (Element child : children) { - Element child = (Element) it.next(); - String elementName = child.getName(); + String elementName = child.getTagName(); if ("CATEGORY".equals(elementName)) { loadCategory(child, rs, categories); @@ -191,10 +198,49 @@ else if ("RULE".equals(elementName)) private static void loadCategory(Element category, RuleSet rs, Map> categories) { - String key = category.getAttributeValue("title"); + String key = category.getAttribute("title"); categories.computeIfAbsent(key, k -> new ArrayList<>()).add(rs); } + private static List childElements(Element parent) + { + NodeList nodes = parent.getChildNodes(); + List out = new ArrayList<>(nodes.getLength()); + IntStream.range(0, nodes.getLength()) + .mapToObj(nodes::item) + .filter(n -> n.getNodeType() == Node.ELEMENT_NODE) + .map(Element.class::cast) + .forEach(out::add); + return out; + } + + private static List childElements(Element parent, String tagName) + { + return childElements(parent).stream() + .filter(e -> tagName.equals(e.getTagName())) + .toList(); + } + + /** + * Returns the concatenation of direct child text nodes only — + * matches JDOM2's {@code Element.getText()} semantics, which + * excludes text inside descendant elements. Needed because the + * data files use mixed content like + * {@code Donn...} where the + * value is just {@code "Donn"}. + */ + private static String directText(Element parent) + { + NodeList nodes = parent.getChildNodes(); + StringBuilder sb = new StringBuilder(); + IntStream.range(0, nodes.getLength()) + .mapToObj(nodes::item) + .filter(n -> n.getNodeType() == Node.TEXT_NODE + || n.getNodeType() == Node.CDATA_SECTION_NODE) + .forEach(n -> sb.append(n.getNodeValue())); + return sb.toString(); + } + /** * Resolves {@code generator.dtd} from a known directory rather than the * network. Carried over verbatim from the old Swing panel. diff --git a/code/src/utest/pcgen/core/namegen/NameGeneratorTest.java b/code/src/utest/pcgen/core/namegen/NameGeneratorTest.java index b0b132f9ac4..f31b34d676c 100644 --- a/code/src/utest/pcgen/core/namegen/NameGeneratorTest.java +++ b/code/src/utest/pcgen/core/namegen/NameGeneratorTest.java @@ -22,6 +22,7 @@ import java.io.File; import java.io.IOException; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -126,4 +127,21 @@ public void generateWithRuleAlwaysUsesGivenRule() throws Exception } } + @Test + public void mixedContentValueExcludesSubvalueText() throws Exception + { + // In gaelic.xml, Donnbrown, + // brown-haired: the value text must be just + // "Donn", not "Donnbrown, brown-haired". Regression guard for + // JDOM2 → javax.xml.parsers (Element.getText vs getTextContent). + DataElement el = generator.getData().allVars() + .getDataElement("gaelic-male-descriptive-byname"); + assertTrue(el instanceof DDList, "expected a DDList"); + DDList list = (DDList) el; + Optional donn = list.stream() + .filter(v -> "Donn".equals(v.getValue())) + .findFirst(); + assertTrue(donn.isPresent(), "expected to find 'Donn' value in list"); + assertEquals("brown, brown-haired", donn.get().getSubValue("meaning")); + } } From 82213006dcf3bb1690816fd6e0df7477ad3880bf Mon Sep 17 00:00:00 2001 From: Vest Date: Wed, 20 May 2026 23:32:43 +0200 Subject: [PATCH 07/34] Drop the JDOM2 dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last call into JDOM2 went away with the previous commit's loader rewrite. Drop the runtime dependency and module require, plus every JDOM mention left in source — the lingering "we used to use JDOM2" comments would rot otherwise. About-dialog attribution strings (in_abt_lib_jdom across en/es/fr/it bundles) and a broken-since-forever Src.iml jar reference go too. --- build.gradle | 3 +-- code/src/Src.iml | 9 --------- code/src/java/module-info.java | 1 - .../java/pcgen/core/namegen/NameGenDataLoader.java | 12 ++++++------ .../pcgen/gui3/dialog/AboutDialogController.java | 1 - .../resources/pcgen/lang/LanguageBundle.properties | 2 -- .../pcgen/lang/LanguageBundle_es.properties | 2 -- .../pcgen/lang/LanguageBundle_fr.properties | 2 -- .../pcgen/lang/LanguageBundle_it.properties | 2 -- code/src/testResources/pcgen/lang/cleaned.properties | 2 -- .../pcgen/core/namegen/NameGenDataLoaderTest.java | 3 +-- .../utest/pcgen/core/namegen/NameGeneratorTest.java | 5 +++-- 12 files changed, 11 insertions(+), 33 deletions(-) diff --git a/build.gradle b/build.gradle index 108be993871..659b2d9c5bb 100644 --- a/build.gradle +++ b/build.gradle @@ -233,7 +233,6 @@ dependencies { implementation 'net.sf.saxon:Saxon-HE:12.9' implementation 'org.scijava:jep:2.4.2' implementation 'org.freemarker:freemarker:2.3.34' - implementation 'org.jdom:jdom2:2.0.6.1' implementation 'net.sourceforge.argparse4j:argparse4j:0.9.0' implementation 'org.controlsfx:controlsfx:11.2.3' implementation project(':PCGen-base') @@ -314,7 +313,7 @@ jlink { addExtraDependencies('javafx') forceMerge('jep', 'fop', 'Saxon-HE', 'commons-lang3', 'commons-io', 'spring', - 'freemarker', 'jdom2', 'argparse4j', 'xmlunit', 'controlsfx', + 'freemarker', 'argparse4j', 'xmlunit', 'controlsfx', 'annotations', 'spotbugs', 'xmlresolver') mergedModule { diff --git a/code/src/Src.iml b/code/src/Src.iml index d4ed0035a2e..7698633de7d 100644 --- a/code/src/Src.iml +++ b/code/src/Src.iml @@ -8,15 +8,6 @@ - - - - - - - - - diff --git a/code/src/java/module-info.java b/code/src/java/module-info.java index 6a75e5bcbb1..1e84f7827e6 100644 --- a/code/src/java/module-info.java +++ b/code/src/java/module-info.java @@ -25,7 +25,6 @@ requires org.apache.commons.lang3; requires org.apache.commons.io; requires freemarker; - requires org.jdom2; requires net.sourceforge.argparse4j; requires org.controlsfx.controls; requires org.apache.xmlgraphics.fop.core; diff --git a/code/src/java/pcgen/core/namegen/NameGenDataLoader.java b/code/src/java/pcgen/core/namegen/NameGenDataLoader.java index 37e87c28529..1223a0f982c 100644 --- a/code/src/java/pcgen/core/namegen/NameGenDataLoader.java +++ b/code/src/java/pcgen/core/namegen/NameGenDataLoader.java @@ -46,8 +46,8 @@ * {@code pcgen.gui2.namegen.NameGenPanel}; pulling it here decouples the * data model from any UI toolkit. * - *

Uses the JDK-bundled {@code javax.xml.parsers} API rather than a - * third-party XML library so the project doesn't need to ship JDOM2. + *

Uses the JDK-bundled {@code javax.xml.parsers} API so the project + * doesn't need a third-party XML library. */ public final class NameGenDataLoader { @@ -222,12 +222,12 @@ private static List childElements(Element parent, String tagName) } /** - * Returns the concatenation of direct child text nodes only — - * matches JDOM2's {@code Element.getText()} semantics, which - * excludes text inside descendant elements. Needed because the + * Returns the concatenation of direct child text nodes only, + * excluding text inside descendant elements. Needed because the * data files use mixed content like * {@code Donn...} where the - * value is just {@code "Donn"}. + * value is just {@code "Donn"} — {@code Element.getTextContent()} + * would return {@code "Donn"} concatenated with the subvalue's text. */ private static String directText(Element parent) { diff --git a/code/src/java/pcgen/gui3/dialog/AboutDialogController.java b/code/src/java/pcgen/gui3/dialog/AboutDialogController.java index 386050f7425..9095c139aa5 100644 --- a/code/src/java/pcgen/gui3/dialog/AboutDialogController.java +++ b/code/src/java/pcgen/gui3/dialog/AboutDialogController.java @@ -131,7 +131,6 @@ private void initText() )); String s = LanguageBundle.getString("in_abt_lib_apache"); //$NON-NLS-1$ - s += LanguageBundle.getString("in_abt_lib_jdom"); //$NON-NLS-1$ librariesArea.setText(s); String releaseDateStr = PCGenPropBundle.getReleaseDate(); diff --git a/code/src/resources/pcgen/lang/LanguageBundle.properties b/code/src/resources/pcgen/lang/LanguageBundle.properties index 0b9bde9d640..05cf38c2028 100644 --- a/code/src/resources/pcgen/lang/LanguageBundle.properties +++ b/code/src/resources/pcgen/lang/LanguageBundle.properties @@ -390,8 +390,6 @@ in_abt_java_version=Java Version: in_abt_lib_apache=This product includes software developed by the Apache Software Foundation (http://www.apache.org/): Avalon, Batik, FOP. Copyright (C) 1999-2003 The Apache Software Foundation. All rights reserved.\n\n -in_abt_lib_jdom=This product includes software developed by the JDOM Project (http://www.jdom.org/).\n\n - in_abt_libraries=Libraries in_abt_awards=Awards diff --git a/code/src/resources/pcgen/lang/LanguageBundle_es.properties b/code/src/resources/pcgen/lang/LanguageBundle_es.properties index 6e5811665fb..69a6c0a34b3 100644 --- a/code/src/resources/pcgen/lang/LanguageBundle_es.properties +++ b/code/src/resources/pcgen/lang/LanguageBundle_es.properties @@ -390,8 +390,6 @@ in_abt_java_version=Versión de Java: in_abt_lib_apache=Este producto incluye software desarrollado por Apache Software Foundation (http://www.apache.org/): Avalon, Batik, FOP, Xalan y Xerces. Copyright (C) 1999-2003 La Fundación Apache Software. Todos los derechos reservados.\n\n -in_abt_lib_jdom=Este producto incluye software desarrollado por el Proyecto JDOM (http://www.jdom.org/).\n\n - in_abt_libraries=Bibliotecas in_abt_awards=Premios diff --git a/code/src/resources/pcgen/lang/LanguageBundle_fr.properties b/code/src/resources/pcgen/lang/LanguageBundle_fr.properties index 4bf49996571..bf6a90b442a 100644 --- a/code/src/resources/pcgen/lang/LanguageBundle_fr.properties +++ b/code/src/resources/pcgen/lang/LanguageBundle_fr.properties @@ -369,8 +369,6 @@ in_abt_java_version=Version de Java : in_abt_lib_apache=Ce produit contient des logiciels développés par The Apache Software Foundation (http://www.apache.org/) : Avalon, Batik, FOP, Xalan et Xerces. Copyright © 1999—2003 The Apache Software Foundation. Tous droits réservés.\n\n -in_abt_lib_jdom=Ce produit contient du logiciel développé par JDOM Project (http://www.jdom.org/).\n\n - in_abt_libraries=Bibliothèques in_abt_awards=Récompenses diff --git a/code/src/resources/pcgen/lang/LanguageBundle_it.properties b/code/src/resources/pcgen/lang/LanguageBundle_it.properties index 2d46dea2672..6eb287bb9b8 100644 --- a/code/src/resources/pcgen/lang/LanguageBundle_it.properties +++ b/code/src/resources/pcgen/lang/LanguageBundle_it.properties @@ -341,8 +341,6 @@ in_abt_java_version=Versione di Java: in_abt_lib_apache=Questo prodotto include software sviluppato da Apache Software Foundation (http:#www.apache.org/): Avalon, Batik, FOP, Xalan and Xerces. Copyright (C) 1999-2003 The Apache Software Foundation. Tutti i diritti riservati. -in_abt_lib_jdom=Questo prodotto include software sviluppato dal progetto JDOM (http:#www.jdom.org/). - in_abt_libraries=Librerie in_abt_awards=Premi diff --git a/code/src/testResources/pcgen/lang/cleaned.properties b/code/src/testResources/pcgen/lang/cleaned.properties index b44578e4ee8..c652bd6f181 100644 --- a/code/src/testResources/pcgen/lang/cleaned.properties +++ b/code/src/testResources/pcgen/lang/cleaned.properties @@ -327,8 +327,6 @@ in_abt_java_version=Java Version: in_abt_lib_apache=This product includes software developed by the Apache Software Foundation (http://www.apache.org/): Avalon, Batik, FOP. Copyright (C) 1999-2003 The Apache Software Foundation. All rights reserved.\n\n -in_abt_lib_jdom=This product includes software developed by the JDOM Project (http://www.jdom.org/).\n\n - in_abt_libraries=Libraries in_abt_awards=Awards diff --git a/code/src/utest/pcgen/core/namegen/NameGenDataLoaderTest.java b/code/src/utest/pcgen/core/namegen/NameGenDataLoaderTest.java index 04794a72426..b1f926e01fa 100644 --- a/code/src/utest/pcgen/core/namegen/NameGenDataLoaderTest.java +++ b/code/src/utest/pcgen/core/namegen/NameGenDataLoaderTest.java @@ -28,8 +28,7 @@ /** * Smoke tests for {@link NameGenDataLoader} against the bundled - * {@code plugins/Random Names} dataset. Pinned ahead of the JDOM2 → - * javax.xml.parsers swap so the rewrite has a regression net. + * {@code plugins/Random Names} dataset. */ public class NameGenDataLoaderTest { diff --git a/code/src/utest/pcgen/core/namegen/NameGeneratorTest.java b/code/src/utest/pcgen/core/namegen/NameGeneratorTest.java index f31b34d676c..ffc23a7f6bc 100644 --- a/code/src/utest/pcgen/core/namegen/NameGeneratorTest.java +++ b/code/src/utest/pcgen/core/namegen/NameGeneratorTest.java @@ -132,8 +132,9 @@ public void mixedContentValueExcludesSubvalueText() throws Exception { // In gaelic.xml, Donnbrown, // brown-haired: the value text must be just - // "Donn", not "Donnbrown, brown-haired". Regression guard for - // JDOM2 → javax.xml.parsers (Element.getText vs getTextContent). + // "Donn", not "Donnbrown, brown-haired" — the DOM's + // getTextContent concatenates descendant text, but the data + // model stores the value separately from its subvalues. DataElement el = generator.getData().allVars() .getDataElement("gaelic-male-descriptive-byname"); assertTrue(el instanceof DDList, "expected a DDList"); From e86167d26df2cb7f6f612099d9f3ed9a90a4c5df Mon Sep 17 00:00:00 2001 From: Vest Date: Wed, 20 May 2026 23:34:09 +0200 Subject: [PATCH 08/34] Delete vestigial Src.iml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This IntelliJ module descriptor predates the Gradle build. It references lib/jdom.jar and lib/MRJ141Stubs.jar — neither exists in the repo — and has been functionally dead since the project moved to Gradle. .gitignore already excludes the .iml files that modern IntelliJ generates from the Gradle import. --- code/src/Src.iml | 177 ----------------------------------------------- 1 file changed, 177 deletions(-) delete mode 100644 code/src/Src.iml diff --git a/code/src/Src.iml b/code/src/Src.iml deleted file mode 100644 index 7698633de7d..00000000000 --- a/code/src/Src.iml +++ /dev/null @@ -1,177 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From cee570c2cf0dc28092781e544377d0dc86ea6d8f Mon Sep 17 00:00:00 2001 From: Vest Date: Wed, 20 May 2026 23:35:33 +0200 Subject: [PATCH 09/34] Drop the Gradle 'idea' plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plugin generated IntelliJ project files via ./gradlew idea, a workflow modern IntelliJ supersedes by importing build.gradle directly. The previous developer's TODO already noted that the generated source-set wiring was broken in current IntelliJ, and .gitignore excludes the .iml files the Gradle import would produce — confirming nobody runs the idea task. --- build.gradle | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/build.gradle b/build.gradle index 659b2d9c5bb..a54a537fba2 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,6 @@ import groovy.json.JsonSlurper import org.apache.tools.ant.filters.FixCrLfFilter import org.apache.tools.ant.taskdefs.condition.Os -import org.gradle.plugins.ide.idea.model.ProjectLibrary import de.undercouch.gradle.tasks.download.Download @@ -30,7 +29,6 @@ plugins { id 'com.github.spotbugs' version '6.5.4' // Spotbugs for Java id 'maven-publish' // Publishing to Maven Central id 'pmd' // PMD for Java, configured further below - id 'idea' // For IntelliJ IDEA users id 'de.undercouch.download' version '5.7.0' // Shows download percentage id 'com.github.ben-manes.versions' version '0.54.0' // Checks versions for plugins and dependencies id 'com.dorongold.task-tree' version '4.0.1' // Prints the task dependency tree @@ -115,8 +113,6 @@ repositories { } // Define the source sets for each task/module -// TODO even though we have the idea plugin, IntelliJ doesn't reference -// these source sets whether you run that task or not. sourceSets { // The main build main { @@ -835,32 +831,6 @@ tasks.named("dependencyUpdates") { } } -// Support for creating IntelliJ IDEA files. -idea { - project { - def javafxLib = layout.projectDirectory.dir("mods/lib") - .asFileTree.matching { include "*.jar" } - .files - - def projectLibrary = new ProjectLibrary() - projectLibrary.name = "lib" - projectLibrary.classes += javafxLib - - projectLibraries += [ projectLibrary ] - } - - module { - testSources.from(project.sourceSets.itest.java.srcDirs, - project.sourceSets.slowtest.java.srcDirs) - - iml.withXml { - def module = asNode() - def component = module.find {it.@name == 'NewModuleRootManager'} - def orderEntry = component.appendNode('orderEntry', [ type: 'library', scope: 'RUNTIME', name:'lib', level:'project']) - } - } -} - // Use the gradle wrapper tasks.named("wrapper") { distributionType = Wrapper.DistributionType.ALL From 56df8094ffb2406fd60ee5487300eb5a811bebd5 Mon Sep 17 00:00:00 2001 From: Vest Date: Wed, 20 May 2026 23:41:37 +0200 Subject: [PATCH 10/34] Add JavaFX random-name panel, dialog, and controller Introduces pcgen.gui3.namegen with an FXML-driven panel that drives the headless NameGenerator: two cascading combo boxes (Category > Title), three gender radio buttons with sticky-fallback semantics, and Generate / OK / Cancel actions. The dialog wrapper mirrors the existing Swing API (getChosenName / getGender) over JFXPanelFromResource so Swing callers can switch over in a follow-up commit. The old Swing panel still works; nothing is wired to the new dialog yet. --- code/src/java/module-info.java | 1 + .../pcgen/gui3/namegen/RandomNameDialog.java | 67 ++++ .../namegen/RandomNamePanelController.java | 300 ++++++++++++++++++ .../pcgen/gui3/namegen/RandomNamePanel.fxml | 69 ++++ 4 files changed, 437 insertions(+) create mode 100644 code/src/java/pcgen/gui3/namegen/RandomNameDialog.java create mode 100644 code/src/java/pcgen/gui3/namegen/RandomNamePanelController.java create mode 100644 code/src/resources/pcgen/gui3/namegen/RandomNamePanel.fxml diff --git a/code/src/java/module-info.java b/code/src/java/module-info.java index 1e84f7827e6..a82d96f2b4d 100644 --- a/code/src/java/module-info.java +++ b/code/src/java/module-info.java @@ -72,6 +72,7 @@ opens pcgen.gui3.component; opens pcgen.gui3.core; opens pcgen.gui3.dialog; + opens pcgen.gui3.namegen; opens pcgen.gui3.preferences; opens pcgen.gui3.preloader; opens pcgen.gui3.utilty; diff --git a/code/src/java/pcgen/gui3/namegen/RandomNameDialog.java b/code/src/java/pcgen/gui3/namegen/RandomNameDialog.java new file mode 100644 index 00000000000..825043382f6 --- /dev/null +++ b/code/src/java/pcgen/gui3/namegen/RandomNameDialog.java @@ -0,0 +1,67 @@ +/* + * Copyright 2026 Vest + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package pcgen.gui3.namegen; + +import pcgen.gui3.JFXPanelFromResource; +import pcgen.system.LanguageBundle; + +import javafx.application.Platform; + +/** + * Modal random-name dialog. Presented to Swing callers as a synchronous + * call: construct, {@link #showAndBlock(String)}, then read back + * {@link #getChosenName()} / {@link #getGender()}. + */ +public final class RandomNameDialog +{ + private final JFXPanelFromResource panel; + private final String initialGender; + + public RandomNameDialog(String initialGender) + { + this.initialGender = initialGender; + this.panel = new JFXPanelFromResource<>(RandomNamePanelController.class, "RandomNamePanel.fxml"); + } + + /** + * Show the dialog and block until the user closes it. Must be called + * from a non-FX thread (typically the Swing EDT or the main thread). + */ + public void showAndBlock() + { + if (initialGender != null && !initialGender.isEmpty()) + { + Platform.runLater(() -> { + RandomNamePanelController controller = panel.getControllerFromJavaFXThread(); + if (controller != null) + { + controller.setInitialGender(initialGender); + } + }); + } + panel.showAndBlock(LanguageBundle.getString("in_rndNameTitle")); + } + + public String getChosenName() + { + RandomNamePanelController controller = panel.getController(); + return controller == null ? "" : controller.getChosenName(); + } + + public String getGender() + { + RandomNamePanelController controller = panel.getController(); + return controller == null ? "" : controller.getGender(); + } +} diff --git a/code/src/java/pcgen/gui3/namegen/RandomNamePanelController.java b/code/src/java/pcgen/gui3/namegen/RandomNamePanelController.java new file mode 100644 index 00000000000..6657d92f91f --- /dev/null +++ b/code/src/java/pcgen/gui3/namegen/RandomNamePanelController.java @@ -0,0 +1,300 @@ +/* + * Copyright 2026 Vest + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package pcgen.gui3.namegen; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +import pcgen.core.SettingsHandler; +import pcgen.core.namegen.GeneratedName; +import pcgen.core.namegen.NameGenerator; +import pcgen.core.namegen.RuleSet; +import pcgen.system.LanguageBundle; +import pcgen.util.Logging; + +import javafx.collections.FXCollections; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; +import javafx.stage.Stage; + +/** + * JavaFX controller behind the random-name dialog. Drives two cascading + * combo boxes (Category → Title) and a gender radio group with sticky + * fallback, then asks the headless {@link NameGenerator} for names. + */ +public final class RandomNamePanelController +{ + @FXML + private ComboBox categoryCombo; + @FXML + private ComboBox titleCombo; + @FXML + private ToggleGroup genderGroup; + @FXML + private RadioButton genderFemale; + @FXML + private RadioButton genderMale; + @FXML + private RadioButton genderOther; + @FXML + private Label generatedNameLabel; + @FXML + private Label meaningLabel; + @FXML + private Label pronunciationLabel; + + private NameGenerator nameGenerator; + private String chosenName = ""; + private String chosenGender = ""; + private boolean cancelled = true; + private String preferredGender; + + @FXML + void initialize() + { + try + { + nameGenerator = new NameGenerator(new File(getDataDir())); + } + catch (IOException e) + { + Logging.errorPrint("failed to load random-name data", e); + generatedNameLabel.setText(LanguageBundle.getString("in_rndNmDefault")); + return; + } + + categoryCombo.setItems(FXCollections.observableArrayList(nameGenerator.getCategories())); + categoryCombo.valueProperty().addListener((obs, old, val) -> onCategoryChanged(val)); + titleCombo.valueProperty().addListener((obs, old, val) -> onTitleChanged(val)); + genderGroup.selectedToggleProperty().addListener((obs, old, val) -> onGenderChanged(val)); + + if (!categoryCombo.getItems().isEmpty()) + { + categoryCombo.getSelectionModel().selectFirst(); + } + } + + /** + * Called by {@link RandomNameDialog} before showing, to pre-select a + * gender if the character already has one. Must be invoked on the FX + * thread, after the FXML has loaded. + */ + public void setInitialGender(String gender) + { + preferredGender = gender; + if (gender != null && !gender.isEmpty() && titleCombo.getValue() != null) + { + selectGender(gender); + } + } + + private void onCategoryChanged(String category) + { + if (category == null) + { + titleCombo.setItems(FXCollections.emptyObservableList()); + return; + } + List titles = nameGenerator.getTitlesFor(category); + titleCombo.setItems(FXCollections.observableArrayList(titles)); + if (!titles.isEmpty()) + { + titleCombo.getSelectionModel().selectFirst(); + } + } + + private void onTitleChanged(String title) + { + String category = categoryCombo.getValue(); + if (category == null || title == null) + { + disableAllGenders(); + return; + } + List available = nameGenerator.getGendersFor(category, title); + genderFemale.setDisable(!available.contains("Female")); + genderMale.setDisable(!available.contains("Male")); + genderOther.setDisable(!available.contains("Other")); + + String previous = currentGender(); + String target = chooseStickyGender(available, previous); + selectGender(target); + } + + private void onGenderChanged(Toggle selected) + { + // Cleared selection happens transiently while we swap toggles — + // don't react until a button is actually selected. + if (selected == null) + { + return; + } + clearOutput(); + } + + @FXML + void onGenerate(ActionEvent event) + { + RuleSet catalog = currentCatalog(); + if (catalog == null) + { + return; + } + try + { + GeneratedName result = nameGenerator.generate(catalog); + generatedNameLabel.setText(result.name()); + meaningLabel.setText(LanguageBundle.getString("in_rndNameMeaning") + " " + result.meaning()); + pronunciationLabel.setText(LanguageBundle.getString("in_rndNmPronounciation") + " " + result.pronunciation()); + } + catch (Exception e) + { + Logging.errorPrint("failed to generate random name", e); + } + } + + @FXML + void onOk(ActionEvent event) + { + String text = generatedNameLabel.getText(); + String defaultLabel = LanguageBundle.getString("in_rndNmDefault"); + // If the user clicks OK without ever generating, treat as cancel. + if (text == null || text.isEmpty() || text.equals(defaultLabel)) + { + cancelled = true; + } + else + { + cancelled = false; + chosenName = text; + chosenGender = currentGender(); + } + closeStage(event); + } + + @FXML + void onCancel(ActionEvent event) + { + cancelled = true; + closeStage(event); + } + + public String getChosenName() + { + return cancelled ? "" : chosenName; + } + + public String getGender() + { + return cancelled ? "" : chosenGender; + } + + private RuleSet currentCatalog() + { + String category = categoryCombo.getValue(); + String title = titleCombo.getValue(); + String gender = currentGender(); + if (category == null || title == null || gender.isEmpty()) + { + return null; + } + return nameGenerator.getCatalog(category, title, gender); + } + + private String currentGender() + { + Toggle selected = genderGroup.getSelectedToggle(); + if (selected instanceof RadioButton rb) + { + return rb.getText(); + } + return ""; + } + + private String chooseStickyGender(List available, String previous) + { + if (available.isEmpty()) + { + return ""; + } + boolean previousStillValid = previous != null && available.contains(previous); + if (previousStillValid) + { + return previous; + } + boolean preferredStillValid = preferredGender != null && available.contains(preferredGender); + if (preferredStillValid) + { + return preferredGender; + } + return available.get(0); + } + + private void selectGender(String gender) + { + RadioButton target = switch (gender) + { + case "Female" -> genderFemale; + case "Male" -> genderMale; + case "Other" -> genderOther; + default -> null; + }; + if (target != null && !target.isDisable()) + { + target.setSelected(true); + } + else + { + genderGroup.selectToggle(null); + } + } + + private void disableAllGenders() + { + genderFemale.setDisable(true); + genderMale.setDisable(true); + genderOther.setDisable(true); + genderGroup.selectToggle(null); + } + + private void clearOutput() + { + generatedNameLabel.setText(LanguageBundle.getString("in_rndNmDefault")); + meaningLabel.setText(LanguageBundle.getString("in_rndNameMeaning")); + pronunciationLabel.setText(LanguageBundle.getString("in_rndNmPronounciation")); + } + + private void closeStage(ActionEvent event) + { + Object source = event.getSource(); + if (source instanceof javafx.scene.Node node && node.getScene() != null + && node.getScene().getWindow() instanceof Stage stage) + { + stage.close(); + } + } + + private static String getDataDir() + { + return Objects.requireNonNull(SettingsHandler.getGmgenPluginDir()).toString() + + File.separator + "Random Names"; + } +} diff --git a/code/src/resources/pcgen/gui3/namegen/RandomNamePanel.fxml b/code/src/resources/pcgen/gui3/namegen/RandomNamePanel.fxml new file mode 100644 index 00000000000..795f5ac9705 --- /dev/null +++ b/code/src/resources/pcgen/gui3/namegen/RandomNamePanel.fxml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + +