Conversation
…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
…est (need to clear state between runs)
…verrides into account
- 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]
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
develophas existed long enough. Time to end that. All development will be onmainfrom here on out, andmainwill actually reflect the state of the project. No need for these god awful long lived branches.