Skip to content

Merge develop to main (0.9.5ish)#334

Merged
baughj merged 122 commits into
mainfrom
develop
May 15, 2026
Merged

Merge develop to main (0.9.5ish)#334
baughj merged 122 commits into
mainfrom
develop

Conversation

@baughj

@baughj baughj commented May 15, 2026

Copy link
Copy Markdown
Member

Description

develop has existed long enough. Time to end that. All development will be on main from here on out, and main will actually reflect the state of the project. No need for these god awful long lived branches.

baughj added 30 commits July 22, 2024 15:55
…legends; reorganization of chat commands into single files
* Clean up world init

* Fix longstanding bug in jobs

* Humanize display of job intervals

* Handle OnSpawn() for monsters now that script copies are no longer a thing

* Cleanups in various places
…se conditions

* Fix bug with Disoriented (cut / paste fail)

* Add DirectHeal / DirectDamage to be used by scripting, which will take new conditions into
  account
… gained on level, XP to next level, etc

* Support Reactor display names

* Use NPC display names correctly
…otations

* Add support for several new monster targeting conditions
* Misc fixes
Caeldeth and others added 27 commits April 22, 2026 21:43
- Species system: 50 species enum, ushort sprite index, ServerConfig mapping
- Castable FormulaVariables: CastableObject wrapper following ItemObject pattern
- Chair sitting: walk-into-wall detection, client sprite fold, turn in place
- Clan names: account-level identifier, new DisplayUser field
- Account manager: scoping notes for account entity and auth refactor
- NCalc integer division: regex fix for int/int truncation in all formulas

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allow building against a local Hybrasyl.Xml project by adding a conditional ProjectReference (controlled by UseLocalXml and LocalXmlProjectPath) while keeping the NuGet PackageReference as the default. Add documentation (docs/epona-branch-instances.md) describing Epona's branch-aware server instances, worktree strategy, and the generated Directory.Build.props file used to switch to local XML branches. Update .gitignore to ignore .worktrees/ and Directory.Build.props so Epona-managed worktrees and build overrides remain out of version control.
Guard Console.ReadKey() in Game.cs with !Console.IsInputRedirected so
the "press any key to exit" pause only runs under a real console. When
the server is launched by a wrapper that pipes stdio (Epona embedded
mode, CI), World.Init() failure now exits cleanly with code 1 instead
of throwing InvalidOperationException. Unchanged for standalone
terminal runs. Enables Epona's embedded-console LogPane path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Feature/epona branch instances into develop 🧠
doors.md's array order isn't a fixed [low-coord, high-coord] invariant —
several retail definitions (e.g. 18610 N/S) catalogue the south tile's
sprite at index 0 and the north tile's at index 1. The directional walk
in TryBuildDoorGroup assumed the opposite, so on Abel every correctly-
placed inverted-order door produced two "panel N expected sprite X/Y not
found" warnings and was skipped — leaving the tiles non-functional and
unclickable.

Replace the directional walk with an orientation-agnostic sweep that
walks both directions along the axis from each toggling-panel anchor and
places tiles by the panel index encoded in their sprite via SpriteInfo.
Center-only doors keep their existing semantics: the side panels go at
the tiles immediately adjacent to the center without sprite validation
(per doors.md, side-panel sprites are static jamb art).

Factor the sweep into a static ScanForDoorGroups returning a
DoorScanResult so the scanner is unit-testable in isolation. Add 12
new map-load tests covering forward/reversed N/S and E/W 2-tile,
3-tile center-only, 3-tile all-change reversed, open-state, mixed
open/closed, partial doors, lone-edge anchor, two same-def doors, and
dedup-from-both-panels. All 153 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nings

Reactor.OnSpawn cleared DialogSequences before re-running the script's OnSpawn
function but left SequenceIndex populated. RegisterDialogSequence checks
SequenceIndex for duplicates, so every re-registration after a second OnSpawn
tripped a "Dialog sequence X is being overwritten" warning per registered name.
On Abel/Meena that fired ~10 warnings at startup; merchants don't see this
because Merchant.OnSpawn already calls IPursuitable.ResetPursuits, which clears
all three collections together. Match that pattern in Reactor.

The double OnSpawn that surfaces this is itself worth a separate look — World.Insert
fires OnSpawn for any ISpawnable, and MapObject.InsertReactor / InsertNpc each
call it explicitly a second time. ScriptProcessor.ReloadScript also re-fires
OnSpawn on script reload. ResetPursuits makes the spawn idempotent, so all three
paths are now silent.

Add Reactor regression tests covering ResetPursuits semantics, the pre-fix
DialogSequences.Clear() leaving SequenceIndex stale, and a re-register-after-
reset round trip that confirms no duplicate dictionary entries.

Add .editorconfig pinning end_of_line=lf, charset=utf-8, and standard indent
sizes per file type so future edits stop oscillating between CRLF and LF in a
repo whose committed line endings are already mixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Admin slash command to flip a character's Gender between Male and Female,
intended for testing how the body byte renders for each gender.

Accepts m/male/f/female (case-insensitive). Player characters cannot be
Gender.Neutral — the value exists in the Hybrasyl.Xml enum for non-player
contexts but is rejected here.

Tests in Hybrasyl.Tests/ChatCommands.cs invoke the static Run method via
reflection (matching what ChatCommandHandler does at runtime) and cover
parsing, the male/female aliases, case-insensitivity, neutral rejection,
and invalid-input rejection.

A /bodystyle command was attempted in this change but reverted: setting the
low nibble of the body byte (the Equipment.Armor.BodyStyle slot) does not
swap the underlying khan*.dat sprite — that variant lives somewhere else in
the wire format. Investigation continues on a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Admin slash command to set User.SkinColor — the byte field at
DisplayUser.cs:77 that selects which mm##.epf / wm##.epf variant the
client loads from khanmim / khanwim. The SkinColor enum names cover 0-9
(Basic..Purple) but the wire is byte, so the command accepts 0-255 to
exercise whatever palette/sprite the client has at any given index.

Investigation note (also captured in auto-memory): an earlier attempt
targeted the body byte's low nibble, which is actually PantsColor, not
the body model variant. The mm/wm variant is carried by the SkinColor
byte sent separately in the same packet — and User.SkinColor already
exists as a JsonProperty. This command exposes that field directly.

This same byte will eventually be repurposed for species body style;
the slash-command surface is forward-compatible since the wire is
already a full byte.

Tests cover named-range mapping (3 → Orc), byte boundary (255 accepted),
overflow rejection (256), negative rejection, and non-numeric rejection.
136/136 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three independent bugs related to creature placement and area-of-interest
iteration:

- Spawn placement: MapObject's UsableTiles loop used `<= X/Y`, so the
  off-by-one column/row (which have no collision data, so IsWall returns
  false for them) made it into the spawnable set. Random spawns at those
  tiles produced mobs at coordinates outside the legal map area.

- RandomDestination: Y was being computed from X (copy-paste typo), and
  both axes were clamped to byte.MaxValue instead of the actual map
  bounds. Wandering AI fed A* with destinations on the y=x diagonal and
  past the map edge; A* and Walk absorbed the OOB destination but burned
  cycles on hopeless paths.

- AoI NREs: VisibleObject.Hide/Show snapshot the entity tree once and
  iterate, but MapObject.Remove clears `obj.Map` *outside* the lock that
  guarded the tree removal. Monster.AoiEntry/AoiDeparture dereferenced
  Map.EntityTree directly and NRE'd when the monster was removed mid-
  iteration. Confirmed in three traces (Refresh/Hide/Teleport).

Fixes:
- MapObject.UsableTiles loop now uses `< X/Y` to match LoadMapFile and
  IsValidPoint.
- 2-arg FindEmptyTile gains an IsValidPoint guard for defense in depth.
- Monolith logs an OOB-spawn warning if a placement still escapes, with
  the source (xml vs FindEmptyTile) to ease future diagnosis.
- RandomDestination uses the correct Y axis and clamps to Map.X-1/Map.Y-1.
- Monster.AoiEntry/AoiDeparture capture Map locally and skip the
  EntityTree query when null.
- VisibleObject.Hide/Show skip entries whose Map has already been
  cleared between snapshot and iteration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…veChance guard

Two bugs in RemoveStatus, both from HS-1466 (0ca5922):

- The chance-roll guard was inverted: `if (string.IsNullOrEmpty(...))` ran
  the roll on statuses *without* a RemoveChance and skipped it on those
  with one — feeding Eval an empty string and making the feature a no-op
  for any status that actually configured it.

- The success message and combat-log event fired on every removal,
  including natural expiry from ProcessStatusTicks (which passes
  remover=null). Players saw "You succeed in removing the affliction."
  when statuses like cruth oiche / hide simply timed out, with a
  RemoverName of "unknown" in the combat log.

Fix: invert the condition so RemoveChance actually gates the roll; gate
the success message and combat-log emit on `remover != null` so natural
expiry stays silent. Reactor AoI departure still fires unconditionally
since it's about visibility, not messaging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Followup to the previous commit. With the RemoveChance guard now correct,
the code reaches the snapshot lookup that `WorldStateStore.Get<T>` performs
on `status.OriginSnapshotId` — and that method throws KeyNotFoundException
on miss. The lookup fails whenever:

- the status was applied without a source (OriginSnapshotId stays Guid.Empty
  per CreatureStatus constructor at line 80-84), or
- the snapshot was evicted from WorldState between application and removal.

In production this surfaced as a KeyNotFoundException when breaking stealth:
both UseCastable(BreakStealth=true) and the DisplayCreature handler call
RemoveStatus for invisibility statuses, so the user appeared "stuck" invisible
because the removal aborted mid-flow.

Fix: use TryGetValue and log a warning if the snapshot can't be found, then
skip the chance roll entirely (permissive removal). Without a snapshot the
formula's OriginalCaster has no meaningful value, so refusing the roll is
the only sensible option.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The RemoveChance feature (HS-1466) had multiple latent bugs that the
previous commits' fix to the inverted guard exposed. Removing those one
at a time:

- ICreatureSnapshot.CreateStatSnapshot stored snapshots in WorldState
  under the *creature's* Guid but returned the *snapshot's* Guid, so
  every lookup against OriginSnapshotId missed. Store under snapshot.Guid
  so the round trip works.

- FormulaParser.Parameterize had a copy-paste bug in the Creature-type
  loop: it called `prop.GetValue(eval.OriginalCaster)` even though
  OriginalCaster is a StatInfo (declared in FormulaEvaluation). That
  threw TargetException as soon as a formula actually ran.

- Creature.UseCastable's RemoveStatuses loop called
  `tar.RemoveStatus(icon)` without a remover, leaving the chance
  formula's SOURCE* variables undefined. Pass `this` so the caster is
  recorded as the remover.

- HybrasylUser/HybrasylMonster scripting APIs, ItemObject status-apply,
  and the /status chat command all constructed CreatureStatus with
  source=null. For self-applied statuses (hide, cruth oiche, item
  buffs, GM-applied), pass the target as the source so OriginSnapshotId
  gets populated and the formula evaluates with sensible defaults.

After this, the chance roll actually runs, the formula sees correct
SOURCE/TARGET/ORIGINALCASTER variables, and snapshot lookup hits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new tests addressing gaps surfaced by the RemoveChance work:

- RemoveStatusWithRemovalChanceFails: uses TestFailCurse (RemoveChance=0)
  to deterministically exercise the failure branch. Asserts the status
  remains, the remover received "You try your best, but nothing happens.",
  and the combat log entry recorded RemovalRoll >= RequiredRoll.

- StatusExpiryIsSilent: applies TestAddInvisible, lets it reach Expired,
  drives ProcessStatusTicks manually (the StatusTickJob isn't scheduled
  in the test harness), and asserts that no success message was emitted
  on natural expiry — guarding against the bug where the success message
  fired regardless of how removal was triggered.

Also bumps TestUser's level to 99 in the existing RemoveStatusWithRemovalChance
test so the now-actually-evaluating scaling formula caps at 0.97 and the
test stays at the previously-acknowledged ~3% flake rate instead of
plummeting to ~59% at default test-user levels.

Depends on ceridwen TestFailCurse status (already committed) and the
TestScalingCurse formula fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
already moved to docs
fix: OOB creature spawning, AoI null-deref races, and status removal chain
Door subsystem rewrite: corrected catalog + DoorGroup + identity-based scan
Plumb IPAddress through Server/Lobby/Login/World constructors and bind
to it in StartListening instead of the hardcoded IPAddress.Any. The
BindAddress config attribute existed but was previously ignored, so
listeners always bound 0.0.0.0 regardless of what the operator set.

Drops the Game.IpAddress static in favor of Game.Lobby.BindAddress,
and threads the same parameter into Hybrasyl.Tests fixtures.

Also adds .stignore to .gitignore for Syncthing users.
Required for docker-compose port-publish to forward traffic now that
the server actually respects BindAddress. ExternalAddress stays at
127.0.0.1 since clients connect to the docker-published port on the
host loopback.

Brings contrib/config.xml in line with the helm chart defaults, where
all four network services already use 0.0.0.0.

Also tidies XML whitespace (declaration ordering and self-closing tags).
Console app under grpc_client/ that exercises every RPC in Patron.proto
(health, users, auth, reset-password, shutdown, shutdown-status) and
manages a TOFU trust store for self-signed dev certificates.

Supports four transport modes:
  --insecure              h2c, no TLS (default, dev path)
  --tls                   server-auth TLS using system trust
  --cacert <path>         pin trust to a specific CA
  --cert/--key + optional --cacert   mTLS

Untrusted server certs trigger an interactive prompt with the option
to persist to ~/.config/hybrasyl/grpc-client/trusted.pem, with
non-interactive equivalents (--trust-once, --trust-add) for scripts.
Trust entries are managed via 'hybctl trust list/show/remove/add'.

Run with: dotnet run --project grpc_client -- <subcommand> [opts]
@baughj baughj merged commit fdc365f into main May 15, 2026
2 of 3 checks passed
@codecov

codecov Bot commented May 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 31.87%. Comparing base (6d582aa) to head (65671fc).
⚠️ Report is 123 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #334      +/-   ##
==========================================
+ Coverage   30.50%   31.87%   +1.36%     
==========================================
  Files         238      339     +101     
  Lines       25578    26485     +907     
  Branches     3513     3681     +168     
==========================================
+ Hits         7803     8441     +638     
- Misses      17137    17332     +195     
- Partials      638      712      +74     

☔ View full report in Codecov by Sentry.
📢 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.

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.

2 participants