diff --git a/build.gradle b/build.gradle index 7a5dcb9d6c4..d771f0a1e5d 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.5' // 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 @@ -102,10 +100,6 @@ repositories { url = 'http://pc-gen.org/librepo/' allowInsecureProtocol = true } - maven { - name = 'local' - url = 'installers' - } // Use Maven plugin to reference JBoss artifact repository // TODO Which libs do we pull from here? maven { @@ -115,8 +109,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 { @@ -232,7 +224,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') @@ -313,7 +304,7 @@ jlink { addExtraDependencies('javafx') forceMerge('jep', 'fop', 'Saxon-HE', 'commons-lang3', 'spring', - 'freemarker', 'jdom2', 'argparse4j', 'xmlunit', 'controlsfx', + 'freemarker', 'argparse4j', 'xmlunit', 'controlsfx', 'annotations', 'spotbugs', 'xmlresolver') mergedModule { @@ -697,18 +688,20 @@ tasks.register("slowtest", Test) { tasks.register("datatest", Test) { dependsOn jar + useJUnitPlatform() testClassesDirs = sourceSets.slowtest.output.classesDirs classpath = sourceSets.slowtest.runtimeClasspath - include 'main/pcgen/persistence/lst/DataTest.class' - include 'main/pcgen/persistence/lst/DataLoadTest.class' + include 'pcgen/persistence/lst/DataTest.class' + include 'pcgen/persistence/lst/DataLoadTest.class' } tasks.register("inttest", Test) { dependsOn jar + useJUnitPlatform() testClassesDirs = sourceSets.slowtest.output.classesDirs classpath = sourceSets.slowtest.runtimeClasspath forkEvery = 1 - include 'slowtest/pcgen/inttest/**/*Test.class' + include 'pcgen/inttest/**/*Test.class' } // Per-game-mode inttest variants. Each runs only the inttest classes for one @@ -723,10 +716,10 @@ tasks.register("inttest", Test) { ].each { taskName, gameDir -> tasks.register(taskName, Test) { dependsOn jar + useJUnitPlatform() testClassesDirs = sourceSets.slowtest.output.classesDirs classpath = sourceSets.slowtest.runtimeClasspath - forkEvery = 1 - include "slowtest/pcgen/inttest/${gameDir}/*Test.class" + include "pcgen/inttest/${gameDir}/*Test.class" } } @@ -835,32 +828,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 diff --git a/code/src/Src.iml b/code/src/Src.iml deleted file mode 100644 index d4ed0035a2e..00000000000 --- a/code/src/Src.iml +++ /dev/null @@ -1,186 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/code/src/java/module-info.java b/code/src/java/module-info.java index 2835005271a..f13e7925f0a 100644 --- a/code/src/java/module-info.java +++ b/code/src/java/module-info.java @@ -24,7 +24,6 @@ requires org.apache.commons.lang3; requires freemarker; - requires org.jdom2; requires net.sourceforge.argparse4j; requires org.controlsfx.controls; requires org.apache.xmlgraphics.fop.core; @@ -72,6 +71,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/core/doomsdaybook/CRRule.java b/code/src/java/pcgen/core/doomsdaybook/CRRule.java deleted file mode 100644 index ef5962c53df..00000000000 --- a/code/src/java/pcgen/core/doomsdaybook/CRRule.java +++ /dev/null @@ -1,63 +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 pcgen.core.doomsdaybook; - -import java.util.ArrayList; -import java.util.List; - -public class CRRule implements DataElement -{ - private final List retList = new ArrayList<>(); - - /** Creates a new instance of SpaceRule */ - public CRRule() - { - retList.add(new DataValue(" ")); - } - - @Override - public List getData() - { - return retList; - } - - @Override - public String getId() - { - return "\n"; - } - - @Override - public List getLastData() - { - return retList; - } - - @Override - public String getTitle() - { - return null; - } - - @Override - public int getWeight() - { - return 1; - } - -} diff --git a/code/src/java/pcgen/core/doomsdaybook/DDList.java b/code/src/java/pcgen/core/doomsdaybook/DDList.java deleted file mode 100644 index 5405cf24622..00000000000 --- a/code/src/java/pcgen/core/doomsdaybook/DDList.java +++ /dev/null @@ -1,166 +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 pcgen.core.doomsdaybook; - -import java.util.ArrayList; -import java.util.List; - -import gmgen.plugin.dice.Dice; - -public class DDList extends ArrayList implements DataElement -{ - private final List retList = new ArrayList<>(); - private String id; - private String title; - private final VariableHashMap allVars; - private int weight; - - /** - * Constructor - * @param allVars - * @param title - * @param id - */ - public DDList(VariableHashMap allVars, String title, String id) - { - this(allVars, title, id, 1); - } - - /** - * Constructor - * @param allVars - * @param title - * @param id - * @param weight - */ - private DDList(VariableHashMap allVars, String title, String id, int weight) - { - this.allVars = allVars; - this.title = title; - this.id = id; - this.weight = weight; - } - - @Override - public List getData() - { - retList.clear(); - - int rangeTop = getRange(); - int modifier; - - try - { - modifier = Integer.parseInt(allVars.getVal(getId() + "modifier")); - } - catch (NumberFormatException | variableException e) - { - modifier = 0; - } - - // Determine which entry to choose - Dice die = new Dice(1, rangeTop, 0); - int choice = die.roll(); - choice += modifier; - choice = (choice < 0) ? rangeTop : choice; - - //select the detail to return - int aWeight = 0; - - // Iterate through the list of choices until the weights (from each DataValue) - // are greater the num chosen as the 'choice' - for (WeightedDataValue chkValue : this) - { - int valueWeight = chkValue.getWeight(); - - if (valueWeight > 0) - { - aWeight += valueWeight; - - if (aWeight >= choice) - { - retList.add(chkValue); - - break; - } - } - } - - return retList; - } - - /** - * Set the id of the list - * @param id - */ - public void setId(String id) - { - this.id = id; - } - - @Override - public String getId() - { - return id; - } - - @Override - public List getLastData() - { - return retList; - } - - /** - * Get the range - * @return the range - */ - private int getRange() - { - int rangeTop = this.stream() - .mapToInt(WeightedDataValue::getWeight) - .sum(); - - if (rangeTop <= 0) - { //the die will nullpointer if it is not at least 1 - rangeTop = 1; - } - - return rangeTop; - } - - /** - * Set the title of the list - * @param title - */ - public void setTitle(String title) - { - this.title = title; - } - - @Override - public String getTitle() - { - return title; - } - - @Override - public int getWeight() - { - return weight; - } -} diff --git a/code/src/java/pcgen/core/doomsdaybook/DataElement.java b/code/src/java/pcgen/core/doomsdaybook/DataElement.java deleted file mode 100644 index 822fe97ede0..00000000000 --- a/code/src/java/pcgen/core/doomsdaybook/DataElement.java +++ /dev/null @@ -1,58 +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 pcgen.core.doomsdaybook; - -import java.util.List; - -public interface DataElement -{ - - /** - * Get Data. - * - * @return ArrayList - * @throws Exception the exception - */ - public List getData() throws Exception; - - /** - * Get id - * @return id - */ - public String getId(); - - /** - * Get last data. - * - * @return last data - * @throws Exception the exception - */ - public List getLastData() throws Exception; - - /** - * Get title - * @return title - */ - public String getTitle(); - - /** - * Get weight - * @return weight - */ - public int getWeight(); -} diff --git a/code/src/java/pcgen/core/doomsdaybook/DataElementComperator.java b/code/src/java/pcgen/core/doomsdaybook/DataElementComperator.java deleted file mode 100644 index 530dab15d1e..00000000000 --- a/code/src/java/pcgen/core/doomsdaybook/DataElementComperator.java +++ /dev/null @@ -1,33 +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 pcgen.core.doomsdaybook; - -import java.io.Serializable; -import java.util.Comparator; - -/** - * DataElementComperator - */ -public class DataElementComperator implements Comparator, Serializable -{ - @Override - public int compare(DataElement o1, DataElement o2) - { - return o1.getTitle().compareTo(o2.getTitle()); - } -} diff --git a/code/src/java/pcgen/core/doomsdaybook/HyphenRule.java b/code/src/java/pcgen/core/doomsdaybook/HyphenRule.java deleted file mode 100644 index 4953ca3a2a5..00000000000 --- a/code/src/java/pcgen/core/doomsdaybook/HyphenRule.java +++ /dev/null @@ -1,62 +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 pcgen.core.doomsdaybook; - -import java.util.ArrayList; -import java.util.List; - -public class HyphenRule implements DataElement -{ - private final List retList = new ArrayList<>(); - - /** Creates a new instance of SpaceRule */ - public HyphenRule() - { - retList.add(new DataValue("-")); - } - - @Override - public List getData() - { - return retList; - } - - @Override - public String getId() - { - return "-"; - } - - @Override - public List getLastData() - { - return retList; - } - - @Override - public String getTitle() - { - return null; - } - - @Override - public int getWeight() - { - return 1; - } -} diff --git a/code/src/java/pcgen/core/doomsdaybook/Operation.java b/code/src/java/pcgen/core/doomsdaybook/Operation.java deleted file mode 100644 index 97d10c9f922..00000000000 --- a/code/src/java/pcgen/core/doomsdaybook/Operation.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * RPGeneration - A role playing utility generate interesting things - * Copyright (C) 2002 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 pcgen.core.doomsdaybook; - -/** - * {@code Operation} encapsulates an action that can be performed - * on a variable. These are actions such as setting, adding or - * multiplying the variable's current value. - */ -class Operation implements Comparable -{ - /** The identifying key of the variable the operation is to affect. */ - private final String key; - /** The name of the operation. */ - private final String name; - /** The type of action to take on the variable. */ - private final String type; - /** The value to be used in the operation. */ - private final String value; - - /** - * Create a new Operation instance. - * - * @param type The type of action to take on the variable. - * @param key The identifying key of the variable the operation is to affect. - * @param value The value to be used in the operation. - * @param name The name of the operation. - */ - private Operation(String type, String key, String value, String name) - { - this.type = type; - this.key = key; - this.value = value; - this.name = name; - } - - /** - * Create a new unnamed Operation instance. - * - * @param type The type of action to take on the variable. - * @param key The identifying key of the variable the operation is to affect. - * @param value The value to be used in the operation. - */ - public Operation(String type, String key, String value) - { - this(type, key, value, ""); - } - - /** - * @return The current value of the key. - */ - public String getKey() - { - return key; - } - - /** - * @return The current value of the type. - */ - public String getType() - { - return type; - } - - /** - * @return The current value of the value. - */ - public String getValue() - { - return value; - } - - @Override - public int compareTo(Object obj) - { - String title = this.toString(); - String compared = obj.toString(); - - return title.compareTo(compared); - } - - @Override - public String toString() - { - return name; - } -} diff --git a/code/src/java/pcgen/core/doomsdaybook/Rule.java b/code/src/java/pcgen/core/doomsdaybook/Rule.java deleted file mode 100644 index 44a14f2a3a5..00000000000 --- a/code/src/java/pcgen/core/doomsdaybook/Rule.java +++ /dev/null @@ -1,126 +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 pcgen.core.doomsdaybook; - -import java.util.ArrayList; -import java.util.List; - -import pcgen.util.Logging; - -public class Rule extends ArrayList implements DataElement -{ - private final List retList = new ArrayList<>(); - private String id; - private String title; - private final VariableHashMap allVars; - private int weight; - - public Rule(VariableHashMap allVars, String title, String id, int weight) - { - this.allVars = allVars; - this.title = title; - this.id = id; - this.weight = weight; - } - - @Override - public List getData() throws Exception - { - retList.clear(); - - for (String key : this) - { - DataElement ele = allVars.getDataElement(key); - retList.addAll(ele.getData()); - } - - return retList; - } - - public void setId(String id) - { - this.id = id; - } - - @Override - public String getId() - { - return id; - } - - @Override - public List getLastData() throws Exception - { - retList.clear(); - - for (String key : this) - { - DataElement ele = allVars.getDataElement(key); - retList.addAll(ele.getLastData()); - } - - return retList; - } - - public void setTitle(String title) - { - this.title = title; - } - - @Override - public String getTitle() - { - return title; - } - - public void setWeight(int weight) - { - this.weight = weight; - } - - @Override - public int getWeight() - { - return weight; - } - - @Override - public String toString() - { - StringBuilder sb = new StringBuilder(); - - for (String key : this) - { - try - { - DataElement ele = allVars.getDataElement(key); - - if (ele.getTitle() != null) - { - sb.append("[").append(ele.getTitle()).append("] "); - } - } - catch (Exception e) - { - Logging.errorPrint(e.getMessage(), e); - } - } - - return sb.toString(); - } -} diff --git a/code/src/java/pcgen/core/doomsdaybook/RuleSet.java b/code/src/java/pcgen/core/doomsdaybook/RuleSet.java deleted file mode 100644 index 4da6d2583ce..00000000000 --- a/code/src/java/pcgen/core/doomsdaybook/RuleSet.java +++ /dev/null @@ -1,352 +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 pcgen.core.doomsdaybook; - -import java.util.ArrayList; -import java.util.List; - -import gmgen.plugin.dice.Dice; - -/** - * This class deals with RuleSets for Random name generation - * - */ -public class RuleSet extends ArrayList implements DataElement -{ - private final List retList = new ArrayList<>(); - private Rule retRule; - private String id; - private String title; - private String usage = "private"; - private final VariableHashMap allVars; - private int weight; - - /** - * Constructor - * - * @param allVars - * @param title - * @param id - * @param weight - */ - public RuleSet(VariableHashMap allVars, String title, String id, int weight) - { - this.allVars = allVars; - this.title = title; - this.id = id; - this.weight = weight; - } - - /** - * Constructor - * - * @param allVars - * @param title - * @param id - * @param usage - */ - public RuleSet(VariableHashMap allVars, String title, String id, String usage) - { - this(allVars, title, id, 1, usage); - } - - /** - * Constructor - * - * @param allVars - * @param title - * @param id - * @param weight - * @param usage - */ - private RuleSet(VariableHashMap allVars, String title, String id, int weight, String usage) - { - this.allVars = allVars; - this.title = title; - this.id = id; - this.weight = weight; - this.usage = usage; - } - - /** - * Get the data - * - * @return A list of data - * @throws Exception - */ - @Override - public List getData() throws Exception - { - retList.clear(); - - int rangeTop = getRange(); - int modifier; - - try - { - modifier = Integer.parseInt(allVars.getVal(getId() + "modifier")); - } - catch (Exception e) - { - modifier = 0; - } - - // Determine which entry to choose - Dice die = new Dice(1, rangeTop, 0); - int choice = die.roll(); - choice += modifier; - choice = (choice < 0) ? rangeTop : choice; - - //select the detail to return - int aWeight = 0; - - // Iterate through the list of choices until the weights (from each DataValue) - // are greater the num chosen as the 'choice' - for (String key : this) - { - DataElement chkValue = allVars.getDataElement(key); - int valueWeight = chkValue.getWeight(); - - if (valueWeight > 0) - { - aWeight += valueWeight; - - if (aWeight >= choice) - { - retList.addAll(chkValue.getData()); - - break; - } - } - } - - return retList; - } - - /** - * Set the id - * @param id - */ - public void setId(String id) - { - this.id = id; - } - - /** - * Get the id - * @return id - */ - @Override - public String getId() - { - return id; - } - - /** - * Get the last data - * @return the last list of data - */ - @Override - public List getLastData() - { - return retList; - } - - /** - * Get the last rule - * @return last rule - */ - public Rule getLastRule() - { - return retRule; - } - - /** - * Get the range. - * - * @return range - * @throws Exception When no entry exists for the supplied key. - */ - private int getRange() throws Exception - { - int rangeTop = 0; - - for (String key : this) - { - DataElement value = allVars.getDataElement(key); - rangeTop += value.getWeight(); - } - - if (rangeTop <= 0) - { //the die will nullpointer if it is not at least 1 - rangeTop = 1; - } - - return rangeTop; - } - - /** - * Get the rule. - * - * @return rule - * @throws Exception When no entry exists for the supplied key. - */ - public Rule getRule() throws Exception - { - int rangeTop = getRange(); - int modifier; - - try - { - modifier = Integer.parseInt(allVars.getVal(getId() + "modifier")); - } - catch (Exception e) - { - modifier = 0; - } - - // Determine which entry to choose - Dice die = new Dice(1, rangeTop, 0); - int choice = die.roll(); - choice += modifier; - - choice = (choice < 0) ? rangeTop : choice; - - //select the detail to return - int aWeight = 0; - - // Iterate through the list of choices until the weights (from each DataValue) - // are greater the num chosen as the 'choice' - for (String key : this) - { - Rule chkValue = (Rule) allVars.getDataElement(key); - int valueWeight = chkValue.getWeight(); - - if (valueWeight > 0) - { - aWeight += valueWeight; - - if (aWeight >= choice) - { - retRule = chkValue; - - return chkValue; - } - } - } - - return retRule; - } - - /** - * Get the rule given a choice - * - * @param choice - * @return Rule - * @throws Exception When no entry exists for the supplied key. - */ - public Rule getRule(int choice) throws Exception - { - //select the detail to return - int aWeight = 0; - - // Iterate through the list of choices until the weights (from each DataValue) - // are greater the num chosen as the 'choice' - for (String key : this) - { - Rule chkValue = (Rule) allVars.getDataElement(key); - int valueWeight = chkValue.getWeight(); - - if (valueWeight > 0) - { - aWeight += valueWeight; - - if (aWeight >= choice) - { - retRule = chkValue; - - break; - } - } - } - - return retRule; - } - - /** - * Set the title - * @param title - */ - public void setTitle(String title) - { - this.title = title; - } - - /** - * Get the title - * @return title - */ - @Override - public String getTitle() - { - return title; - } - - /** - * Set the usage - * @param usage - */ - public void setUsage(String usage) - { - this.usage = usage; - } - - /** - * Get the usage - * @return usage - */ - public String getUsage() - { - return usage; - } - - /** - * Set the weight - * @param weight - */ - public void setWeight(int weight) - { - this.weight = weight; - } - - /** - * Get the weight - * @return weight - */ - @Override - public int getWeight() - { - return weight; - } - - @Override - public String toString() - { - return getTitle(); - } -} diff --git a/code/src/java/pcgen/core/doomsdaybook/SpaceRule.java b/code/src/java/pcgen/core/doomsdaybook/SpaceRule.java deleted file mode 100644 index de286950017..00000000000 --- a/code/src/java/pcgen/core/doomsdaybook/SpaceRule.java +++ /dev/null @@ -1,62 +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 pcgen.core.doomsdaybook; - -import java.util.ArrayList; -import java.util.List; - -public class SpaceRule implements DataElement -{ - private final List retList = new ArrayList<>(); - - /** Creates a new instance of SpaceRule */ - public SpaceRule() - { - retList.add(new DataValue(" ")); - } - - @Override - public List getData() - { - return retList; - } - - @Override - public String getId() - { - return " "; - } - - @Override - public List getLastData() - { - return retList; - } - - @Override - public String getTitle() - { - return null; - } - - @Override - public int getWeight() - { - return 1; - } -} diff --git a/code/src/java/pcgen/core/doomsdaybook/VariableHashMap.java b/code/src/java/pcgen/core/doomsdaybook/VariableHashMap.java deleted file mode 100644 index 64664879e97..00000000000 --- a/code/src/java/pcgen/core/doomsdaybook/VariableHashMap.java +++ /dev/null @@ -1,337 +0,0 @@ -/* - * RPGeneration - A role playing utility generate interesting things - * Copyright (C) 2002 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 pcgen.core.doomsdaybook; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -/** - * {@code VariableHashMap} is a collection of variables (key/value - * pairs) and DataElements optimised for quick lookup and updating. Note - * there are three structures here, the principle map of variables, a - * secondary map of DataElement objects and a list of Operation objects. - */ -public class VariableHashMap extends HashMap -{ - private final Collection initialize = new ArrayList<>(); - private final Map dataElements; - - /** Creates a new instance of VariableHashMap */ - public VariableHashMap() - { - this.dataElements = new HashMap<>(); - } - - /** - * Retrieve the data element for the given key. If no entry is - * present for the key an exception will be thrown. - * - * @param key The key of the element to be retrieved. - * @return The data element. - * @throws Exception When no entry exists for the supplied key. - */ - public DataElement getDataElement(String key) throws Exception - { - DataElement de = dataElements.get(key); - - if (de == null) - { - throw new Exception("Data Set " + key + " Does Not Exist"); - } - - return de; - } - - /** - * Retrieve the variable value for the given key. If no entry is - * present for the key an exception will be thrown. - * - * @param key The key of the variable to be retrieved. - * @return The value fo the variable. - * @throws variableException When no entry exists for the supplied key. - */ - String getVal(String key) throws variableException - { - String value = get(key); - - if (value == null) - { - throw new variableException("Variable " + key + " does not exist, cannot get value"); - } - - return value; - } - - /** - * Set the value for an existing variable. If no entry is - * present for the key, an exception will be thrown. - * - * @param key The key of the variable to be set. - * @param value The new value for the variable - * @throws variableException When no entry exists for the supplied key. - */ - private void setVar(String key, String value) throws variableException - { - if (get(key) == null) - { - throw new variableException("Variable " + key + " does not exist, cannot set value"); - } - - put(key, value); - } - - /** - * Add a new DataElement to the map of DataElement objects. - * The dataElement's Id will be used as the key for the map. - * - * @param dataElement The DataElement object to be added. - */ - public void addDataElement(DataElement dataElement) - { - String key = dataElement.getId(); - dataElements.put(key, dataElement); - } - - /** - * Add an amount to an existing variable. If no entry is - * present for the key, an exception will be thrown. - * - * @param key The key of the variable to be updated. - * @param add The amount to add to the variable. - * @return The new value of the variable. - * @throws variableException When no entry exists for the supplied key. - */ - private String addVar(String key, int add) throws variableException - { - String value = get(key); - - if (value == null) - { - throw new variableException("Variable " + key + " does not exist, cannot add to value"); - } - - int val; - - if (value.isEmpty()) - { - val = 0; - } - else - { - val = Integer.parseInt(get(key)); - } - - return put(key, Integer.toString(val + add)); - } - - /** - * Divide the value of an existing variable by an amount. If no entry is - * present for the key, an exception will be thrown. NB: Integer divison - * is used. - * - * @param key The key of the variable to be updated. - * @param divide The amount to divide the variable's value by. - * @return The new value of the variable. - * @throws variableException When no entry exists for the supplied key. - */ - private String divideVar(String key, int divide) throws variableException - { - String value = get(key); - - if (value == null) - { - throw new variableException("Variable " + key + " does not exist, cannot divide by value"); - } - - int val; - - if (value.isEmpty()) - { - val = 0; - } - else - { - val = Integer.parseInt(get(key)); - } - - return put(key, Integer.toString(val / divide)); - } - - /** - * Perform the supplied Operations in sequence. - * - * @param ops The Operations to be performed. - * @throws variableException When no entry exists for an Operation's key. - */ - private void doOperation(Collection ops) throws variableException - { - for (Operation op : ops) - { - doOperation(op); - } - } - - /** - * Perform the Operation. The Operation defines, the action, the variable - * to be affected and the value to be used in the operation. If no entry is - * present for the key expected by the Operation, an exception will be - * thrown. - * - * @param op The Operation to be performed. - * @throws variableException When no entry exists for the Operation's key. - */ - private void doOperation(Operation op) throws variableException - { - String type = op.getType(); - String key = op.getKey(); - String value = op.getValue(); - - switch (type) - { - case "Set" -> { - value = parse(value); - setVar(key, value); - } - case "Add" -> { - int val = Integer.parseInt(parse(value)); - addVar(key, val); - break; - } - case "Subtract" -> { - int val = Integer.parseInt(parse(value)); - subtractVar(key, val); - break; - } - case "Multiply" -> { - int val = Integer.parseInt(parse(value)); - multiplyVar(key, val); - break; - } - case "Divide" -> { - int val = Integer.parseInt(parse(value)); - divideVar(key, val); - break; - } - } - } - - /** - * Perform the initialization Operations in sequence. - * - * @throws variableException When no entry exists for an Operation's key. - */ - public void initialize() throws variableException - { - doOperation(initialize); - } - - /** - * Multiply the value of an existing variable by an amount. If no entry is - * present for the key, an exception will be thrown. NB: Integer divison - * is used. - * - * @param key The key of the variable to be updated. - * @param multiply The amount to multiply the variable's value by. - * @return The new value of the variable. - * @throws variableException When no entry exists for the supplied key. - */ - private String multiplyVar(String key, int multiply) throws variableException - { - String value = get(key); - - if (value == null) - { - throw new variableException("Variable " + key + " does not exist, cannot multiply by value"); - } - - int val; - - if (value.isEmpty()) - { - val = 0; - } - else - { - val = Integer.parseInt(get(key)); - } - - return put(key, Integer.toString(val * multiply)); - } - - /** - * Parse a value substituting the first variable referred to as - * ${varname} with the variable's value. - * - * @param val The value to be parsed - * @return The parsed value. - */ - private String parse(String val) - { - String retString = val; - - if (val.matches("\\$\\{.*?}.*")) - { - String var = val.substring(val.indexOf("${") + 2, val.indexOf('}')); - String value = get(var); - - if (value == null) - { - value = ""; - } - - retString = val.replaceFirst("\\$\\{.*?}", value); - } - - return retString; - } - - /** - * Subtract an amount to an existing variable. If no entry is - * present for the key, an exception will be thrown. - * - * @param key The key of the variable to be updated. - * @param subtract The amount to subtract from the variable. - * @return The new value of the variable. - * @throws variableException When no entry exists for the supplied key. - */ - private String subtractVar(String key, int subtract) throws variableException - { - String value = get(key); - - if (value == null) - { - throw new variableException("Variable " + key + " does not exist, cannot subtract from value"); - } - - int val; - - if (value.isEmpty()) - { - val = 0; - } - else - { - val = Integer.parseInt(get(key)); - } - - return put(key, Integer.toString(val - subtract)); - } -} diff --git a/code/src/java/pcgen/core/doomsdaybook/variableException.java b/code/src/java/pcgen/core/doomsdaybook/variableException.java deleted file mode 100644 index bdbe7fbc0be..00000000000 --- a/code/src/java/pcgen/core/doomsdaybook/variableException.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * RPGeneration - A role playing utility generate interesting things - * Copyright (C) 2002 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 pcgen.core.doomsdaybook; - -/** - * {@code variableException} is an exception raised when a problem - * occurs when processing a variable. - */ -class variableException extends Exception -{ - - /** - * Constructs an instance of {@code variableException} with the specified detail message. - * @param msg the detail message. - */ - variableException(String msg) - { - super(msg); - } -} 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/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..cffe6ef6b7b --- /dev/null +++ b/code/src/java/pcgen/core/namegen/NameGenData.java @@ -0,0 +1,51 @@ +/* + * 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. References between rules, rulesets, and lists are resolved + * at load time, so generation reads only from these maps. + * + *

{@link #unresolvedReferences()} lists every {@code GETLIST}/{@code GETRULE} + * whose target id was not present after the load completed. The loader + * skips such parts (matching the legacy engine's silent behaviour); the + * list lets callers and tests detect data-file bugs. + */ +public record NameGenData( + Map lists, + Map rulesets, + Map> categories, + List unresolvedReferences) +{ + public NameGenData + { + lists = Map.copyOf(lists); + rulesets = Map.copyOf(rulesets); + categories = Map.copyOf(categories); + unresolvedReferences = List.copyOf(unresolvedReferences); + } + + /** A {@code GETLIST}/{@code GETRULE} whose target id wasn't found. */ + public record UnresolvedReference(Kind kind, String targetId) + { + public enum Kind + { + GETLIST, GETRULE + } + } +} 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..0891f43c12d --- /dev/null +++ b/code/src/java/pcgen/core/namegen/NameGenDataLoader.java @@ -0,0 +1,540 @@ +/* + * 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.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +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; + +/** + * Loads random-name XML files from a directory into a {@link NameGenData} + * snapshot, keeping the data model independent of any UI toolkit. + * + *

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

Loading is two-pass: first parse every file and gather raw + * {@code }/{@code } elements with their ids, then resolve + * cross-references and build the immutable model. The two passes let + * rulesets refer to each other freely without forward-declaration issues. + */ +public final class NameGenDataLoader +{ + private static final String ATTR_TITLE = "title"; + private static final String ATTR_IDREF = "idref"; + private static final String TAG_GETLIST = "GETLIST"; + private static final String TAG_GETRULE = "GETRULE"; + + 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 or any file + * fails to parse + */ + 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); + } + + DocumentBuilder builder = newDocumentBuilder(); + builder.setEntityResolver(dtdResolver(dataDir)); + + Map rawLists = new LinkedHashMap<>(); + Map rawRuleSets = new LinkedHashMap<>(); + Map> rawCategories = new LinkedHashMap<>(); + + // Pass 1: parse each file, harvest raw LIST and RULESET elements. + for (File dataFile : dataFiles) + { + RawFile rf = parseOne(dataFile, builder); + for (Element list : rf.lists) + { + rawLists.put(list.getAttribute("id"), list); + } + for (Element ruleSet : rf.ruleSets) + { + String id = ruleSet.getAttribute("id"); + rawRuleSets.put(id, new RawRuleSet(ruleSet, id)); + } + } + + // Pass 2a: build NameList records (no cross-refs). + Map lists = rawLists.values().stream() + .map(NameGenDataLoader::buildList) + .collect(Collectors.toMap( + NameList::id, + nl -> nl, + (a, b) -> b, + LinkedHashMap::new)); + + // Pass 2b: build RuleSet records. RuleSetRef parts share a single + // map instance that gets populated as we go, so a ruleset can + // reference any other ruleset regardless of file order. + Map rulesets = new LinkedHashMap<>(); + List unresolved = new ArrayList<>(); + for (RawRuleSet raw : rawRuleSets.values()) + { + RuleSet rs = buildRuleSet(raw, lists, rulesets, rawRuleSets, unresolved); + rulesets.put(raw.id, rs); + collectCategories(raw.element, raw.id, rawCategories); + } + + // Pass 3: resolve category ids to RuleSet records. Categories + // declared on rulesets that ultimately failed to build are skipped. + Map> categories = rawCategories.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> e.getValue().stream() + .map(rulesets::get) + .filter(Objects::nonNull) + .toList(), + (a, b) -> b, + LinkedHashMap::new)); + + return new NameGenData(lists, rulesets, categories, unresolved); + } + + private static RawFile parseOne(File dataFile, DocumentBuilder builder) throws IOException + { + try + { + Document nameSet = builder.parse(dataFile); + DocumentType dt = nameSet.getDoctype(); + if (dt == null || !"GENERATOR".equals(dt.getName())) + { + return RawFile.EMPTY; + } + Element generator = nameSet.getDocumentElement(); + return new RawFile( + childElements(generator, "LIST"), + childElements(generator, "RULESET")); + } catch (SAXException | NumberFormatException e) + { + Logging.errorPrint("Failed to parse " + dataFile.getName(), e); + throw new IOException("XML error in file " + dataFile.getName(), e); + } + } + + /** + * Lazy-loader phase 1. Parses a single file's DOM, registers every + * {@code } from this file into the live {@code lists} map, and + * returns the raw ruleset elements together with the idrefs the file + * reaches into. The lazy caller demand-parses the owners of any + * not-yet-loaded ids before invoking + * {@link #buildRuleSetsForLazy(LazyFilePrepared, Map, Map, List, UnaryOperator)}. + */ + static LazyFilePrepared prepareFileForLazy(File dataFile, File dataDir, + Map lists) throws IOException + { + DocumentBuilder builder = newDocumentBuilder(); + builder.setEntityResolver(dtdResolver(dataDir)); + RawFile raw = parseOne(dataFile, builder); + + for (Element listEl : raw.lists) + { + NameList nl = buildList(listEl); + lists.put(nl.id(), nl); + } + + Map localRawRuleSets = raw.ruleSets.stream() + .collect(Collectors.toMap( + rsEl -> rsEl.getAttribute("id"), + rsEl -> new RawRuleSet(rsEl, rsEl.getAttribute("id")), + (a, b) -> b, + LinkedHashMap::new)); + + Set referencedListIds = new LinkedHashSet<>(); + Set referencedRuleSetIds = new LinkedHashSet<>(); + for (Element rsEl : raw.ruleSets) + { + for (Element ruleEl : childElements(rsEl, "RULE")) + { + for (Element child : childElements(ruleEl)) + { + switch (child.getTagName()) + { + case TAG_GETLIST -> referencedListIds.add(child.getAttribute(ATTR_IDREF)); + case TAG_GETRULE -> referencedRuleSetIds.add(child.getAttribute(ATTR_IDREF)); + default -> + { + // nothing + } + } + } + } + } + + return new LazyFilePrepared(localRawRuleSets, referencedListIds, referencedRuleSetIds); + } + + /** + * Lazy-loader phase 2. Builds and registers {@link RuleSet} records for + * a file whose phase-1 result was returned by {@link + * #prepareFileForLazy(File, File, Map)}. + * + *

{@code crossFileRuleSetTitle} is consulted whenever a {@code GETRULE} + * points at an id not declared in the current file. Returning {@code null} + * from the resolver records an unresolved reference; returning a title + * string produces a {@link RulePart.RuleSetRef} that will resolve through + * the shared {@code rulesets} map at generation time. + */ + static void buildRuleSetsForLazy(LazyFilePrepared prepared, + Map lists, + Map rulesets, + List unresolved, + UnaryOperator crossFileRuleSetTitle) + { + for (RawRuleSet rrs : prepared.localRawRuleSets().values()) + { + RuleSet rs = buildRuleSetLazy(rrs, lists, rulesets, + prepared.localRawRuleSets(), unresolved, crossFileRuleSetTitle); + rulesets.put(rrs.id, rs); + } + } + + private static RuleSet buildRuleSetLazy(RawRuleSet raw, + Map lists, + Map rulesets, + Map localRawRuleSets, + List unresolved, + UnaryOperator crossFileRuleSetTitle) + { + List rules = childElements(raw.element, "RULE").stream() + .map(rule -> buildRuleLazy(rule, lists, rulesets, + localRawRuleSets, unresolved, crossFileRuleSetTitle)) + .toList(); + return new RuleSet(raw.id, + raw.element.getAttribute(ATTR_TITLE), + raw.element.getAttribute("usage"), + rules); + } + + private static Rule buildRuleLazy(Element rule, + Map lists, + Map rulesets, + Map localRawRuleSets, + List unresolved, + UnaryOperator crossFileRuleSetTitle) + { + List parts = new ArrayList<>(); + StringBuilder label = new StringBuilder(); + for (Element child : childElements(rule)) + { + RulePart part = switch (child.getTagName()) + { + case TAG_GETLIST -> resolveListRef(child.getAttribute(ATTR_IDREF), lists, unresolved); + case TAG_GETRULE -> resolveRuleSetRefLazy(child.getAttribute(ATTR_IDREF), + rulesets, localRawRuleSets, unresolved, crossFileRuleSetTitle); + case "SPACE" -> RulePart.Literal.SPACE; + case "HYPHEN" -> RulePart.Literal.HYPHEN; + case "CR" -> RulePart.Literal.CR; + default -> null; + }; + if (part != null) + { + parts.add(part); + label.append(part.label()); + } + } + return new Rule(parseWeight(rule), label.toString(), parts); + } + + private static RulePart resolveRuleSetRefLazy(String idref, + Map rulesets, + Map localRawRuleSets, + List unresolved, + UnaryOperator crossFileRuleSetTitle) + { + // Same-file target: title from the local raw element so it works + // even when the target hasn't been built yet. + RawRuleSet local = localRawRuleSets.get(idref); + if (local != null) + { + return new RulePart.RuleSetRef(idref, local.element.getAttribute(ATTR_TITLE), rulesets); + } + String title = crossFileRuleSetTitle.apply(idref); + if (title == null) + { + unresolved.add(new NameGenData.UnresolvedReference( + NameGenData.UnresolvedReference.Kind.GETRULE, idref)); + return null; + } + return new RulePart.RuleSetRef(idref, title, rulesets); + } + + /** + * Phase-1 output for {@link #prepareFileForLazy(File, File, Map)}. + */ + record LazyFilePrepared(Map localRawRuleSets, + Set referencedListIds, + Set referencedRuleSetIds) + { + } + + private static DocumentBuilder newDocumentBuilder() throws IOException + { + try + { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + // XXE hardening: data files are local but the parser shouldn't fetch external entities or evaluate + // parameter entities. We keep external-DTD loading on so generator.dtd still resolves + // through the EntityResolver. + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + return factory.newDocumentBuilder(); + } catch (ParserConfigurationException e) + { + throw new IOException("Cannot create XML parser", e); + } + } + + private static NameList buildList(Element list) + { + String id = list.getAttribute("id"); + String title = list.getAttribute(ATTR_TITLE); + List values = new ArrayList<>(); + for (Element child : childElements(list, "VALUE")) + { + WeightedDataValue dv = new WeightedDataValue(directText(child), parseWeight(child)); + childElements(child, "SUBVALUE").forEach(sub -> + dv.addSubValue(sub.getAttribute("type"), directText(sub))); + values.add(dv); + } + return new NameList(id, title, values); + } + + private static RuleSet buildRuleSet(RawRuleSet raw, + Map lists, + Map rulesets, + Map rawRuleSets, + List unresolved) + { + List rules = childElements(raw.element, "RULE").stream() + .map(rule -> buildRule(rule, lists, rulesets, rawRuleSets, unresolved)) + .toList(); + return new RuleSet(raw.id, + raw.element.getAttribute(ATTR_TITLE), + raw.element.getAttribute("usage"), + rules); + } + + private static Rule buildRule(Element rule, + Map lists, + Map rulesets, + Map rawRuleSets, + List unresolved) + { + List parts = new ArrayList<>(); + StringBuilder label = new StringBuilder(); + for (Element child : childElements(rule)) + { + RulePart part = switch (child.getTagName()) + { + case TAG_GETLIST -> resolveListRef(child.getAttribute(ATTR_IDREF), lists, unresolved); + case TAG_GETRULE -> + resolveRuleSetRef(child.getAttribute(ATTR_IDREF), rulesets, rawRuleSets, unresolved); + case "SPACE" -> RulePart.Literal.SPACE; + case "HYPHEN" -> RulePart.Literal.HYPHEN; + case "CR" -> RulePart.Literal.CR; + default -> null; + }; + if (part != null) + { + parts.add(part); + label.append(part.label()); + } + } + return new Rule(parseWeight(rule), label.toString(), parts); + } + + private static RulePart resolveListRef(String idref, + Map lists, + List unresolved) + { + NameList target = lists.get(idref); + if (target == null) + { + unresolved.add(new NameGenData.UnresolvedReference( + NameGenData.UnresolvedReference.Kind.GETLIST, idref)); + return null; + } + return new RulePart.ListRef(target); + } + + private static RulePart resolveRuleSetRef(String idref, + Map rulesets, + Map rawRuleSets, + List unresolved) + { + RawRuleSet raw = rawRuleSets.get(idref); + if (raw == null) + { + unresolved.add(new NameGenData.UnresolvedReference( + NameGenData.UnresolvedReference.Kind.GETRULE, idref)); + return null; + } + // Title comes from the raw element so forward references work + // before the target ruleset has been built. + String title = raw.element.getAttribute(ATTR_TITLE); + return new RulePart.RuleSetRef(idref, title, rulesets); + } + + private static void collectCategories(Element ruleSet, String id, + Map> rawCategories) + { + for (Element category : childElements(ruleSet, "CATEGORY")) + { + rawCategories.computeIfAbsent(category.getAttribute(ATTR_TITLE), k -> new ArrayList<>()).add(id); + } + } + + private static List childElements(Element parent) + { + NodeList nodes = parent.getChildNodes(); + return IntStream.range(0, nodes.getLength()) + .mapToObj(nodes::item) + .filter(n -> n.getNodeType() == Node.ELEMENT_NODE) + .map(Element.class::cast) + .toList(); + } + + /** + * Reads the {@code weight} attribute and defaults to 1 when absent or + * blank, matching the DTD's {@code weight CDATA "1"} default. The + * project uses a non-validating parser, so DTD-defaulted attributes + * arrive here as {@code ""} rather than {@code "1"}. + */ + private static int parseWeight(Element element) + { + String raw = element.getAttribute("weight"); + if (raw.isBlank()) + { + return 1; + } + return Integer.parseInt(raw.trim()); + } + + private static List childElements(Element parent, String tagName) + { + NodeList nodes = parent.getChildNodes(); + return IntStream.range(0, nodes.getLength()) + .mapToObj(nodes::item) + .filter(n -> n.getNodeType() == Node.ELEMENT_NODE) + .map(Element.class::cast) + .filter(e -> tagName.equals(e.getTagName())) + .toList(); + } + + /** + * 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"} — {@code Element.getTextContent()} + * would return {@code "Donn"} concatenated with the subvalue's text. + */ + 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(); + } + + /** + * Parsed DOM element + id, captured in pass 1 for use in pass 2. + */ + record RawRuleSet(Element element, String id) + { + } + + /** + * Per-file pass-1 result: the LIST and RULESET elements harvested. + */ + private record RawFile(List lists, List ruleSets) + { + static final RawFile EMPTY = new RawFile(List.of(), List.of()); + } + + /** + * Returns an {@link EntityResolver} that resolves {@code generator.dtd} + * from the given directory rather than the network. Other system ids are + * delegated to the parser's default resolution. + */ + private static EntityResolver dtdResolver(File parent) + { + return (publicId, systemId) -> + { + if (systemId == null || !systemId.endsWith("generator.dtd")) + { + return null; + } + File dtd = new File(parent, "generator.dtd"); + try + { + return new InputSource(Files.newInputStream(dtd.toPath())); + } catch (IOException e) + { + Logging.errorPrint("Cannot open " + dtd, e); + return null; + } + }; + } +} diff --git a/code/src/java/pcgen/core/namegen/NameGenIndex.java b/code/src/java/pcgen/core/namegen/NameGenIndex.java new file mode 100644 index 00000000000..f287274840a --- /dev/null +++ b/code/src/java/pcgen/core/namegen/NameGenIndex.java @@ -0,0 +1,324 @@ +/* + * 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. + */ +package pcgen.core.namegen; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.xml.XMLConstants; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.apache.commons.lang3.StringUtils; + +import pcgen.util.Logging; + +/** + * Lightweight pre-scan of the random-name XML directory. Reads only top-level + * {@code } / {@code } attributes and the {@code } + * children of each ruleset — never the {@code } or {@code } + * bodies — so it is dramatically cheaper than a full DOM parse. + * + *

Drives the lazy-load path: at startup the UI only needs to know which + * categories exist and which rulesets/lists each file declares. The bodies + * are parsed on demand by {@link NameGenLazyData} once the user picks a + * category. + */ +public final class NameGenIndex +{ + private static final String TAG_RULESET = "RULESET"; + private static final String TAG_CATEGORY = "CATEGORY"; + private static final String TAG_LIST = "LIST"; + private static final String ATTR_ID = "id"; + private static final String ATTR_TITLE = "title"; + private static final String ATTR_USAGE = "usage"; + + private final Map rulesetsById; + private final Map listIdToFile; + private final Map> rulesetIdsByCategory; + + private NameGenIndex(Map rulesetsById, + Map listIdToFile, + Map> rulesetIdsByCategory) + { + this.rulesetsById = Map.copyOf(rulesetsById); + this.listIdToFile = Map.copyOf(listIdToFile); + this.rulesetIdsByCategory = unmodifiableDeep(rulesetIdsByCategory); + } + + /** + * Scan every {@code *.xml} file in {@code dataDir} for ruleset/list/ + * category metadata. Returns an empty index if no files match. + * + * @throws IOException if {@code dataDir} is not a directory or any file + * fails to scan + */ + public static NameGenIndex scan(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); + } + Arrays.sort(dataFiles); + + XMLInputFactory factory = newSecureInputFactory(); + Map rulesetsById = new LinkedHashMap<>(); + Map listIdToFile = new LinkedHashMap<>(); + Map> rulesetIdsByCategory = new LinkedHashMap<>(); + + for (File file : dataFiles) + { + scanOne(file, factory, rulesetsById, listIdToFile, rulesetIdsByCategory); + } + return new NameGenIndex(rulesetsById, listIdToFile, rulesetIdsByCategory); + } + + /** All ruleset ids declared anywhere, mapped to their metadata. */ + public Map rulesetsById() + { + return rulesetsById; + } + + /** Owning file for a {@code }, or {@code null} if unknown. */ + public File fileForList(String listId) + { + return listIdToFile.get(listId); + } + + /** Owning file for a {@code }, or {@code null} if unknown. */ + public File fileForRuleset(String rulesetId) + { + RuleSetMeta meta = rulesetsById.get(rulesetId); + return meta == null ? null : meta.file(); + } + + /** + * Ordered ruleset ids declared under each {@code }. + * Order matches scan order (file alphabetical, then declaration order). + */ + public Map> rulesetIdsByCategory() + { + return rulesetIdsByCategory; + } + + /** + * Per-ruleset metadata gathered without parsing rule bodies: enough to + * populate the UI's category/title/gender pickers. + */ + public record RuleSetMeta(File file, String id, String title, String usage, + List categoryTitles) + { + public RuleSetMeta + { + categoryTitles = List.copyOf(categoryTitles); + } + } + + private static void scanOne(File file, XMLInputFactory factory, + Map rulesetsById, + Map listIdToFile, + Map> rulesetIdsByCategory) throws IOException + { + try (InputStream in = new FileInputStream(file)) + { + // Pass a systemId so the parser can resolve the DOCTYPE's + // relative SYSTEM "generator.dtd" against the data file's URL. + XMLStreamReader reader = factory.createXMLStreamReader( + file.toURI().toString(), in); + try + { + scanStream(reader, file, rulesetsById, listIdToFile, rulesetIdsByCategory); + } + finally + { + reader.close(); + } + } + catch (XMLStreamException e) + { + Logging.errorPrint("Failed to pre-scan " + file.getName(), e); + throw new IOException("XML error scanning " + file.getName(), e); + } + } + + /** + * Drives the StAX state machine over one document. The body of every + * {@code } is skipped via a depth counter on {@link ScanState} + * rather than parsed; only attribute headers on {@code }, + * {@code }, and {@code } matter to the index. + */ + private static void scanStream(XMLStreamReader reader, File file, + Map rulesetsById, + Map listIdToFile, + Map> rulesetIdsByCategory) throws XMLStreamException + { + ScanState state = new ScanState(file, rulesetsById, listIdToFile, rulesetIdsByCategory); + while (reader.hasNext()) + { + int event = reader.next(); + if (event == XMLStreamConstants.START_ELEMENT) + { + state.handleStart(reader); + } + else if (event == XMLStreamConstants.END_ELEMENT) + { + state.handleEnd(reader.getLocalName()); + } + } + } + + /** + * Streaming-scan accumulator. Holds the in-flight {@code }'s + * attributes and a depth counter for skipping {@code } bodies, and + * commits a {@link RuleSetMeta} record on each {@code }. + */ + private static final class ScanState + { + private final File file; + private final Map rulesetsById; + private final Map listIdToFile; + private final Map> rulesetIdsByCategory; + + private String currentRulesetId; + private String currentRulesetTitle; + private String currentRulesetUsage; + private List currentCategories; + private int listDepth; + + ScanState(File file, + Map rulesetsById, + Map listIdToFile, + Map> rulesetIdsByCategory) + { + this.file = file; + this.rulesetsById = rulesetsById; + this.listIdToFile = listIdToFile; + this.rulesetIdsByCategory = rulesetIdsByCategory; + } + + void handleStart(XMLStreamReader reader) + { + if (listDepth > 0) + { + // Inside a ; ignore everything until close. + listDepth++; + return; + } + switch (reader.getLocalName()) + { + case TAG_RULESET -> beginRuleSet(reader); + case TAG_CATEGORY -> addCategory(reader); + case TAG_LIST -> beginList(reader); + default -> + { + // We don't care about RULE/GETLIST/etc. for the index. + } + } + } + + void handleEnd(String localName) + { + if (listDepth > 0) + { + listDepth--; + return; + } + if (TAG_RULESET.equals(localName) && currentRulesetId != null) + { + commitRuleSet(); + } + } + + private void beginRuleSet(XMLStreamReader reader) + { + currentRulesetId = reader.getAttributeValue(null, ATTR_ID); + currentRulesetTitle = StringUtils.defaultString(reader.getAttributeValue(null, ATTR_TITLE)); + currentRulesetUsage = StringUtils.defaultString(reader.getAttributeValue(null, ATTR_USAGE)); + currentCategories = new ArrayList<>(); + } + + private void addCategory(XMLStreamReader reader) + { + if (currentCategories == null) + { + return; + } + String title = reader.getAttributeValue(null, ATTR_TITLE); + if (title != null) + { + currentCategories.add(title); + } + } + + private void beginList(XMLStreamReader reader) + { + String id = reader.getAttributeValue(null, ATTR_ID); + if (id != null) + { + listIdToFile.put(id, file); + } + listDepth = 1; + } + + private void commitRuleSet() + { + RuleSetMeta meta = new RuleSetMeta(file, currentRulesetId, + currentRulesetTitle, currentRulesetUsage, + currentCategories); + rulesetsById.put(currentRulesetId, meta); + for (String cat : currentCategories) + { + rulesetIdsByCategory + .computeIfAbsent(cat, k -> new ArrayList<>()) + .add(currentRulesetId); + } + currentRulesetId = null; + currentRulesetTitle = null; + currentRulesetUsage = null; + currentCategories = null; + } + } + + private static XMLInputFactory newSecureInputFactory() + { + XMLInputFactory factory = XMLInputFactory.newInstance(); + // XXE hardening. The data files declare so we must allow `file` access for the local DTD, + // but not network access. External entity references stay disabled. + factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false); + factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "file"); + return factory; + } + + private static Map> unmodifiableDeep(Map> src) + { + return src.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> List.copyOf(e.getValue()), + (a, b) -> b, + LinkedHashMap::new)); + } +} diff --git a/code/src/java/pcgen/core/namegen/NameGenLazyData.java b/code/src/java/pcgen/core/namegen/NameGenLazyData.java new file mode 100644 index 00000000000..504168cc2e9 --- /dev/null +++ b/code/src/java/pcgen/core/namegen/NameGenLazyData.java @@ -0,0 +1,257 @@ +/* + * 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. + */ +package pcgen.core.namegen; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * On-demand backend that mirrors what {@link NameGenData} exposes but only + * parses each XML file when one of its rulesets is actually used. Built on + * top of {@link NameGenIndex} (cheap per-file pre-scan) and the per-file + * full-parse helpers in {@link NameGenDataLoader}. + * + *

A {@link RuleSet} returned to a caller is always fully materialised + * (its {@link RuleSet#rules() rules} list is real, not a stub). The {@code + * GETRULE} cross-references it contains keep working through + * {@link RulePart.RuleSetRef}'s shared map; that map grows as more files are + * parsed, so a ruleset that today references a not-yet-loaded ruleset will + * resolve once that target's owning file is parsed. + * + *

Not thread-safe. The UI calls into this serially on the FX thread. + */ +public final class NameGenLazyData +{ + private final File dataDir; + private final NameGenIndex index; + + // Live, mutable accumulators populated as files are parsed. + private final Map lists = new HashMap<>(); + private final Map rulesets = new HashMap<>(); + private final List unresolved = new ArrayList<>(); + private final Set parsedFiles = new LinkedHashSet<>(); + private final Set inProgressFiles = new LinkedHashSet<>(); + + private NameGenLazyData(File dataDir, NameGenIndex index) + { + this.dataDir = dataDir; + this.index = index; + } + + /** + * Build a lazy data layer over {@code dataDir}. Performs the StAX + * pre-scan immediately; XML bodies are not parsed until needed. + */ + public static NameGenLazyData open(File dataDir) throws IOException + { + NameGenIndex index = NameGenIndex.scan(dataDir); + return new NameGenLazyData(dataDir, index); + } + + /** + * Sorted display category titles, exactly as a UI combo would show. + * Reads only index metadata — no XML bodies parsed. + */ + public List categoryTitles() + { + return List.copyOf(index.rulesetIdsByCategory().keySet()); + } + + /** + * Ruleset metadata for every ruleset declared under {@code category}. + * Reads index only — no parse triggered. Returns an empty list if the + * category is unknown. + */ + public List rulesetMetaFor(String category) + { + return index.rulesetIdsByCategory().getOrDefault(category, List.of()).stream() + .map(index.rulesetsById()::get) + .filter(Objects::nonNull) + .toList(); + } + + /** + * Returns whether the ruleset id {@code candidate} is also declared + * under the {@code Sex: } bucket. Reads index only — no parse + * triggered. + */ + public boolean isInGenderBucket(String candidateId, String gender) + { + List ids = index.rulesetIdsByCategory() + .getOrDefault("Sex: " + gender, List.of()); + return ids.contains(candidateId); + } + + /** + * Genders declared for ruleset id {@code rulesetId}. Reads index only. + */ + public List gendersForRuleset(String rulesetId) + { + return index.rulesetIdsByCategory().entrySet().stream() + .filter(e -> e.getKey().startsWith("Sex:")) + .filter(e -> e.getValue().contains(rulesetId)) + .map(e -> e.getKey().substring("Sex:".length()).trim()) + .toList(); + } + + /** + * Fully-materialised {@link RuleSet} for {@code rulesetId}. Triggers + * parsing of the owning file (and any files it transitively references + * via {@code GETLIST}/{@code GETRULE}). Returns {@code null} if the id + * is unknown. + */ + public RuleSet ruleSet(String rulesetId) + { + File owner = index.fileForRuleset(rulesetId); + if (owner == null) + { + return null; + } + ensureFileParsed(owner); + return rulesets.get(rulesetId); + } + + /** + * Live ruleset map — every entry is fully materialised. + */ + Map liveRulesets() + { + return rulesets; + } + + /** + * Live name-list map — entries appear as their owning files are parsed. + */ + Map liveLists() + { + return lists; + } + + /** + * All ruleset metadata known to the index. + */ + Map rulesetMeta() + { + return index.rulesetsById(); + } + + /** + * Index's category map (raw): category title -> ruleset ids. + */ + Map> rulesetIdsByCategory() + { + return index.rulesetIdsByCategory(); + } + + /** + * Live unresolved-references list, mutated as files get parsed. + */ + public List unresolvedReferences() + { + return Collections.unmodifiableList(unresolved); + } + + /** + * Files parsed so far. Useful for benchmarks/tests. + */ + public Set parsedFiles() + { + return Collections.unmodifiableSet(parsedFiles); + } + + private String crossFileRuleSetTitle(String rulesetId) + { + NameGenIndex.RuleSetMeta meta = index.rulesetsById().get(rulesetId); + return meta == null ? null : meta.title(); + } + + /** + * Idempotently parse {@code file} and every file it transitively pulls + * in via {@code GETLIST}/{@code GETRULE}. The {@code parsedFiles} guard + * keeps us from reparsing; {@code inProgressFiles} short-circuits + * cycles between mutually-referencing files. {@link RulePart.RuleSetRef} + * resolves through a shared map at generation time, so a cycle just + * means one ruleset's reference temporarily points at an unbuilt + * target — populated correctly once the outer call's phase 2 returns. + */ + private void ensureFileParsed(File file) + { + if (parsedFiles.contains(file) || inProgressFiles.contains(file)) + { + return; + } + // Two-phase parse so cross-file GETLIST/GETRULE references can be + // satisfied before this file's rule bodies are built. Phase 1 + // registers this file's s into the live map and returns the + // idrefs this file points at. We then transitively parse any + // referenced files (which goes through the same two-phase dance). + // Phase 2 finally builds this file's rulesets, by which time every + // referenced list/ruleset is in the live maps. + inProgressFiles.add(file); + NameGenDataLoader.LazyFilePrepared prepared; + try + { + try + { + prepared = NameGenDataLoader.prepareFileForLazy(file, dataDir, lists); + } catch (IOException e) + { + throw new UncheckedIOException(e); + } + // Mark as parsed BEFORE recursing, otherwise a cycle (file A's + // rules reference file B which references file A) causes + // infinite recursion. The in-progress guard already short- + // circuits, but adding to parsedFiles here also lets us return + // early on identity-cycles without redoing prepare. + parsedFiles.add(file); + + for (String idref : prepared.referencedListIds()) + { + if (lists.containsKey(idref)) + { + continue; + } + File target = index.fileForList(idref); + if (target != null) + { + ensureFileParsed(target); + } + } + for (String idref : prepared.referencedRuleSetIds()) + { + if (rulesets.containsKey(idref)) + { + continue; + } + File target = index.fileForRuleset(idref); + if (target != null) + { + ensureFileParsed(target); + } + } + + // Phase 2: build this file's rulesets now that every cross-file + // dependency has been registered. + NameGenDataLoader.buildRuleSetsForLazy(prepared, lists, rulesets, unresolved, + this::crossFileRuleSetTitle); + } finally + { + inProgressFiles.remove(file); + } + } +} 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..6813e457c35 --- /dev/null +++ b/code/src/java/pcgen/core/namegen/NameGenerator.java @@ -0,0 +1,202 @@ +/* + * 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. + */ +package pcgen.core.namegen; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * UI-toolkit-independent facade for the random-name engine. Backed by the + * lazy {@link NameGenLazyData} loader: at construction time only a cheap + * StAX index pre-scan runs; XML rule bodies are parsed when the user + * actually selects a category/title/gender combination. + * + *

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 NameGenLazyData backing; + + public NameGenerator(File dataDir) throws IOException + { + this.backing = NameGenLazyData.open(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 backing.categoryTitles().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. + * + *

Reads index metadata only — no XML body parsed. + */ + public List getTitlesFor(String category) + { + return backing.rulesetMetaFor(category).stream() + .filter(meta -> FINAL_USAGE.equals(meta.usage())) + .map(NameGenIndex.RuleSetMeta::title) + .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. + * + *

Reads index metadata only — no XML body parsed. + */ + public List getGendersFor(String category, String title) + { + Set raw = backing.rulesetMetaFor(category).stream() + .filter(meta -> FINAL_USAGE.equals(meta.usage())) + .filter(meta -> meta.title().equals(title)) + .flatMap(meta -> backing.gendersForRuleset(meta.id()).stream()) + .collect(Collectors.toSet()); + + 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. + * Triggers parsing of the file containing the matched ruleset (and any + * files it transitively references via {@code GETLIST}/{@code GETRULE}). + * + * @return matching {@link RuleSet}, or {@code null} if no entry matches + */ + public RuleSet getCatalog(String category, String title, String gender) + { + return backing.rulesetMetaFor(category).stream() + .filter(meta -> FINAL_USAGE.equals(meta.usage())) + .filter(meta -> meta.title().equals(title)) + .filter(meta -> backing.isInGenderBucket(meta.id(), gender)) + .findFirst() + .map(meta -> backing.ruleSet(meta.id())) + .orElse(null); + } + + /** + * Pick a {@link Rule} from {@code catalog} by weighted random and + * generate a name from it. + */ + public GeneratedName generate(RuleSet catalog) + { + Rule rule = catalog.pick(); + if (rule == null) + { + return new GeneratedName("", "", "", null, List.of()); + } + List parts = rule.generate(); + return assemble(rule, parts); + } + + /** + * Generate a name forcing a specific {@link Rule} (Structure override). + */ + public GeneratedName generateWithRule(Rule forcedRule) + { + List parts = forcedRule.generate(); + return assemble(forcedRule, parts); + } + + /** + * Returns the {@link Rule} alternatives within {@code catalog}; used by + * the Advanced "Structure" picker. + */ + public List getRulesFor(RuleSet catalog) + { + return catalog.rules(); + } + + /** + * Exposes a snapshot of all loaded data. Forces every XML file to be + * parsed (equivalent to the legacy eager load). Primarily for tests + * that want to inspect the full corpus. + */ + public NameGenData getData() + { + // Parse every ruleset declared anywhere — that pulls in every + // referenced list/ruleset transitively, which between them touch + // every file. + for (String id : backing.rulesetMeta().keySet()) + { + backing.ruleSet(id); + } + Map rulesets = new LinkedHashMap<>(backing.liveRulesets()); + + Map> categories = backing.rulesetIdsByCategory().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> e.getValue().stream() + .map(rulesets::get) + .filter(Objects::nonNull) + .toList(), + (a, b) -> b, + LinkedHashMap::new)); + return new NameGenData(backing.liveLists(), rulesets, categories, + backing.unresolvedReferences()); + } + + private static GeneratedName assemble(Rule rule, List parts) + { + StringBuilder name = new StringBuilder(); + StringBuilder meaning = new StringBuilder(); + StringBuilder pron = new StringBuilder(); + boolean anyMeaning = false; + boolean anyPron = false; + for (DataValue v : parts) + { + name.append(v.getValue()); + + String m = v.getSubValue("meaning"); + meaning.append(m == null ? v.getValue() : m); + anyMeaning |= m != null; + + String p = v.getSubValue("pronounciation"); + pron.append(p == null ? v.getValue() : p); + anyPron |= p != null; + } + return new GeneratedName(name.toString(), + anyMeaning ? meaning.toString() : "", + anyPron ? pron.toString() : "", + rule, List.copyOf(parts)); + } +} diff --git a/code/src/java/pcgen/core/namegen/NameList.java b/code/src/java/pcgen/core/namegen/NameList.java new file mode 100644 index 00000000000..454a12245f5 --- /dev/null +++ b/code/src/java/pcgen/core/namegen/NameList.java @@ -0,0 +1,61 @@ +/* + * 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.concurrent.ThreadLocalRandom; + +/** + * Immutable named bag of weighted values, picked from at name-generation + * time. Replaces the legacy {@code DDList}. + */ +public record NameList(String id, String title, List values) +{ + public NameList + { + values = List.copyOf(values); + } + + /** + * Pick one value by weighted random. Zero-weight entries are skipped + * so authors can disable an entry without removing it. + * + * @return the chosen value, or {@code null} if the list has no + * positive-weight entries + */ + public WeightedDataValue pick() + { + int total = values.stream().mapToInt(WeightedDataValue::getWeight).filter(w -> w > 0).sum(); + if (total <= 0) + { + return null; + } + int roll = ThreadLocalRandom.current().nextInt(total) + 1; + int running = 0; + for (WeightedDataValue v : values) + { + int w = v.getWeight(); + if (w <= 0) + { + continue; + } + running += w; + if (roll <= running) + { + return v; + } + } + return values.get(values.size() - 1); + } +} diff --git a/code/src/java/pcgen/core/namegen/Rule.java b/code/src/java/pcgen/core/namegen/Rule.java new file mode 100644 index 00000000000..bcdb64974c8 --- /dev/null +++ b/code/src/java/pcgen/core/namegen/Rule.java @@ -0,0 +1,53 @@ +/* + * 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.util.ArrayList; +import java.util.List; + +/** + * One alternative within a {@link RuleSet}: a sequence of {@link RulePart}s + * that together produce a name. References inside the parts are linked at + * load time, so generation is a flat traversal — no map lookups, no + * runtime casts. + */ +public record Rule(int weight, String displayLabel, List parts) +{ + public Rule + { + parts = List.copyOf(parts); + } + + /** Expand the rule into the value sequence consumed by the assembler. */ + public List generate() + { + List out = new ArrayList<>(); + for (RulePart part : parts) + { + out.addAll(part.generate()); + } + return out; + } + + /** + * Used by the Advanced "Structure" combo box, which renders rule + * alternatives like {@code "[Given] [Surname] "}. + */ + @Override + public String toString() + { + return displayLabel; + } +} diff --git a/code/src/java/pcgen/core/namegen/RulePart.java b/code/src/java/pcgen/core/namegen/RulePart.java new file mode 100644 index 00000000000..778e20af86e --- /dev/null +++ b/code/src/java/pcgen/core/namegen/RulePart.java @@ -0,0 +1,101 @@ +/* + * 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; + +/** + * One element of a {@link Rule}: either a reference to a {@link NameList} + * or another {@link RuleSet}, or a literal separator. Resolved once at + * load time so name generation needs at most one map lookup per part. + */ +public sealed interface RulePart +{ + /** Materialise the part into one or more {@link DataValue}s. */ + List generate(); + + /** Human-readable label for {@code Rule.toString()} (e.g. "[Given] "). */ + String label(); + + /** A reference to a {@link NameList}: pick one weighted value. */ + record ListRef(NameList list) implements RulePart + { + @Override + public List generate() + { + WeightedDataValue picked = list.pick(); + return picked == null ? List.of() : List.of(picked); + } + + @Override + public String label() + { + return "[" + list.title() + "] "; + } + } + + /** + * A reference to a {@link RuleSet}, resolved through a shared map. + * Rulesets reference each other, so we can't hold the target record + * directly at construction time — the map is populated and frozen + * after the loader has built every {@code RuleSet}, then handed in. + */ + record RuleSetRef(String targetId, String targetTitle, + Map rulesets) implements RulePart + { + @Override + public List generate() + { + RuleSet rs = rulesets.get(targetId); + if (rs == null) + { + return List.of(); + } + Rule picked = rs.pick(); + return picked == null ? List.of() : picked.generate(); + } + + @Override + public String label() + { + return "[" + targetTitle + "] "; + } + } + + /** Literal separator emitted as-is into the generated name. */ + enum Literal implements RulePart + { + SPACE(" "), HYPHEN("-"), CR("\n"); + + private final DataValue value; + + Literal(String text) + { + this.value = new DataValue(text); + } + + @Override + public List generate() + { + return List.of(value); + } + + @Override + public String label() + { + return ""; + } + } +} diff --git a/code/src/java/pcgen/core/namegen/RuleSet.java b/code/src/java/pcgen/core/namegen/RuleSet.java new file mode 100644 index 00000000000..6147b6bfed3 --- /dev/null +++ b/code/src/java/pcgen/core/namegen/RuleSet.java @@ -0,0 +1,70 @@ +/* + * 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.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Immutable bag of {@link Rule} alternatives keyed by display title. + * The {@code usage} field carries the legacy {@code "final"} marker that + * tells the facade which rulesets the user picks directly versus the + * shared building-block rulesets ({@code "private"}). + */ +public record RuleSet(String id, String title, String usage, List rules) +{ + public RuleSet + { + rules = List.copyOf(rules); + } + + /** Title is what the combo boxes show for a ruleset. */ + @Override + public String toString() + { + return title; + } + + /** + * Pick one rule by weighted random. Zero-weight entries are skipped. + * + * @return the chosen rule, or {@code null} if no rule has positive + * weight + */ + public Rule pick() + { + int total = rules.stream().mapToInt(Rule::weight).filter(w -> w > 0).sum(); + if (total <= 0) + { + return null; + } + int roll = ThreadLocalRandom.current().nextInt(total) + 1; + int running = 0; + for (Rule r : rules) + { + int w = r.weight(); + if (w <= 0) + { + continue; + } + running += w; + if (roll <= running) + { + return r; + } + } + return rules.get(rules.size() - 1); + } +} 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/gui2/dialog/RandomNameDialog.java b/code/src/java/pcgen/gui2/dialog/RandomNameDialog.java deleted file mode 100644 index 205d0b05538..00000000000 --- a/code/src/java/pcgen/gui2/dialog/RandomNameDialog.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright James Dempsey, 2010 - * - * 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 pcgen.gui2.dialog; - -import java.awt.BorderLayout; -import java.io.File; - -import javax.swing.JDialog; -import javax.swing.JFrame; -import javax.swing.WindowConstants; - -import pcgen.core.SettingsHandler; -import pcgen.gui2.doomsdaybook.NameGenPanel; -import pcgen.gui2.tools.Utility; -import pcgen.gui3.GuiUtility; -import pcgen.gui3.component.OKCloseButtonBar; -import pcgen.system.LanguageBundle; - -import javafx.scene.control.ButtonBar; -import org.apache.commons.lang3.StringUtils; - -/** - * The Class {@code RandomNameDialog} is a dialog in which the user can - * generate a random name for their character. - */ -public final class RandomNameDialog extends JDialog -{ - private final NameGenPanel nameGenPanel; - private boolean cancelled; - - /** - * Create a new Random Name Dialog - * @param frame The parent frame. The dialog will be centred on this frame - * @param gender The current gender of the character. - */ - public RandomNameDialog(JFrame frame, String gender) - { - super(frame, LanguageBundle.getString("in_rndNameTitle"), true); //$NON-NLS-1$ - nameGenPanel = new NameGenPanel(new File(getDataDir())); - nameGenPanel.setGender(gender); - initUserInterface(); - pack(); - setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); - this.setLocationRelativeTo(frame); - cancelled = false; - - Utility.installEscapeCloseOperation(this); - } - - private void initUserInterface() - { - getContentPane().setLayout(new BorderLayout()); - - getContentPane().add(nameGenPanel, BorderLayout.CENTER); - - ButtonBar buttonBar = new OKCloseButtonBar( - evt -> okButtonActionPerformed(), - evt -> cancelButtonActionPerformed() - ); - - getContentPane().add(GuiUtility.wrapParentAsJFXPanel(buttonBar), BorderLayout.PAGE_END); - } - - private void okButtonActionPerformed() - { - setVisible(false); - } - - private void cancelButtonActionPerformed() - { - cancelled = true; - setVisible(false); - } - - /** - * @return The directory where the random name data is held - */ - private String getDataDir() - { - String pluginDirectory = SettingsHandler.getGmgenPluginDir().toString(); - - return pluginDirectory + File.separator + "Random Names"; - } - - /** - * @return The name the user generated. - */ - public String getChosenName() - { - if (cancelled) - { - return StringUtils.EMPTY; - } - return nameGenPanel.getChosenName(); - } - - /** - * @return the gender - */ - public String getGender() - { - if (cancelled) - { - return StringUtils.EMPTY; - } - return nameGenPanel.getGender(); - } - -} diff --git a/code/src/java/pcgen/gui2/doomsdaybook/NameButton.java b/code/src/java/pcgen/gui2/doomsdaybook/NameButton.java deleted file mode 100644 index 292e852bc14..00000000000 --- a/code/src/java/pcgen/gui2/doomsdaybook/NameButton.java +++ /dev/null @@ -1,43 +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 pcgen.gui2.doomsdaybook; - -import pcgen.core.doomsdaybook.DataElement; - -class NameButton extends javax.swing.JButton -{ - private final DataElement element; - - /** Creates a new instance of NameButton - * @param element - */ - NameButton(DataElement element) - { - this.element = element; - super.setText(element.getTitle()); - } - - /** - * Get the data element for the name button - * @return the data element for the name button - */ - DataElement getDataElement() - { - return element; - } -} diff --git a/code/src/java/pcgen/gui2/doomsdaybook/NameGenPanel.java b/code/src/java/pcgen/gui2/doomsdaybook/NameGenPanel.java deleted file mode 100644 index e5a2b6d1c27..00000000000 --- a/code/src/java/pcgen/gui2/doomsdaybook/NameGenPanel.java +++ /dev/null @@ -1,1005 +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 pcgen.gui2.doomsdaybook; - -import java.awt.BorderLayout; -import java.awt.FlowLayout; -import java.awt.Insets; -import java.awt.datatransfer.Clipboard; -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.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; -import javax.swing.ComboBoxModel; -import javax.swing.DefaultComboBoxModel; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -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.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. - */ -@SuppressWarnings({"UseOfObsoleteCollectionType", "PMD.UseArrayListInsteadOfVector"}) -public class NameGenPanel extends JPanel -{ - private final Map> categories = new HashMap<>(); - private JButton generateButton; - private JButton jButton1; - private JCheckBox chkStructure; - private JComboBox cbCatalog; - private JComboBox cbCategory; - private JComboBox cbGender; - private JComboBox cbStructure; - private JLabel jLabel1; - private JLabel jLabel2; - private JLabel jLabel3; - - private JLabel jLabel4; - private JLabel jLabel5; - private JLabel jLabel6; - private JLabel meaning; - private JLabel pronounciation; - private JPanel buttonPanel; - private JPanel genCtrlPanel; - private JPanel jPanel10; - private JPanel jPanel11; - private JPanel jPanel12; - private JPanel jPanel13; - private JPanel jPanel14; - private JPanel nameDisplayPanel; - private JPanel namePanel; - private JPanel jPanel4; - private JPanel nameSubInfoPanel; - private JPanel nameActionPanel; - private JPanel jPanel7; - private JPanel jPanel8; - private JPanel jPanel9; - private JSeparator jSeparator1; - private JSeparator jSeparator2; - private JSeparator jSeparator3; - private JSeparator jSeparator4; - private JTextField name; - private final VariableHashMap allVars = new VariableHashMap(); - - private Rule lastRule = null; - - /** Creates new form NameGenPanel */ - public NameGenPanel() - { - this(new File(".")); - } - - /** - * Constructs a NameGenPanel given a dataPath - * - * @param dataPath The path to the random name data files. - */ - public NameGenPanel(File dataPath) - { - initComponents(); - loadData(dataPath); - } - - /** - * Generate a Rule - * @return new Rule - */ - public Rule generate() - { - try - { - Rule rule; - - if (chkStructure.isSelected()) - { - RuleSet rs = (RuleSet) cbCatalog.getSelectedItem(); - rule = rs.getRule(); - } - else - { - rule = (Rule) cbStructure.getSelectedItem(); - } - - List aName = rule.getData(); - setNameText(aName); - setMeaningText(aName); - setPronounciationText(aName); - - return 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 - { - NameButton nb = (NameButton) evt.getSource(); - DataElement element = nb.getDataElement(); - element.getData(); - - Rule rule = this.lastRule; - - if (rule == null) - { - if (chkStructure.isSelected()) - { - RuleSet rs = (RuleSet) cbCatalog.getSelectedItem(); - rule = rs.getLastRule(); - } - else - { - rule = (Rule) cbStructure.getSelectedItem(); - } - - this.lastRule = rule; - } - - List aName = rule.getLastData(); - - setNameText(aName); - setMeaningText(aName); - setPronounciationText(aName); - } - catch (Exception e) - { - Logging.errorPrint(e.getMessage(), e); - } - } - - 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(); - buttonPanel.repaint(); - } - - private void displayButtons(Rule rule) - { - clearButtons(); - - for (String key : rule) - { - try - { - DataElement ele = allVars.getDataElement(key); - - if (ele.getTitle() != null) - { - NameButton nb = new NameButton(ele); - nb.addActionListener(this::NameButtonActionPerformed); - buttonPanel.add(nb); - } - } - catch (Exception e) - { - Logging.errorPrint(e.getMessage(), e); - } - } - - buttonPanel.repaint(); - } - - private void generateButtonActionPerformed(ActionEvent evt) - { //GEN-FIRST:event_generateButtonActionPerformed - - try - { - this.lastRule = generate(); - displayButtons(this.lastRule); - } - catch (Exception e) - { - Logging.errorPrint(e.getMessage(), e); - } - } - - //GEN-LAST:event_generateButtonActionPerformed - - /** - * This method is called from within the constructor to - * initialize the form. - */ - private void initComponents() - { - genCtrlPanel = new JPanel(); - jPanel4 = new JPanel(); - jPanel13 = new JPanel(); - jPanel10 = new JPanel(); - jLabel4 = new JLabel(); - cbCatalog = new JComboBox<>(); - jPanel8 = new JPanel(); - jLabel1 = new JLabel(); - cbCategory = new JComboBox<>(); - jPanel14 = new JPanel(); - jPanel11 = new JPanel(); - generateButton = new JButton(); - jPanel9 = new JPanel(); - jLabel5 = new JLabel(); - cbGender = new JComboBox<>(); - jPanel7 = new JPanel(); - jSeparator4 = new JSeparator(); - jPanel12 = new JPanel(); - jLabel6 = new JLabel(); - cbStructure = new JComboBox<>(); - chkStructure = new JCheckBox(); - buttonPanel = new JPanel(); - nameDisplayPanel = new JPanel(); - nameSubInfoPanel = new JPanel(); - jSeparator2 = new JSeparator(); - jLabel2 = new JLabel(); - meaning = new JLabel(); - jSeparator1 = new JSeparator(); - jLabel3 = new JLabel(); - pronounciation = new JLabel(); - jSeparator3 = new JSeparator(); - namePanel = new JPanel(); - name = new JTextField(); - nameActionPanel = new JPanel(); - jButton1 = new JButton(); - - setLayout(new BorderLayout(0, 5)); - - genCtrlPanel.setLayout(new BorderLayout()); - - jPanel4.setLayout(new BoxLayout(jPanel4, BoxLayout.X_AXIS)); - - jPanel13.setLayout(new BorderLayout()); - - jPanel10.setLayout(new FlowLayout(FlowLayout.LEFT)); - - jLabel4.setText(LanguageBundle.getString("in_rndNameCatalog")); //$NON-NLS-1$ - jPanel10.add(jLabel4); - - cbCatalog.addActionListener(this::cbCatalogActionPerformed); - - jPanel10.add(cbCatalog); - - jPanel13.add(jPanel10, BorderLayout.CENTER); - - jPanel8.setLayout(new FlowLayout(FlowLayout.LEFT)); - - jLabel1.setText(LanguageBundle.getString("in_rndNameCategory")); //$NON-NLS-1$ - jPanel8.add(jLabel1); - - cbCategory.addActionListener(this::cbCategoryActionPerformed); - - jPanel8.add(cbCategory); - - jPanel13.add(jPanel8, BorderLayout.NORTH); - - jPanel4.add(jPanel13); - - jPanel14.setLayout(new BorderLayout()); - - jPanel11.setLayout(new FlowLayout(FlowLayout.LEFT)); - - generateButton.setText(LanguageBundle.getString("in_rndNameGenerate")); //$NON-NLS-1$ - generateButton.addActionListener(this::generateButtonActionPerformed); - - jPanel11.add(generateButton); - - jPanel14.add(jPanel11, BorderLayout.CENTER); - - jPanel9.setLayout(new FlowLayout(FlowLayout.LEFT)); - - jLabel5.setText(LanguageBundle.getString("in_rndNameSex")); //$NON-NLS-1$ - jPanel9.add(jLabel5); - - cbGender.addActionListener(this::cbGenderActionPerformed); - - jPanel9.add(cbGender); - - jPanel14.add(jPanel9, BorderLayout.NORTH); - - jPanel4.add(jPanel14); - - genCtrlPanel.add(jPanel4, BorderLayout.NORTH); - - jPanel7.setLayout(new BorderLayout()); - - jPanel7.add(jSeparator4, BorderLayout.NORTH); - - jPanel12.setLayout(new FlowLayout(FlowLayout.LEFT)); - - jLabel6.setText(LanguageBundle.getString("in_rndNameStructure")); //$NON-NLS-1$ - jPanel12.add(jLabel6); - - cbStructure.setEnabled(false); - cbStructure.addActionListener(this::cbStructureActionPerformed); - jPanel12.add(cbStructure); - - chkStructure.setSelected(true); - chkStructure.setText(LanguageBundle.getString("in_randomButton")); //$NON-NLS-1$ - chkStructure.addActionListener(this::chkStructureActionPerformed); - - jPanel12.add(chkStructure); - - jPanel7.add(jPanel12, BorderLayout.CENTER); - jPanel7.add(new JSeparator(), BorderLayout.SOUTH); - - JPanel adjustNamePanel = new JPanel(); - adjustNamePanel.setLayout(new BorderLayout()); - - JLabel adjNameLabel = new JLabel(LanguageBundle.getString("in_rndNameAdjust")); //$NON-NLS-1$ - - adjustNamePanel.add(adjNameLabel, BorderLayout.NORTH); - - buttonPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); - // CODE-2099 Component needed to have correct vertical space available. - JLabel nb = new JLabel(" "); //$NON-NLS-1$ - buttonPanel.add(nb); - - adjustNamePanel.add(buttonPanel, BorderLayout.CENTER); - - add(adjustNamePanel, BorderLayout.SOUTH); - - genCtrlPanel.add(jPanel7, BorderLayout.CENTER); - - // Name display - nameDisplayPanel.setLayout(new BorderLayout()); - - nameSubInfoPanel.setLayout(new BoxLayout(nameSubInfoPanel, BoxLayout.Y_AXIS)); - - nameSubInfoPanel.add(jSeparator2); - - jLabel2.setText(LanguageBundle.getString("in_rndNameMeaning")); //$NON-NLS-1$ - nameSubInfoPanel.add(jLabel2); - - meaning.setText(LanguageBundle.getString("in_rndNmDefault")); //$NON-NLS-1$ - nameSubInfoPanel.add(meaning); - - nameSubInfoPanel.add(jSeparator1); - - jLabel3.setText(LanguageBundle.getString("in_rndNmPronounciation")); //$NON-NLS-1$ - nameSubInfoPanel.add(jLabel3); - - pronounciation.setText("nAm"); - nameSubInfoPanel.add(pronounciation); - - nameSubInfoPanel.add(jSeparator3); - - nameDisplayPanel.add(nameSubInfoPanel, BorderLayout.SOUTH); - - JLabel nameTitleLabel = new JLabel(LanguageBundle.getString("in_sumName")); //$NON-NLS-1$ - JPanel nameTitlePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - nameTitlePanel.add(nameTitleLabel); - - JPanel topPanel = new JPanel(); - topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS)); - - namePanel.setLayout(new BoxLayout(namePanel, BoxLayout.X_AXIS)); - - FontManipulation.xxlarge(name); - name.setText(LanguageBundle.getString("in_nameLabel")); //$NON-NLS-1$ - namePanel.add(name); - - nameActionPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); - - jButton1.setIcon(Icons.Copy16.getImageIcon()); - jButton1.setAlignmentY(0.0F); - jButton1.setIconTextGap(0); - jButton1.setMargin(new Insets(2, 2, 2, 2)); - jButton1.addActionListener(this::jButton1ActionPerformed); - nameActionPanel.add(jButton1); - - namePanel.add(nameActionPanel); - - topPanel.add(genCtrlPanel); - topPanel.add(nameTitlePanel); - topPanel.add(namePanel); - topPanel.add(nameDisplayPanel); - 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 - { - String catKey = (String) cbCategory.getSelectedItem(); - String genderKey = (String) cbGender.getSelectedItem(); - RuleSet oldRS = (RuleSet) cbCatalog.getSelectedItem(); - String catalogKey = ""; - - if (oldRS != null) - { - catalogKey = oldRS.getTitle(); - } - - List cats = categories.get(catKey); - List genders = categories.get("Sex: " + genderKey); - List join = new ArrayList<>(cats); - join.retainAll(genders); - join.sort(new DataElementComperator()); - - Vector catalogs = new Vector<>(); - int oldSelected = -1; - int n = 0; - - for (final RuleSet rs : join) - { - if (rs.getUsage().equals("final")) - { - catalogs.add(rs); - - if (rs.getTitle().equals(catalogKey)) - { - oldSelected = n; - } - - n++; - } - } - - ComboBoxModel catalogModel = new DefaultComboBoxModel<>(catalogs); - cbCatalog.setModel(catalogModel); - if (oldSelected >= 0) - { - cbCatalog.setSelectedIndex(oldSelected); - } - } - catch (Exception e) - { - Logging.errorPrint(e.getMessage(), e); - } - } - - // 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) - { - // 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 - 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); - break; - } - } - } - - // Sort the genders - Collections.sort(selectable); - - // Create a new model for the combobox and set it - cbGender.setModel(new DefaultComboBoxModel<>(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(); - 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.doomsdaybook.DDList dataList = new pcgen.core.doomsdaybook.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()) - { - cbStructure.setModel(new DefaultComboBoxModel<>()); - cbStructure.setEnabled(false); - } - else - { - Vector struct = new Vector<>(); - - for (String key : ((RuleSet) cbCatalog.getSelectedItem())) - { - try - { - struct.add(allVars.getDataElement(key)); - } - catch (Exception e) - { - Logging.errorPrint(e.getMessage(), e); - } - } - - DefaultComboBoxModel structModel = new DefaultComboBoxModel<>(struct); - cbStructure.setModel(structModel); - cbStructure.setEnabled(true); - } - } - - /** - * @return the generated name chosen by the user - */ - public String getChosenName() - { - return name.getText(); - } - - /** - * @return the gender - */ - public String getGender() - { - return (String) cbGender.getSelectedItem(); - } - - /** - * @param gender the gender to set - */ - public void setGender(String gender) - { - if (gender != null && cbGender != null) - { - 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; - } - } - } -} diff --git a/code/src/java/pcgen/gui2/doomsdaybook/XMLFilter.java b/code/src/java/pcgen/gui2/doomsdaybook/XMLFilter.java deleted file mode 100644 index ed26642906e..00000000000 --- a/code/src/java/pcgen/gui2/doomsdaybook/XMLFilter.java +++ /dev/null @@ -1,37 +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 pcgen.gui2.doomsdaybook; - -import java.io.File; - -public class XMLFilter implements java.io.FilenameFilter -{ - - /** - * Returns true if file matches *.xml - * - * @param file - * @param str - * @return true if filter matches *.xml - */ - @Override - public boolean accept(File file, String str) - { - return str.matches(".*\\.xml$"); - } -} diff --git a/code/src/java/pcgen/gui2/tabs/SummaryInfoTab.java b/code/src/java/pcgen/gui2/tabs/SummaryInfoTab.java index 99efab00557..3b4eecba185 100644 --- a/code/src/java/pcgen/gui2/tabs/SummaryInfoTab.java +++ b/code/src/java/pcgen/gui2/tabs/SummaryInfoTab.java @@ -83,7 +83,7 @@ import pcgen.gui2.UIPropertyContext; import pcgen.gui2.dialog.CharacterHPDialog; import pcgen.gui2.dialog.KitSelectionDialog; -import pcgen.gui2.dialog.RandomNameDialog; +import pcgen.gui3.namegen.RandomNameDialog; import pcgen.gui2.dialog.SinglePrefDialog; import pcgen.gui2.prefs.CharacterStatsPanel; import pcgen.gui2.tabs.models.CharacterComboBoxModel; @@ -647,7 +647,7 @@ public ModelMap createModels(final CharacterFacade character) models.put(ComboBoxModelHandler.class, new ComboBoxModelHandler(character)); models.put(RandomNameAction.class, - new RandomNameAction(character, (JFrame) SwingUtilities.getWindowAncestor(this))); + new RandomNameAction(character)); models.put(ClassLevelTableModel.class, new ClassLevelTableModel(character, classLevelTable, classComboBox)); models.put(GenerateRollsAction.class, new GenerateRollsAction(character)); @@ -1172,22 +1172,22 @@ private static class RandomNameAction extends AbstractAction { private final CharacterFacade character; - private final JFrame frame; - RandomNameAction(CharacterFacade character, JFrame frame) + RandomNameAction(CharacterFacade character) { this.character = character; - this.frame = frame; } @Override public void actionPerformed(ActionEvent e) { + java.awt.Window owner = (e.getSource() instanceof java.awt.Component c) + ? javax.swing.SwingUtilities.getWindowAncestor(c) : null; String gender = character.getGenderRef().get() != null ? character.getGenderRef().get().toString() : ""; //$NON-NLS-1$ - RandomNameDialog dialog = new RandomNameDialog(frame, gender); - dialog.setVisible(true); + RandomNameDialog dialog = new RandomNameDialog(owner, gender); + dialog.showAndBlock(); String chosenName = dialog.getChosenName(); if (chosenName != null && !chosenName.isEmpty() && !chosenName.equals(LanguageBundle.getString("in_rndNmDefault"))) //$NON-NLS-1$ diff --git a/code/src/java/pcgen/gui3/JFXPanelFromResource.java b/code/src/java/pcgen/gui3/JFXPanelFromResource.java index 0d471d77b89..ad2391afa56 100644 --- a/code/src/java/pcgen/gui3/JFXPanelFromResource.java +++ b/code/src/java/pcgen/gui3/JFXPanelFromResource.java @@ -106,7 +106,6 @@ public void showAndBlock(String title) stage.setScene(getScene()); stage.sizeToScene(); stage.showAndWait(); - Logging.errorPrint("passed wait"); lock.completeAsync(() -> 0); }); diff --git a/code/src/java/pcgen/gui3/application/DesktopHandler.java b/code/src/java/pcgen/gui3/application/DesktopHandler.java index 26e2392054b..2a04933eda4 100644 --- a/code/src/java/pcgen/gui3/application/DesktopHandler.java +++ b/code/src/java/pcgen/gui3/application/DesktopHandler.java @@ -20,6 +20,7 @@ import java.awt.Desktop; import java.awt.Desktop.Action; +import java.awt.EventQueue; import java.awt.desktop.AboutEvent; import java.awt.desktop.PreferencesEvent; import java.awt.desktop.QuitEvent; @@ -93,7 +94,13 @@ private static class QuitHandler implements java.awt.desktop.QuitHandler @Override public void handleQuitRequestWith(final QuitEvent quitEvent, final QuitResponse quitResponse) { - PCGenUIManager.closePCGen(); + // Cancel the system quit so macOS stops intercepting mouse events — without this, the save dialog appears, + // but its buttons are frozen because the OS is waiting for a quit response and routing clicks through the + // quit machinery. We drive the quit ourselves via closePCGen. + quitResponse.cancelQuit(); + + // Defer closePCGen to a fresh EDT turn so handleQuitRequestWith returns first. + EventQueue.invokeLater(PCGenUIManager::closePCGen); } } } 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/java/pcgen/gui3/namegen/GenderSelection.java b/code/src/java/pcgen/gui3/namegen/GenderSelection.java new file mode 100644 index 00000000000..0b40213aed3 --- /dev/null +++ b/code/src/java/pcgen/gui3/namegen/GenderSelection.java @@ -0,0 +1,54 @@ +/* + * 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.util.List; + +/** + * Pure helpers for the random-name UI's gender-selection rules. Kept + * out of the FX controller so the logic can be exercised without + * spinning up a JavaFX runtime. + */ +final class GenderSelection +{ + private GenderSelection() + { + } + + /** + * Pick a gender to select after the available list changes. Order: + * keep {@code previous} if still valid, then fall back to + * {@code preferred} (caller-supplied initial gender) if valid, then + * the first {@code available} entry. Returns the empty string when + * no gender is available. + */ + static String chooseSticky(List available, String previous, String preferred) + { + if (available.isEmpty()) + { + return ""; + } + boolean previousStillValid = previous != null && available.contains(previous); + if (previousStillValid) + { + return previous; + } + boolean preferredStillValid = preferred != null && available.contains(preferred); + if (preferredStillValid) + { + return preferred; + } + return available.get(0); + } +} 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..71d796b62bc --- /dev/null +++ b/code/src/java/pcgen/gui3/namegen/RandomNameDialog.java @@ -0,0 +1,167 @@ +/* + * 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.awt.Dialog; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Window; +import java.io.IOException; +import java.net.URL; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; + +import javax.swing.JDialog; +import javax.swing.SwingUtilities; + +import pcgen.gui3.GuiAssertions; +import pcgen.system.LanguageBundle; +import pcgen.util.Logging; + +import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; + +/** + * Modal random-name dialog. Presented to Swing callers as a synchronous + * call: construct, {@link #showAndBlock()}, then read back + * {@link #getChosenName()} / {@link #getGender()}. + * + *

Hosts the FXML scene inside a Swing {@link JDialog} via + * {@link JFXPanel}. The Swing dialog gives us proper owner/modality + * semantics against the rest of the Swing UI; the FX side keeps the + * existing FXML, controller, and scene graph untouched. + */ +public final class RandomNameDialog +{ + private static final String FXML_RESOURCE = "RandomNamePanel.fxml"; + + private final Window owner; + private final String initialGender; + private RandomNamePanelController controller; + + public RandomNameDialog(Window owner, String initialGender) + { + this.owner = owner; + this.initialGender = initialGender; + } + + public RandomNameDialog(String initialGender) + { + this(null, initialGender); + } + + /** + * Show the dialog and block until the user closes it. Must be called + * from a non-FX thread; the call hops to the EDT if needed because + * Swing modal dialogs require it. + */ + public void showAndBlock() + { + GuiAssertions.assertIsNotJavaFXThread(); + if (!EventQueue.isDispatchThread()) + { + try + { + EventQueue.invokeAndWait(this::showAndBlock); + } + catch (InterruptedException _) + { + Thread.currentThread().interrupt(); + } + catch (java.lang.reflect.InvocationTargetException e) + { + Logging.errorPrint("failed to show random-name dialog", e); + } + return; + } + + JDialog dialog = new JDialog(owner, LanguageBundle.getString("in_rndNameTitle"), + Dialog.ModalityType.APPLICATION_MODAL); + JFXPanel fxPanel = new JFXPanel(); + dialog.setContentPane(fxPanel); + + CountDownLatch sceneReady = new CountDownLatch(1); + Platform.runLater(() -> { + try + { + FXMLLoader loader = new FXMLLoader(); + URL location = RandomNameDialog.class.getResource(FXML_RESOURCE); + Objects.requireNonNull(location, FXML_RESOURCE); + loader.setLocation(location); + loader.setResources(LanguageBundle.getBundle()); + Scene scene = loader.load(); + controller = loader.getController(); + assert controller != null; + if (initialGender != null && !initialGender.isEmpty()) + { + controller.setInitialGender(initialGender); + } + controller.setCloseAction(() -> SwingUtilities.invokeLater(dialog::dispose)); + fxPanel.setScene(scene); + // Read the FXML's preferred size from the scene root and + // pin both the JFXPanel's preferred size and the dialog's + // minimum size to it. Without this, JFXPanel reports a + // preferred size of 0 and pack() collapses the dialog. + javafx.scene.Parent root = scene.getRoot(); + root.applyCss(); + root.layout(); + int prefW = (int) Math.ceil(root.prefWidth(-1)); + int prefH = (int) Math.ceil(root.prefHeight(prefW)); + SwingUtilities.invokeLater(() -> { + Dimension pref = new Dimension(prefW, prefH); + fxPanel.setPreferredSize(pref); + dialog.pack(); + Dimension packed = dialog.getSize(); + // Lock the height (user can't change it), let the + // width float above the design width. + dialog.setMinimumSize(new Dimension(packed.width, packed.height)); + dialog.setMaximumSize(new Dimension(Integer.MAX_VALUE, packed.height)); + dialog.setResizable(true); + dialog.setLocationRelativeTo(owner); + }); + } + catch (IOException e) + { + Logging.errorPrint("failed to load random-name dialog FXML", e); + } + finally + { + sceneReady.countDown(); + } + }); + try + { + sceneReady.await(); + } + catch (InterruptedException _) + { + Thread.currentThread().interrupt(); + return; + } + + dialog.setVisible(true); + } + + public String getChosenName() + { + return controller == null ? "" : controller.getChosenName(); + } + + public String getGender() + { + 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..0b55be00161 --- /dev/null +++ b/code/src/java/pcgen/gui3/namegen/RandomNamePanelController.java @@ -0,0 +1,346 @@ +/* + * 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.Rule; +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.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.TextField; +import javafx.scene.control.Toggle; +import javafx.scene.control.ToggleGroup; + +/** + * 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 TextField generatedNameLabel; + @FXML + private Label meaningLabel; + @FXML + private Label pronunciationLabel; + @FXML + private CheckBox randomStructureCheck; + @FXML + private ComboBox structureCombo; + + private NameGenerator nameGenerator; + private String chosenName = ""; + private String chosenGender = ""; + private boolean cancelled = true; + private String preferredGender; + private Runnable closeAction; + + @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)); + randomStructureCheck.selectedProperty().addListener((obs, old, val) -> onRandomStructureChanged(val)); + structureCombo.setDisable(true); + setOptionalLabel(meaningLabel, "in_rndNameMeaning", "", ""); + setOptionalLabel(pronunciationLabel, "in_rndNmPronounciation", "", ""); + + 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); + } + } + + /** + * Hook supplied by {@link RandomNameDialog} so OK/Cancel can dispose + * the hosting Swing dialog without the controller knowing about it. + */ + public void setCloseAction(Runnable closeAction) + { + this.closeAction = closeAction; + } + + 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); + // If the sticky gender is unchanged, the toggle listener won't fire, + // so refresh the structure combo here too — the catalog is keyed on + // (category, title, gender) and title just changed. + refreshStructureCombo(); + } + + 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; + } + refreshStructureCombo(); + clearOutput(); + } + + private void onRandomStructureChanged(boolean random) + { + structureCombo.setDisable(random); + } + + private void refreshStructureCombo() + { + RuleSet catalog = currentCatalog(); + if (catalog == null) + { + structureCombo.setItems(FXCollections.emptyObservableList()); + return; + } + structureCombo.setItems(FXCollections.observableArrayList(nameGenerator.getRulesFor(catalog))); + if (!structureCombo.getItems().isEmpty()) + { + structureCombo.getSelectionModel().selectFirst(); + } + } + + @FXML + void onGenerate(ActionEvent event) + { + RuleSet catalog = currentCatalog(); + if (catalog == null) + { + return; + } + try + { + GeneratedName result = useForcedRule() + ? nameGenerator.generateWithRule(structureCombo.getValue()) + : nameGenerator.generate(catalog); + generatedNameLabel.setText(result.name()); + setOptionalLabel(meaningLabel, "in_rndNameMeaning", result.meaning(), result.name()); + setOptionalLabel(pronunciationLabel, "in_rndNmPronounciation", result.pronunciation(), result.name()); + } + catch (Exception e) + { + Logging.errorPrint("failed to generate random name", e); + generatedNameLabel.setText(LanguageBundle.getString("in_rndNmDefault")); + setOptionalLabel(meaningLabel, "in_rndNameMeaning", "", ""); + setOptionalLabel(pronunciationLabel, "in_rndNmPronounciation", "", ""); + } + } + + private boolean useForcedRule() + { + return !randomStructureCheck.isSelected() && structureCombo.getValue() != null; + } + + @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(); + } + fireClose(); + } + + @FXML + void onCancel(ActionEvent event) + { + cancelled = true; + fireClose(); + } + + 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) + { + return GenderSelection.chooseSticky(available, previous, preferredGender); + } + + 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")); + setOptionalLabel(meaningLabel, "in_rndNameMeaning", "", ""); + setOptionalLabel(pronunciationLabel, "in_rndNmPronounciation", "", ""); + } + + private static void setOptionalLabel(Label label, String prefixKey, String value, String generatedName) + { + boolean hasValue = value != null && !value.isEmpty() && !value.equals(generatedName); + String shown = hasValue ? value : "-"; + label.setText(LanguageBundle.getString(prefixKey) + " " + shown); + } + + private void fireClose() + { + if (closeAction != null) + { + closeAction.run(); + } + } + + private static String getDataDir() + { + return Objects.requireNonNull(SettingsHandler.getGmgenPluginDir()).toString() + + File.separator + "Random Names"; + } +} diff --git a/code/src/java/pcgen/system/Main.java b/code/src/java/pcgen/system/Main.java index 5c321aea9d7..63c3e18f8d1 100644 --- a/code/src/java/pcgen/system/Main.java +++ b/code/src/java/pcgen/system/Main.java @@ -18,7 +18,6 @@ */ package pcgen.system; -import java.awt.Component; import java.awt.Font; import java.awt.FontFormatException; import java.awt.GraphicsEnvironment; @@ -41,8 +40,8 @@ import pcgen.gui2.PCGenUIManager; import pcgen.gui2.UIPropertyContext; import pcgen.gui2.converter.TokenConverter; -import pcgen.gui2.dialog.RandomNameDialog; import pcgen.gui3.JFXPanelFromResource; +import pcgen.gui3.namegen.RandomNameDialog; import pcgen.gui3.dialog.OptionsPathDialogController; import pcgen.gui3.preloader.PCGenPreloader; import pcgen.io.ExportHandler; @@ -127,8 +126,8 @@ public static void main(String... args) if (commandLineArguments.isStartNameGenerator()) { - Component dialog = new RandomNameDialog(null, null); - dialog.setVisible(true); + RandomNameDialog dialog = new RandomNameDialog(null); + dialog.showAndBlock(); GracefulExit.exit(0); } 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 - 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..130fe64a228 --- /dev/null +++ b/code/src/resources/pcgen/gui3/namegen/RandomNamePanel.fxml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + +