Skip to content

fix: correct misplaced parens in generated regen formula defaults (regen was silently 0) - #5

Merged
baughj merged 1 commit into
mainfrom
fix/regen-formula-parens
Aug 2, 2026
Merged

baughj merged 1 commit into
mainfrom
fix/regen-formula-parens

Conversation

@baughj

@baughj baughj commented Aug 2, 2026

Copy link
Copy Markdown
Member

Problem

HP/MP regen has been silently zero for any world whose config omits a Formulas
section — which includes ceridwen, whose xml/serverconfigs/config.xml contains no
Formula elements at all, so the hardcoded C# defaults are in use.

src/Objects/ServerFormulas.cs is <auto-generated> by Xsd2Code++ and had drifted
from the XSD:

Min( MAXHP * (0.1 + Max(CON, CON-LVL) * 0.01, MAXHP * 0.20) )
             ^ this paren should close after * 0.01

Min() therefore received a single argument. NCalc throws
NCalcEvaluationException: Min() takes exactly 2 arguments; Hybrasyl's
FormulaParser.Eval catches all exceptions and returns 0.0; World.cs casts that
to (uint) — so regen is 0.

How the drift happened

  • a5759ab fixed the parens in both the XSD and ServerFormulas.cs, but to the
    wrong parenthesization (Min still got one argument).
  • 5a1a080 ("fix misplaced parens in HpRegenPerTick/MpRegenPerTick formulas")
    corrected the XSD onlyServerFormulas.cs was never regenerated.

So the XSD has been right since April and the generated C# has been wrong since.

Fix

Regenerate-equivalent hand-patch of the two literals to match the XSD. The XSD is
unchanged and already correct, so a future Xsd2Code++ run will now agree with this
file.

Verification

  • Both C# literals confirmed byte-identical to their XSD default= attributes by
    extracting and comparing them programmatically (not by eye — an eyeballed paren in a
    wrapped string literal is how this was introduced).
  • Defaults extracted from the patched file and evaluated through NCalcSync 6.3.1
    using FormulaParser.Eval's semantics, for a level-10 character with CON/WIS 30 and
    1000 max HP/MP:
[PASS] _mpRegenPerTick          = 200      (was: throws -> 0)
[PASS] _hpRegenPerTick          = 200      (was: throws -> 0)
[PASS] _xpToNextLevel           = 250000
[PASS] _hpGainPerLevel          = 161.36
[PASS] _allowedCarryWeight      = 100.5
[PASS] _allowedEquipmentWeight  = 50.25
  • dotnet build across all four TFMs: 0 Error(s).

Behaviour change — please read

This restores a cap that has not been functioning. With correct parens the Min binds
at CON >= 10, so HP regen becomes a flat 20% of max HP per tick for essentially
any character. That is exactly what the XSD specifies, but since regen has effectively
been 0, the swing is large and worth a balance look before this reaches players.

Release note

Package version bumped 0.9.60.9.7. Publishing is triggered by pushing a v*
tag (or manual workflow_dispatch), not by merging this PR — the package won't
exist on nuget.org until tagged. hybrasyl-server pins Hybrasyl.Xml at 0.9.6 via
PackageReference, so it will not pick this up until the tag is pushed and the
reference bumped (or it is built with UseLocalXml=true).

Context

Found while investigating a claim that NCalc truncates integer division. That claim was
measured false — 25 / 1000.25 [Double] on 6.3.1, 5.8.0, and master — and the
regen symptom traced entirely to these parens. Full write-up in comhaigne
(docs/plans/hybrasyl-server/ncalc-integer-division.md).

Generated with Imbas imbas@eris.co

ServerFormulas.cs is <auto-generated> by Xsd2Code++ and had drifted from the
XSD. Commit 5a1a080 ("fix misplaced parens in HpRegenPerTick/MpRegenPerTick
formulas") corrected src/XSD/ServerConfig.xsd but did not regenerate the C#,
so the generated defaults kept the earlier, wrong parenthesization from
a5759ab:

  Min( MAXHP * (0.1 + Max(CON, CON-LVL) * 0.01, MAXHP * 0.20) )
               ^ should close after * 0.01

Min() therefore received a single argument and NCalc threw
"Min() takes exactly 2 arguments". Hybrasyl's FormulaParser.Eval catches all
exceptions and returns 0.0, so HP/MP regen was silently zero for any world
whose config omits a Formulas section (ceridwen's config.xml has none, so the
hardcoded defaults were in use).

The XSD is already correct; a future regeneration will now agree with this
file. Both literals verified byte-identical to the XSD defaults by extracting
and comparing them programmatically, and evaluated through NCalc: regen now
yields 200 for a level-10/CON-30/1000-maxHP character instead of 0.

Note this changes live behaviour: with the cap restored, Min() binds at
CON >= 10, so regen is 20% of max HP per tick for most characters. That is
what the XSD specifies, but it has effectively been 0, so the swing is large.

Bumps package version to 0.9.7.

Co-Authored-By: Imbas <imbas@eris.co>
@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 61.24%. Comparing base (8953d25) to head (6e2d3ca).

Additional details and impacted files
@@           Coverage Diff           @@
##             main       #5   +/-   ##
=======================================
  Coverage   61.24%   61.24%           
=======================================
  Files          40       40           
  Lines        1538     1538           
  Branches      350      350           
=======================================
  Hits          942      942           
  Misses        475      475           
  Partials      121      121           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@baughj
baughj merged commit 455dd00 into main Aug 2, 2026
3 checks passed
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