Repository navigation
fix(settings): apply hero settings keys per the pinned per-hero schema - #509
Conversation
e54-bot
left a comment
There was a problem hiding this comment.
Independent review: verified the diff against the pinned oracle (vendored tools/overpy/oracle, overpy 9.7.10) with ~27 adversarial probe compiles diffed against opy-cli output.
Verified correct: the regenerated hero_applicability.json is byte-identical (node tools/overpy/gen_hero_applicability.cjs → no diff; 19/75/8 split confirmed); hero_setting_applies membership semantics match compileCustomGameSettingsDict's merged-set lookup including the enableGenericSecondaryFire→enableSecondaryFire dest-key rename; verbatim_form/written_form split is right (Raw bypasses name resolution, RawValue resolves canonically — needed at catalogued paths); canonicalization order (group rename → ability1KB% rename → gate on canonical name) is correct; roster lists emit last with canonicalized elements; main/lobby/gamemodes paths untouched by hero logic; unknown heroes still fail downstream; the 4 tests discriminate (they fail without the gate). fmt/clippy/tests all green.
Findings (none block the issue's stated scope, ranked):
- Medium —
general-flattened members get reprocessed as heroes. Inpass_through_unknown_members,general's children are spliced into the teamchildrenbefore the secondfor hero in childrenloop, so aGroupauthored insidegeneralis then canonicalized,ability1KB%-renamed, and hero-gated; ageneral-nestedListnamedenabledHeroesgets its elements canonicalized. Probes:{"general": {"mccree": {"health%": 50}}}→ oracle emits verbatimmccree { health%: 50 }; opy-rs emitsCassidy { Health: 50 }(renamed group + translated label, verbatim value without%).{"general": {"enabledHeroes": ["mccree"]}}→ oracleenabledHeroes { mccree }; opy-rsenabled heroes { Cassidy }.
Upstream compilesgeneral's dict strictly againstgeneral.values— nested members write verbatim with authored names. Fix is cheap: run the hero pass only overrest/hero_lists, or record the general-member count andskip()it in the second loop.
- Medium-low —
ability1KB%rename keeps authored position; upstream moves it to the end. Upstream doesd["ability1Kb%"] = d["ability1KB%"]; delete d["ability1KB%"]— JS insertion order places the renamed key last. Probedva: {"health%":40, "ability1KB%":50, "ultGen%":60}→ oracleHealth / Ultimate Generation… / Boosters Knockback; opy-rsHealth / Boosters Knockback / Ultimate Generation…. Also{"ability1Kb%":10, "ability1KB%":50}→ oracle collapses to oneBoosters …: 50%(overwrite); opy-rs emits both lines. The new tests only use single-member hero dicts so they can't see this. - Low — both
enabledHeroesanddisabledHeroesauthored. Oracle hard-errorsCannot have both 'enabledHeroes' and 'disabledHeroes' in team 'allTeams'; opy-rs emits both in authored order. Pre-existing leniency, but the reorder makes the divergence visible — either reproduce the error or record the exception. - Low — inapplicable catalogued key with non-scalar value errors instead of passing through.
verbatim_formfalls back towritten_formfor List/Group, producing aGroupat a catalogued path thatmember::acceptthen rejects:ana: {"ability3Cooldown%": [11,22]}→settings key 'ability3Cooldown%' does not match its table kind; oracle emits a verbatimability3Cooldown% { 11 / 22 }block. Pathological input and consistent with pre-existing catalogued-kind strictness, but it holes the "fully verbatim" claim for non-scalars. - Low — non-Group
general({"general": 5}): opy-rs errors "outside the emission table"; oracle silently drops it. Arguably better, noting for completeness.
Notes: inapplicable keys warn as unknown settings key 'ability3Cooldown%' — technically catalogued-but-inapplicable; cosmetic. Serde ignores the audit fields (schemaVersion/pinned/derivation/generalKeys) — intentional, though deny_unknown_fields would catch generator drift. genji.enableSecondaryFire → enableSecondaryFire: Off vs oracle Secondary Fire: Off reproduces the recorded wk#414 generic-label gap, as documented.
Core fix is correct and matches the oracle on the issue's examples; findings 1–2 are real upstream divergences on accepted input worth fixing or tracking.
|
Re-review findings addressed in Fixed
Routed to wrightkit/workshop-rs#417 (canonical emitter gap, needs a pass-through escape for non-scalar nodes at catalogued paths):
New tests cover the KB ordering/collapse, general scoping, non-dict |
e54-bot
left a comment
There was a problem hiding this comment.
Follow-up verification of 39e7f96 against the pinned oracle (50 probes, whitespace-normalized content comparison; oracle emits settings\n{\n\t… so raw bytes always differ — semantics/order compared token-for-token).
Claimed fixes — verified
- Flattened
generalchildren: hero passes no longer reach them.general: {"dva": {"ability1KB%": 50}, "damageDealt%": 50}keepsability1KB%verbatim (previouslyBoosters Knockback Scalar) and translatesDamage Dealt: 50%. The residualdva→D.Vagroup-name resolution is emission-side — routed gap, below. ability1KB%assign+delete order: all five order-sensitive probes match the oracle — move-to-end ({ability1KB%, health%}→ Health first,Boosters Knockback Scalarlast) and collapse onto an authoredability1Kb%at the destination position with the source value, in both authored orders.- Both-lists error: verbatim upstream message
Cannot have both 'enabledHeroes' and 'disabledHeroes' in team '<authored>'forallTeams,team1,team2, and when the offending team is not the first authored. An emptyenabledHeroes: []still counts, matching upstream key-presence. - Non-dict
general: number/boolean dropped cleanly, matching oracle. - Error path:
workshop_settings→Resultis handled at both call sites (compiler/lowering.rs:293?,tooling.rs:208match →Diagnostic::from_integration_erroras Error severity).opy compileandopy checkboth surfaceerror[settings-hero-lists]and fail. No other callers. - Regression:
generalscalars translate, general members emit before hero groups, roster lists emit last, roster elements canonicalize (mccree/hammondin lists → Cassidy/Wrecking Ball) — all match oracle. - Gates:
cargo test -p opy-rs(all pass),fmt --check,clippy --all-targetsclean.
Routed residuals — confirmed in scope, nothing more
ana.ability3Cooldown%: [11,22]/{x:1}→ oracle emits the verbatim block; nativeerror[workshop-emission]: settings key 'ability3Cooldown%' does not match its table kind.- General-nested groups/lists: oracle emits
someGroup{a:1},someList{x,y},enabledHeroes{mccree}verbatim; native resolves names (dva→D.Va,enabledHeroes→enabled heroes/Ana) or errorsunknown hero 'mccree'/'someGroup'/'someList'. Exactly the canonical-emitter verbatim escape tracked inwrightkit/workshop-rs#417.
New findings
Medium — hero-group name canonicalization still uses in-place rename (settings.rs:199-203). Upstream canonicalizes mccree/hammond team members by assign+delete (overpy.js:71287-71294) — the same mechanism this commit fixed for ability1KB%, but the group-name rename was left in place. Verified divergences:
{"mccree": {health%: 50}, "ana": {health%: 60}}→ oracle emitsAnathenCassidy(destination appended at end); native emitsCassidythenAna(order flipped). Same forhammond→Wrecking Ball.{"cassidy": {health%: 10}, "mccree": {health%: 50}}(and the reverse order) → oracle collapses to a singleCassidy { Health: 50% }(source value wins at the destination position); native emits twoCassidyblocks (Health: 10%andHealth: 50%).
The same position/remove/find/push treatment used for ability1KB% is needed on the group name: remove mccree/hammond, then overwrite an authored cassidy/wreckingBall group in place or append at the end.
Low — non-dict general drop is broader than upstream (settings.rs:162). Upstream runs compileCustomGameSettingsDict over general and Object.keys of a string or list yields index keys: general: "ab" emits 0: a, 1: b; general: [10, 20] emits 0: 10, 1: 20. Only numbers/booleans produce zero keys and are truly dropped (and null is rejected earlier at settings-parse, which native also rejects). Native drops the string/list forms. Garbage-tier input, but observable.
Low — both-lists check is List-scoped; upstream checks key presence (settings.rs:168-184). "enabledHeroes" in team is true for any value kind: {"enabledHeroes": 5, "disabledHeroes": ["genji"]} or a dict-valued enabledHeroes still produces Cannot have both … upstream. Native misses the presence check for non-list members and instead errors later at emission ('enabledHeroes' does not match its table kind / unknown hero 'enabledHeroes'). Both reject the program — diagnostic parity only.
Informational (pre-existing, not introduced here)
{"ana": [1, 2]}at team level → oracle emitsAna { 0: 1; 1: 2 }; native errorsoutside the emission table.heroes: 5→ oracle emits emptyheroes {}; native errorssettings block children must be groups. Same verbatim-emission class as workshop-rs#417; the_ => {}arms predate this commit.{"ana": 5},allTeams: 5, non-hero team members (foo: 5,foo: {},foo: [1,2]), duplicate keys, and bad roster elements all error on both sides (upstream viatows/TypeError, native via emission/parse errors) — parity holds.- Nit: the stored commit message lost every backticked identifier ("The flattened team members", "renamed in place", "both and ", "A non-dict under a team") — likely stripped during message authoring.
Verdict
The four claimed fixes are real and verified; the two routed residuals reproduce exactly as described and nothing beyond their scope was found. One Medium finding remains: the identical assign+delete semantics for mccree/hammond group names (duplicate Cassidy blocks + ordering) should get the same treatment before merge, or be explicitly deferred with a routed issue.
|
Second re-review round addressed in de5f20e:
All 6 cases probe-verified against the pinned oracle; 21 tooling tests, fmt, clippy green. |
e54-bot
left a comment
There was a problem hiding this comment.
Third re-review round verified de5f20e against the pinned oracle (58 probe compiles, token-level comparison; probes kept under target/review-509-probes/).
Claimed fixes — verified
- Group rename = assign+delete: all single-alias shapes match the oracle exactly — move-to-end (
{"mccree":{"health%":50},"ana":{"health%":60}}→AnathenCassidy), collapse onto the canonical dest in both authored orders (cassidy+mccreeand reverse → singleCassidy { Health: 50% }at the dest position, source members wholesale replacing dest members), mid-list dest (ana, mccree, cassidy→AnathenCassidyat dest index), empty-group collapse, non-Group dests ({"cassidy":[1,2],"mccree":{…}}→ singleCassidygroup), andability1KB%inside a renamed group ({"mccree":{"ability1KB%":30}}→Cassidy { ability1Kb%: 30 }verbatim, correctly gated against cassidy's set). - Non-dict
general:{"general":"ab"}→0: a,1: b;{"general":[10,20]}→0: 10,1: 20; number/bool/empty-string drop — all match, including a hero-named string ("dva"→0: d,1: v,2: a, not aD.Vagroup, confirming flattened members skip the hero pass). - Roster conflict by key presence:
enabledHeroesas number/dict/string +disabledHeroeslist, both-numbers, empty-list, andteam2— all produce the verbatim upstream messageCannot have both 'enabledHeroes' and 'disabledHeroes' in team '<name>'. - Parity holds on rejects: scalar/unknown hero members, duplicate keys,
null, and non-listenabledHeroesalone (upstream TypeError vs native table-kind error — both reject).
Finding
Medium — the two alias renames must run in upstream's fixed order, not authored order (settings.rs:218-237). Upstream performs mccree→cassidy then hammond→wreckingBall as two sequential steps (overpy.js:71286-71294), so when both aliases append they always land cassidy-before-wreckingBall. The single left-to-right pass appends whichever alias is authored first:
{"hammond":{"health%":50},"mccree":{"health%":60}}→ oracleCassidy{60}thenWrecking Ball{50}; nativeWrecking Ball{50}thenCassidy{60}.{"hammond":{…},"genji":{…},"mccree":{…}}→ oracleGenji, Cassidy, Wrecking Ball; nativeGenji, Wrecking Ball, Cassidy.{"disabledHeroes":["mccree"],"hammond":{…},"mccree":{…}}→ oracleCassidy, Wrecking Ball, disabled heroes; nativeWrecking Ball, Cassidy, disabled heroes.
Trigger: hammond authored before mccree with no wreckingBall group present (a wreckingBall dest collapses in place and hides it; cassidy dests don't matter). Alphabetical OW1-era member lists hit this since hammond < mccree. Fix matches upstream structure: run the remove→rename→collapse/append once for mccree, then again for hammond, instead of one generic pass.
Low / route-with-#417
generallist with non-scalar elements:{"general":[[1,2]]}→ oracle emits a0 { 1 / 2 }block; native flattens to0: [1,2].{"general":[{"a":1},"x"]}→ oracle0 { a: 1 }+1: x; native rejects at parse (expected ',' or ']'). The index-keyed flattening assumes scalarelement.value; the structured cases need the same verbatim-emission escape aswrightkit/workshop-rs#417(the dict-element one additionally needs list-element grammar support).- Alias rename is
Group-scoped; upstream's truthiness check covers any value kind:{"mccree":[1,2]}→ oracleCassidy { 0: 1 / 1: 2 };{"mccree":[1,2],"cassidy":{…}}→ oracle replaces cassidy with the list. Native leaves the list namedmccreeand errors at emission — inside the #417 gap today, but when that escape lands this path would still emitmccreeinstead ofCassidy. Worth capturing in the routed issue so the rename isn't silently dict-only. - Astral chars in
generalstrings:{"general":"a😀"}→ oracle iterates UTF-16 units (1: \ud83d,2: \ude00);chars()iterates scalars (1: 😀). Upstream's lone-surrogate output is arguably its own bug — pathological, noting for routing completeness. mccree/hammondscalar values (5,0,false,"x") reject on both sides — upstream via TypeError ("ability1KB%" in 5) ortows, native via emission kind-check. Parity.
Gates
cargo test -p opy-rs --test tooling — 21/21 pass (incl. the new group-rename, non-dict-general, and both-rosters tests). The tests cover single-alias shapes; none exercise the both-alias ordering above, which is how the Medium slipped through.
|
The both-aliases ordering medium is fixed in 67e8925: the rename now runs one pass per entry of HERO_NAME_ALIASES (mccree, then hammond), so appended groups follow alias order rather than authored order. Verified against the oracle for hammond-before-mccree, mccree-before-hammond, and both aliases with canonical destinations authored first. The alias list is now a const table shared by canonical_hero_name so the pass order cannot drift from the map. The three lows are recorded: general list/dict elements and non-group alias values stay routed with wrightkit/workshop-rs#417 (the verbatim-emission wall), and the astral-char case is noted there as pathological UTF-16-vs-chars divergence. |
e54-bot
left a comment
There was a problem hiding this comment.
Follow-up review at 67e8925. Verified the per-alias pass against the pinned oracle (overpy 9.7.10) and opy-cli compile.
Fix verified. HERO_NAME_ALIASES iterates one full pass per alias in upstream's order (overpy.js:71286-71294), and the while scan correctly leaves index unincremented after a remove so consecutive same-alias groups are all handled. Oracle probes — both aliases in both authored orders, aliases interleaved with canonical groups and enabledHeroes, alias+canonical-dest collapse in all four orders, all four names in one team, canonical dest authored as a scalar, ability1KB% inside renamed groups, both teams, empty alias group — are token-identical between the oracle and our output, including the prior Medium shape (hammond before mccree → Cassidy emitted first). Regressions re-checked: ability1KB% assign+delete, general flattening, roster conflict detection all still match; cargo test -p opy-rs --test tooling, fmt, clippy clean.
Non-blocking observations (all predate this commit — the diff touches only the alias loop — but sit in this PR's feature area, flagging for follow-up triage):
generalmembers that are group/list-valued with a non-hero, non-settings name (e.g.general: {"foo": {"x": 1}}or{"mccree": {...}}) hard-errorunknown heroat emission; upstream writes them verbatim insideGeneral(foo { x: 1 }). A group named like a hero (general: {"dva": {"health%": 50}}) resolves and translates (D.Va { Health: 50 }) where upstream stays fully verbatim (dva { health%: 50 }). Upstream compiles general's dict againstgeneral.valuesonly; routing general children through theheroes.<team>path makes hero resolution apply.- List-valued team members (
ana: [1,2],mccree: [1,2]) erroroutside the emission table; upstream emits an index-keyed block (Ana { 0: 1; 1: 2 }). Scalar/string/falsy alias values already fail upstream (TypeError or "No match found for keyword 'mccree'"), so our structured error there is parity-or-better.
Neither blocks this fix; both are narrow edge cases on upstream-accepted input. Consider a follow-up issue for verbatim group emission under heroes.<team> and non-dict team members.
The pinned OverPy compiler merges `__generalAndEachHero__` plus the `__eachHero__` keys a key's include/exclude filters admit into every hero's settings set, then writes authored keys missing from that set back verbatim. opy-rs translated through a hero-agnostic table, so catalogued keys the reference does not apply to a hero emitted canonical names and values (e.g. `ana.ability3Cooldown%` → `50%`). Gate the catalog definition on the merged per-hero applicability set recorded in `data/hero_applicability.json` (generated by `tools/overpy/gen_hero_applicability.cjs` from the pinned compiler's own schema merge), and emit inapplicable members fully verbatim. Also converge the pinned compiler's adjacent rewrites: `mccree`/`hammond` hero aliases in groups and hero lists, the `ability1KB%` → `ability1Kb%` member-key rewrite, and `enabledHeroes`/`disabledHeroes` emission after hero groups. A 53-hero x 102-key oracle differential now diverges only in workshop-rs name data, recorded on the issue and tracked in wrightkit/workshop-rs#414. Fixes #495
Address re-review findings on the per-hero schema gate: - The flattened team members took the hero-name and applicability passes, so a nested hero dict was canonicalized and a general-level roster list had its elements rewritten. Only authored hero groups and roster lists take the hero passes now. - renamed in place; upstream's assign+delete moves the member to the end, or collapses it onto an authored position with the source value. - A team carrying both and emitted both lists; upstream hard-errors with 'Cannot have both ... in team <name>'. - A non-dict under a team now drops silently like upstream instead of failing emission. Two remaining verbatim-emission gaps need a canonical emitter escape and are routed to wrightkit/workshop-rs#417: inapplicable keys with non-scalar values, and general-nested groups/roster lists.
Hero group aliases rename via the same assign+delete order as member keys: the renamed group moves to the end, or collapses onto the canonical group's position with the source members. A non-dict general emits index-keyed members for strings/lists and drops numbers/booleans. The enabled/disabled rosters conflict is detected by key presence regardless of node kind, and flattened general members are kept out of the hero pass.
Upstream assigns mccree then hammond in separate steps, so appended groups follow alias order rather than authored order; a single left-to-right pass flipped the result when hammond was authored first.
67e8925 to
3148c91
Compare
Summary
Fixes #495.
The pinned OverPy compiler merges
__generalAndEachHero__plus each admissible__eachHero__key into every hero's settings set; authored keys missing from the merged set are written back verbatim. opy-rs translated through a hero-agnostic table, so catalogued-but-inapplicable keys emitted canonical names and percent formatting (e.g.ana.ability3Cooldown%→50%,reinhardt.ammoClipSize%→Ammunition Clip Size Scalar).crates/opy-rs/src/compiler/data/hero_applicability.json, generated bytools/overpy/gen_hero_applicability.cjsfrom the pinned compiler's own post-load schema merge (OverPy 9.7.10; issue comments confirm identical in 9.7.17): 19 universal keys, 75 include-form, 8 exclude-form across 53 heroes.pass_through_membersnow gatesworkshop_rs::settings::definitionon that set; inapplicable members take a newverbatim_form(Rawfor scalars) so the authored key spelling survives —RawValuewould still resolve the canonical name at a catalogued path.mccree→cassidy/hammond→wreckingBallin hero groups and roster lists,ability1KB%→ability1Kb%member keys, andenabledHeroes/disabledHeroesemitted after hero groups.Differential (53 heroes × 102 authored keys vs oracle)
All applicability divergences resolved; both issue examples match. Remaining divergences are workshop-rs name-data gaps, recorded on #495 and tracked in
wrightkit/workshop-rs#414:enableAutomaticFire/enableScopinghard-error on ana/ashe/widowmaker (missingprimaryFireslot mapping).Secondary Fireliteral for ~14 heroes (blank export label).Pig Pen/Take a Breather/Secondary Fire).Verification
cargo test --workspace --all-targets --all-features— all green (incl. 4 new regression tests throughcompiled_lines)cargo fmt --all -- --check,cargo clippy --workspace --all-targets --all-features -- -D warnings,git diff --check— cleanpython3 -m unittest discover -s tools/overpy/tests— 40 passed