Skip to content

Add TYPE= filter clause to COMPANIONLIST — issue #7555 (comment)#7660

Draft
Vest wants to merge 6 commits into
PCGen:masterfrom
Vest:companionlist-type-filter
Draft

Add TYPE= filter clause to COMPANIONLIST — issue #7555 (comment)#7660
Vest wants to merge 6 commits into
PCGen:masterfrom
Vest:companionlist-type-filter

Conversation

@Vest

@Vest Vest commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Adds a fourth race-clause form to the COMPANIONLIST global LST tag so data authors can filter companion candidates by the race's own TYPE: tag instead of hand-enumerating every eligible race.

Requested by @LegacyKing in issue #7555 comment — the concrete driver is the Empyreal Knight / Shining Knight / similar archetype mount-choice lists in data/pathfinder/paizo/roleplaying_game/ultimate_combat/uc_abilities_class.lst and siblings, which currently hand-list every eligible race and go stale when new mounts arrive from later sourcebooks. With this PR, tagging a race once with e.g. TYPE:EmpyrealKnightMount makes it available to every archetype that uses that filter.

New syntax

COMPANIONLIST:<companionType>|<clause>,<clause>,…[|FOLLOWERADJUSTMENT:<n>][|PRExxx:…]

<clause> ::= <literal race name>
          |  ANY
          |  RACETYPE=<name>
          |  RACESUBTYPE=<name>
          |  TYPE=<name>{.<name>}         ← new
  • TYPE=Foo — every race whose TYPE: tag contains Foo.
  • TYPE=Foo.Bar — dotted AND, races carrying both Foo and Bar (matches every other TYPE= in PCGen).
  • Multiple TYPE= clauses in the same tag union: Pet|TYPE=Animal,TYPE=Vermin means Animal or Vermin races.
  • Composes with everything else: Pet|Cat,TYPE=MyCompanion,RACESUBTYPE=Fire,RACETYPE=Animal|FOLLOWERADJUSTMENT:-3 is valid.
  • ANY + TYPE=Foo is rejected, same as ANY,Wolf today (existing guard, regression test added).

Parse-time validation

Malformed clauses fail loudly at load:

Input Result
TYPE= Fail — empty payload
TYPE=.Foo Fail — leading empty segment
TYPE=Foo. Fail — trailing empty segment
TYPE=Foo..Bar Fail — inner empty segment
ANY,TYPE=Foo Fail — ANY conflicts with any specific clause

Unknown type names are not diagnosed at parse — PCGen loads types lazily, so a typo silently resolves to an empty group. This is deliberate and matches every sibling TYPE= in PCGen.

Implementation

  • CompanionListLst.java — one new else if (tokString.startsWith("TYPE=")) branch in the existing race-clause loop, sitting between RACESUBTYPE= and the prereq guard. Uses context.getReferenceContext().getCDOMTypeReference(Race.class, types) — the same helper WeaponProf, ArmorProf, Equipment, ClassesToken, Changeprof, and SpellCasterAdd all use. That returns a CDOMGroupRef<Race> whose getLSTformat() re-emits TYPE=Foo.Bar, so unparse round-trips for free.
  • CompanionListLstTest.java — 5 new invalid-parse tests (four malformed forms + ANY,TYPE= conflict) and 4 new round-trip tests (TYPE=Animal, TYPE=Animal.Magical, TYPE=Animal,TYPE=Vermin, and a mixed-clause coverage that combines literal race + RACESUBTYPE= + RACETYPE= + TYPE= + FOLLOWERADJUSTMENT).
  • globalfilesother.html — new Variables used (y) line for TYPE=Text[.Text...] (marked new in 6.09.08) plus a matching example block, mirroring the existing RACETYPE= / RACESUBTYPE= doc rows. Javadoc header of the token class updated the same way.

No production data files touched. No CDOM or reference-context changes.

Verified locally

Command Result
./gradlew :test --tests plugin.lsttokens.CompanionListLstTest --rerun-tasks 46/46 pass
./gradlew :itest --tests plugin.lsttokens.editcontext.CompanionListIntegrationTest --tests tokencontent.GlobalCompanionListTest --rerun-tasks 33/33 pass
./gradlew :test (full root unit-test suite) BUILD SUCCESSFUL
./gradlew datatest (all LST data loads end-to-end) BUILD SUCCESSFUL

Out of scope

The underlying Empyreal Knight / Wolfdog mount-choice bug in issue #7555 is a data-layer fix (LST files under data/pathfinder/paizo/roleplaying_game/ultimate_combat/). This PR ships the tool the data team needs; the data patch is a separate change.

Vest added 5 commits July 4, 2026 14:47
New race-clause form on the COMPANIONLIST global LST tag:
    COMPANIONLIST:Pet|TYPE=MyAnimalCompanion
    COMPANIONLIST:Pet|TYPE=Animal.Magical      (dotted = AND)
    COMPANIONLIST:Pet|TYPE=Animal,TYPE=Vermin  (multi-clause = OR)

Matches every Race whose TYPE: tag carries all the listed types.
Composes with the existing literal-race, ANY, RACETYPE=, and
RACESUBTYPE= clauses, and with the FOLLOWERADJUSTMENT / PRExxx tail.
Uses ReferenceContext.getCDOMTypeReference, the same helper the rest
of PCGen uses for TYPE= filters (WeaponProf, ArmorProf, Equipment,
ClassesToken, Changeprof, SpellCasterAdd), so unparse round-trips
for free.

Parser rejects malformed clauses at load time:
    TYPE=            → empty payload
    TYPE=.Foo        → leading empty segment
    TYPE=Foo.        → trailing empty segment
    TYPE=Foo..Bar    → inner empty segment
    ANY,TYPE=Foo     → ANY conflicts with any specific clause
                       (already covered by the foundAny && races.size()>1
                       guard, added a regression test)

Requested by LegacyKing in PCGen#7555 so archetype mount-choice
lists (Empyreal Knight, Shining Knight, and similar) can filter by a
Race TYPE tag instead of hand-enumerating every eligible race, making
new-source mounts a single-line data change.

Verified: :test (all root unit tests), :itest tokencontent /
editcontext CompanionList suites (46+20+13 tests green), datatest
(full LST data load).
…valid-parse assertions

Silences SonarLint S2699 ("Tests should include assertions") false
positives on the four testInvalidType* tests. Also strengthens the
failure diagnostic: on a red run the assertion now prints which
malformed TYPE= clause was expected to fail, instead of just
"expected: <false> but was: <true>".

No behavior change; 46/46 tests still pass.
… invalid-parse tests

Belt-and-braces: SonarLint S2699 ("Tests should include assertions") was
flagging the four testInvalidType* tests despite the assertFalse(parse)
call and the inherited assertNoSideEffects() helper. Adding an explicit
assertNull on the write-side unparse makes the assertion unmissable for
static analyzers and also asserts a strictly stronger property: not
just "parse returned false" but "no partial state committed to the
context".

No behavior change; 46/46 tests still pass.
…sts into two @ParameterizedTest methods

Silences SonarLint S5976 ("Replace these N tests with a single Parameterized
one") and the trailing S2699 ("Add at least one assertion to this test
case") false positives in one pass. Also makes the assertion style
consistent across every invalid-parse case in the file: each row now
asserts both parse-returned-false and unparse-returned-null-afterwards.

- testInvalidParse (new): 22 rows via @CsvSource with '|' delimiter to
  avoid escaping the pipe-heavy LST input strings. Each row carries the
  original test method's name as its case label so a red run still
  points at the specific failing case ("testInvalidRaceCommaEnding:
  expected parse to fail for input <Familiar|Lion,>").

- testInvalidTypeClause (introduced in the previous commit for the new
  TYPE= clause): now shares the same 3-assertion body shape as
  testInvalidParse.

- Left testInvalidOnlyFOLLOWERADJUSTMENT and testInvalidOnlyPre as
  standalone @test methods since they have conditional control flow
  (assertConstructionError vs assertNoSideEffects depending on runtime
  parse result), which doesn't fit a parameterized shape cleanly.

Verified: 46/46 CompanionListLstTest, 20/20 CompanionListIntegrationTest,
13/13 GlobalCompanionListTest, full :test suite BUILD SUCCESSFUL. Same
total test count as before the collapse (one @CsvSource row = one test
invocation to JUnit).
- S2699 (Add at least one assertion): after runRoundRobin(...) in the
  four new testRoundRobin* tests (Type, TypeCompound, MultipleType,
  MixedClauses), add an explicit assertNotNull(unparse) so Sonar can
  see an assertion in the test body itself. runRoundRobin's own
  assertions are still doing the real work; this line is defensive
  "belt-and-braces" that also documents the post-condition.

- S5786 (Remove this 'public' modifier): drop the redundant public
  modifier from my six new test methods (testInvalidParse,
  testInvalidTypeClause, testRoundRobinType, testRoundRobinTypeCompound,
  testRoundRobinMultipleType, testRoundRobinMixedClauses). Follows
  the JUnit 5 idiom and matches the recent repo-wide cleanup in
  aa06fa3 (which happened to miss this file). Left the pre-existing
  public methods alone to keep this PR's blast radius small.

Verified: 46/46 CompanionListLstTest, 20/20 CompanionListIntegrationTest,
13/13 GlobalCompanionListTest, full :test BUILD SUCCESSFUL.
@Vest Vest marked this pull request as draft July 4, 2026 14:21
Not caused by the COMPANIONLIST TYPE= feature, but the round-robin
tests and residual scaffolding were carrying a long tail of SonarLint
warnings that made the fresh flags on my new tests hard to see. Handled
in one pass so the file is a clean starting point:

- S5786 (Remove this 'public' modifier): stripped from the class
  declaration and every remaining test method. Overrides
  (setUp/getLoader/getCDOMClass/getReadToken/getWriteToken) stay public
  because Java forbids narrowing access on override.

- S2699 (Add at least one assertion): appended
  assertNotNull(getWriteToken().unparse(primaryContext, primaryProf))
  after runRoundRobin(...) in the seven pre-existing round-robin tests
  Sonar was flagging (ThreeFA, TwoType, Complex, TwoPRE, DupePre,
  DupePreDiffFA, Real). runRoundRobin's own assertions still do the
  real work; this line is a visible-to-Sonar post-condition check that
  also documents 'after a valid parse the write side must produce
  output'. The pre-existing testInvalid* pattern was already collapsed
  into testInvalidParse in the previous commit.

- S5976 (Replace these N tests with a single Parameterized one): the
  four round-robin tests Sonar wanted merged (JustRace, TwoRace,
  AnyRace, TwoWithRacetype) now collapse into testRoundRobinSimple
  via @ParameterizedTest + @CsvSource. Tab delimiter avoids escaping
  the pipe-heavy LST inputs. testRoundRobinFA fits the same shape and
  joins the parameterized set. Case labels ensure a red run still names
  the specific failing input.

- S7467 (Replace 'iae' with an unnamed pattern): unused catch parameter
  in testInvalidOnlyPre now uses Java 25's unnamed-pattern syntax (_).

- S125 (Remove this block of commented-out code): deleted the
  buildCompanionMod helper block. That code was added in 2012
  (c5918d7) as private-but-never-called scaffolding for potential
  CompanionMod x CompanionList TYPE tests, then commented-out in 2014
  (00e63c2) instead of deleted. Twelve years later, the APIs it
  references (primaryContext.ref, ReferenceContext.reassociateCategory)
  no longer exist -- uncommenting would not compile. git log -S
  "buildCompanionMod" retrieves the block if it's ever needed.

Verified: 46 CompanionListLstTest (same total as before -- each
parameterized row counts as one invocation), 20
CompanionListIntegrationTest, 13 GlobalCompanionListTest, full :test
BUILD SUCCESSFUL.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant