Repository navigation
Define the elements in the element table (breaking) - #10
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #10 +/- ##
==========================================
+ Coverage 63.12% 68.03% +4.91%
==========================================
Files 43 43
Lines 1524 1627 +103
Branches 346 385 +39
==========================================
+ Hits 962 1107 +145
+ Misses 457 412 -45
- Partials 105 108 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@imbas-forosnai — please review this PR as well. GitHub cannot request your review here, because the account is not a collaborator on this repository. |
imbas-forosnai
left a comment
There was a problem hiding this comment.
I'm Imbas (@imbas-forosnai), the AI collaborator pattern J works with. Angelique asked for this review. It was done at d6550cc5 by two independent reviewers: me, and Codex reading cold. I checked every finding below against the source, mostly by running probes. This is comment-only, because approving in this repo is J's call.
The registry itself looks solid. There is one thing to fix before merging, which is the rebase, and two mediums in validation. I've included a tested resolution for the rebase.
What was verified
- Generated code.
tools/regenerate.shon Linux (xscgen 3.0.1356) reproducessrc/Objects/with no diff, and it does not recreateElementType.cs. - Build and tests. The library builds on net8, net9 and net10. All 164 tests pass against ceridwen HEAD.
- Schema constraints. Codex ran mutation tests on the
xs:key,xs:uniqueandxs:keyrefconstraints, and each one rejects what it should: duplicate keys, ids, sources and targets, and undefined references.
Before merge: the rebase onto main (found by Imbas; resolution reviewed by Codex)
main now includes #11 (HS-1552). A trial merge of d6550cc5 onto origin/main shows two problems:
src/Extensions/Variant.csconflicts. Taking this branch's side would silently drop every #11 fix: the Physical precedence fix, the SmallMax scaling (this branch still computes SmallMin twice), the flag union, the tag dedupe, the guards for an item with no Equipment, no Damage or no Restrictions, the damage clamp, and the level default.tests/VariantResolution.csmerges without a conflict, then fails to compile. It still usesElementType, so the build has eight CS0103/CS0246 errors.
The resolution below starts from main's Variant.cs and changes only the element checks to string form. With it, the merged tree passes 181/181 (164 from here plus 17 from #11). Codex reviewed it and found that it keeps both branches' intent.
--- a/src/Extensions/Variant.cs
+++ b/src/Extensions/Variant.cs
@@ public Item ResolveVariant(Item originalItem)
if (Properties.StatModifiers is { } modifiers)
{
variantItem.Properties.StatModifiers += modifiers;
- // The generated element properties have no Specified flag, so None is the only way to tell an
- // element the variant leaves unset; the clone already holds the base item's.
- if (modifiers.BaseDefensiveElement != ElementType.None)
+ // An element the variant leaves unset is null; the clone already holds the base item's.
+ if (!string.IsNullOrEmpty(modifiers.BaseDefensiveElement))
variantItem.Properties.StatModifiers.BaseDefensiveElement = modifiers.BaseDefensiveElement;
- if (modifiers.BaseOffensiveElement != ElementType.None)
+ if (!string.IsNullOrEmpty(modifiers.BaseOffensiveElement))
variantItem.Properties.StatModifiers.BaseOffensiveElement = modifiers.BaseOffensiveElement;
}
--- a/tests/VariantResolution.cs
+++ b/tests/VariantResolution.cs
@@
- [InlineData("""<StatModifiers BonusHp="5" />""", ElementType.Fire, ElementType.Water)]
- [InlineData("", ElementType.Fire, ElementType.Water)]
- [InlineData("""<StatModifiers BaseDefensiveElement="Earth" />""", ElementType.Earth, ElementType.Water)]
+ [InlineData("""<StatModifiers BonusHp="5" />""", "Fire", "Water")]
+ [InlineData("", "Fire", "Water")]
+ [InlineData("""<StatModifiers BaseDefensiveElement="Earth" />""", "Earth", "Water")]
public void ResolveVariant_KeepsBaseElements_UnlessTheVariantSetsItsOwn(string variantProperties,
- ElementType defensive, ElementType offensive)
+ string defensive, string offensive)The Variant.cs file is CRLF with a BOM, so keep both when you edit it. A text-mode rewrite turns the whole file into the diff.
Medium: a missing required attribute loads as a default, and Validate() doesn't catch it (found by Codex; confirmed by Imbas)
World data is loaded through a plain XmlReader (HybrasylEntity.LoadFromFile, line 90), so the schema's use="required" is never enforced at runtime. Two cases:
- An
<Element Key="Fire"/>with noIddeserializes toId = 0.Validate()reports nothing, because 0 is a legal id and is unique as long as no other element uses it, so Fire goes out on the wire as id 0. - A
<Group>with noKey, and a table with no<Source>rows, load cleanly in the same way.
My probe printed Fire.Id=0; Source rows=0; Validate: no errors.
Id is the field that goes on the wire, and this PR depends on load-time validation, so this is the place to close the gap. There are two options, and the choice is yours:
- Schema-validate production loads. This is the broader change: it touches the loader for every type.
- Keep a record of whether each required attribute was present (for example, make
Idoptional in the XSD so that the generator emitsIdSpecified), and haveValidate()reject anything missing.
A related case: an explicit empty element string, such as BaseDefensiveElement="", also gets past the schema at load. The IsNullOrEmpty check in the resolution above treats it as "not set", which is the same behaviour as this branch's own Variant.cs. Whichever option you choose, it should reject empty element strings too.
Medium: duplicate group members weight the random pick (found by both reviewers independently)
Members="Fire Fire Water" passes both the schema and Validate(), because xs:list cannot enforce uniqueness. The reservoir sampling in TryResolve then goes through the list entries, not the distinct keys. Out of 30,000 picks of Random:Dup, Fire came up 19,941 times and Water 10,059. So a typo in content silently changes the balance, and the comment's "uniform over the defined members" doesn't hold.
The fix belongs in Validate(): report duplicate members, compared with KeyComparer, in the same way it already reports duplicate keys. Deduplicating only inside TryResolve would leave the bad data unreported.
Checked and fine
GetResultSummariesnow readsGetValidationResult, which fixes the bug where validation results were swallowed.- Every element comparison goes through
KeyComparer.CreatureBehaviorSet.ImmuneToElementno longer depends on the current culture. TryResolvenever picks an undefined member, and it refuses creature selectors.GetMultiplierfalls back from the target, to the row default, to the table default.
|
Imbas again (@imbas-forosnai). I'm withdrawing one finding from my review above. J and Angelique have confirmed that duplicate group members are intentional: listing a member twice gives it twice the weight, so To make the code say that rather than contradict it:
The rebase item and the required-attribute medium still stand. |
The ElementType enum lists the elements, so each change to the set needs a schema change, a regeneration and a package release. The element table now defines the elements as world data. The element table gets these parts: - <Elements>: one <Element> for each element. Key is the name that content uses. Id is the modern wire value (0 to 254). RetailId is the retail wire value (0 to 9, default 0). Name, Default and Ivernic are optional. - <Groups>: named sets of elements. Content picks a random member with Random:<Key>. - A DefaultMultiplier on each Source row. Target becomes optional. - xs:key and xs:keyref rules, so each Source and Target names an element that the table defines. Content now refers to an element by key (ElementKey). Castable and spawn Elements take a list of selectors (ElementSelectorList): an element key, Random:<Group>, Current, Belt or Necklace. The generated properties are strings. Null means "not set". ElementTable gets DefaultElement, TryGetElement, TryGetGroup, TryResolve and Validate. GetMultiplier takes keys, and uses the Target multiplier, then the row default, then the table default. TryResolve accepts RandomTemuair and RandomExpanded as old names for Random:Cardinal and Random:Ordinal. Spawn and Castable return the selector, and do not resolve it. The server resolves it against the active table. Variant takes each element that the variant does not set from the base item. This change removes ElementType from the package and from tools/enum-values.json. It breaks code and content that use ElementType, None or Wood. BREAKING CHANGE: ElementType is removed. Element properties are strings, and the element table needs an <Elements> block.
GetResultSummaries reads the validation stage with GetProcessResult, so no validation result reaches a summary. CreatureBehaviorSet hides this, because its ValidateAll writes an XmlProcessResult. That also replaces the result of its ProcessAll pass. The validation stage now reads GetValidationResult. A new XmlValidationResult implements IValidationResult, and CreatureBehaviorSet.ValidateAll uses it.
- ElementTable implements IAdditionalValidation. The manager runs Validate on each table at load, and the result goes into the validation summary. - TryResolve picks only from the group members that the table defines. It uses one pass and no list. CanResolve uses TryResolve. - A new GetMultiplier overload takes resolved elements. - ElementTable.KeyComparer is the one comparer for keys. CreatureBehaviorSet and StatModifiers use it. Before this change, ImmuneToElement compared with the current culture. - Validate accepts null collections, and uses one Duplicates helper. - MaxRetailId and TryGetGroup are private.
- StatModifiers: test GetElementalAugment and GetElementalResistance. Test that the key comparison ignores case. Test that the result is 1.0 when no entry has the type and the element, and when the list is null. - ElementTable.Validate: test the duplicate element id, the element without a key, the duplicate group key, the duplicate source row and the duplicate target. Only a table built in code gets to these checks, because the schema rejects them in a file. - Castable.Element: test no elements, one selector and more than one selector. - Spawn: test a pick from more than one element. Refs: HXML-24
d6550cc to
bc6166d
Compare
- Validate reports a group with no key and a table with no Source rows. The schema requires both, but the loader does not check files against the schema. - A group member listed more than once is picked more often. This is a feature: content uses it to weight a group. The TryResolve comments and the schema now say so, and a test covers it. Refs: HXML-24, HXML-35
|
Thank you, @imbas-forosnai. The branch is rebased onto RebaseI used your resolution. Medium: missing required attributes load as defaultsThis is a real gap, and you found the cause: the loader never checks a file against the schema, for any type. A trait with no In this PR, Medium: duplicate group membersThis behaviour is intentional. We discussed it and keep it as a feature: content weights a group by listing a member more than once, for example |
Summary
The
ElementTypeenum lists the elements today. Each change to the element set needs a schema change, a regeneration and a package release. This PR moves the element set into the element table, as world data. It is a breaking change (HXML-24).This PR also fixes a bug on
main: no validation result reaches the result summaries (see "Validation summaries").Element table
<Elements>is required.Keyis the name that content uses, and it must not change.Idis the modern wire value (0 to 254, and 255 means "no element").RetailIdis the retail wire value (0 to 9, default 0). The retail client indexes a 10-entry name table with this byte and does not check the range.Name,DefaultandIvernicare optional.<Groups>is optional. Content picks a random member of a group withRandom:<Key>. Each entry inMembersis one chance, so a member listed twice is picked twice as often. Content can use this to weight a group.Sourcerow can have its ownDefaultMultiplier, andTargetis now optional.xs:keyandxs:keyrefmake sure that eachSourceandTargetnames an element that the table defines. Element keys, element ids and group keys must be unique.Targetmultiplier, then the rowDefaultMultiplier, then the tableDefaultMultiplier.Content
StatModifiersBaseOffensiveElement,BaseDefensiveElement,OffensiveElementOverride,DefensiveElementOverrideElementalModifier/@Element,StatusDamage/@ElementCastable/@Elements,SpawnDamage/@Elements,SpawnDefense/@ElementsA selector is an element key,
Random:<Group>,Current,BeltorNecklace. The schema checks the form of a key ([A-Za-z][A-Za-z0-9]*). It cannot check a key against the element table, because the table is a different document.API changes
ElementTypeis removed, and its entry intools/enum-values.jsonis removed.string.nullmeans "not set". There is noNone.ElementTablegetsDefaultElement,TryGetElement,TryResolve,CanResolveandValidate.GetMultipliertakes keys, and an overload takes resolved elements.ElementTable.KeyComparercompares keys without case, and every element-key comparison uses it.TryResolveresolves an element key,Random:<Group>, and the old namesRandomTemuairandRandomExpanded. It never picks a group member that the table does not define. It returns false forCurrent,BeltandNecklace, because these need creature state.CanResolvegives the same answer without a pick, and is true for those three. The server can use it to check content at load.ElementTableimplementsIAdditionalValidation. The manager runsValidateon each table at load.Validatechecks the rules that the schema cannot check: exactly one default element, reserved keys, group members, and wire values in a table built in code. It also checks a group with no key and a table with noSourcerows, because the loader does not check files against the schema (HXML-35).CardinalandOrdinalgroups. Before this change,RandomTemuairpicked by enum position, so it also picked Light and Dark.Spawn.OffensiveElement,Spawn.DefensiveElementandCastable.Elementreturn the selector. They do not resolve it.Item.Elementreturns a key ornull.StatModifiers.GetElementalAugmentandGetElementalResistancetake a key.CreatureBehaviorSet.ImmuneToElement(ElementType, …)is removed. Thestringoverload stays, and now compares withKeyComparer(before this change, it compared with the current culture).Variantkeeps the code from fix ResolveVariant's arithmetic (HS-1552) #11. Only its element checks change: an element that the variant leaves unset isnullor empty, notNone. The element tests from fix ResolveVariant's arithmetic (HS-1552) #11 use keys.Validation summaries
GetResultSummariesread the validation stage withGetProcessResult, so no validation result reached a summary.CreatureBehaviorSet.ValidateAllhid this, because it wrote anXmlProcessResult, which also replaced the result of itsProcessAllpass. The validation stage now readsGetValidationResult. A newXmlValidationResultimplementsIValidationResult, andCreatureBehaviorSet.ValidateAlluses it. This fix is its own commit.What must change with this release
ServerConfig, resolve selectors withTryResolve, sendRetailIdin 0x08, and useGetMultiplier. The server cannot take this package before that change.world,ceridwen): each element table needs an<Elements>block.Nonebecomes an absent attribute, andWoodbecomesNature. An old table still loads, but it defines no elements, and its validation summary reports errors.Generated code
tools/regenerate.shran on Windows, and the output was normalized to match a Linux run (HXML-26). The script does not delete the file of a type that the schema removes, so this PR deletessrc/Objects/ElementType.csby hand.Tests
184 pass, with the 17 tests from #11. The element table tests cover the schema rules (
Elements, keys, ids, groups,keyref), the three-step multiplier lookup,TryResolveandCanResolve,Validate, the weight of a repeated group member, and the validation summary.Other tests cover
StatModifiers.GetElementalAugmentandGetElementalResistance,Castable.Element, and a spawn element pick from more than one element.Validatetests also use tables built in code, because the schema rejects duplicate ids, group keys, source rows and targets in a file beforeValidatesees them. The changed lines that are not covered are null checks only (?.).Not in this PR
1.0.0-alpha2. This is a breaking change, so the version is the maintainers' choice.