## Summary
Fixes the pathfinding step-cache (`.swb`) prebake so it bakes **once** and skips when a valid cache already exists, instead of re-baking on every boot. The root cause was the staleness fingerprint hashing mutable in-memory tile data rather than the on-disk files. This PR makes the fingerprint a pure function of the client data files and separates dynamic multis (houses/boats) from the static cache.
> This branch builds on the `ConfigurePrompts` first-boot-prompt unification (commit `5df8d0bd`, also included here) — that commit accounts for the `ServerConfiguration.cs` and `dev-docs/server-lifecycle.md` changes in the diff.
## The bug
With `pathfinding.prebakeMaps` set, the cache re-baked on **every** boot. The `.swb` staleness fingerprint hashed the live `TileData.LandTable`/`ItemTable` flags, which the server patches at runtime (`ItemFixes`, `LOSBlocker`, `PotionKeg`, `CTF`) at nondeterministic lifecycle points (Initialize-phase methods share a priority; static ctors fire lazily). So a fingerprint stamped at runtime (`[PathBake`) never matched the one recomputed during startup `Initialize()`, and the cache rebaked every time.
## Changes
**1. Fingerprint the files, not the in-memory tables** (`fix`)
Hash `tiledata.mul` (cached, computed once) plus the per-map `.mul`/`.uop` files — never the runtime-mutated `TileData` tables. The fingerprint is now lifecycle-stable. Existing `.swb` files rebake once after deploy, then stay stable.
**2. Compute the fingerprint once per boot** (`refactor`)
`Configure()`'s `AutoLoadAtStartup()` already opens and fingerprint-validates a reader for every up-to-date `.swb`. `Initialize()` now skips baking any map that already has an open reader (`StepCache.HasLazyReader`) instead of recomputing the fingerprint a second time.
**3. Bake static-only; route multis to the live path** (`refactor`)
Multis (houses/boats) are dynamic, so they're no longer baked into the static chunk cache — they were tagged with `BuiltMultisVersion`, a non-persisted session counter, which made persisting them unsafe (false matches / wasted re-bakes).
- Chunks bake land + `statics.mul` only.
- At query time, any cell whose sector (or its 1-cell halo) contains a multi routes to `Fallthrough_Multi` → the existing live, multi-aware `CheckMovement` path. The halo prevents a cell proposing a walkable edge into a neighbouring wall; interior (multi-free) cells pay one sector lookup.
- Adds `Sector.HasMultis` (one engine accessor); `.swb` format → v9 (rejects old multi-baked files); new `Fallthrough_Multi` telemetry.
- Behaviour-preserving: multis use the same live path the engine used before the cache existed.
**4. Comment polish** (`style`) — no behaviour change.
## Testing
All green: **92** pathfinding (incl. a new fingerprint-stability test and a multi-halo fallthrough test), **423** UOContent, **708** Server.
## Follow-ups (not in this PR)
- **Background bake worker** — make `[PathBake` and the boot prebake non-blocking (game thread serves tile reads to an off-thread worker).
- **Per-multi MCL cache** — cache walkability in each multi's own frame (keyed by multiID, movement-invariant) so houses/boats get a fast path instead of the live fallback.
- **House-pathfinding equivalence tests** — the one area not yet covered by a dedicated automated test; multi pathing is currently correct by delegation to the live path.
43 lines
1.7 KiB
C#
43 lines
1.7 KiB
C#
using Server.Engines.Pathing.Cache;
|
|
using Xunit;
|
|
|
|
namespace Server.Tests.Pathfinding;
|
|
|
|
[Collection("Sequential Pathfinding Tests")]
|
|
public class StepCacheFingerprintTests
|
|
{
|
|
/// <summary>
|
|
/// Regression: the cache fingerprint must hash the on-disk tiledata.mul, NOT the mutable
|
|
/// in-memory <see cref="TileData"/> tables. The server patches item flags/heights at runtime
|
|
/// (ItemFixes, LOSBlocker, PotionKeg, CTF, ...) at nondeterministic lifecycle points, so a
|
|
/// fingerprint taken over the live tables depended on WHEN it was computed: a runtime
|
|
/// [PathBake stamped one value into the .swb and the next startup's Initialize() recomputed a
|
|
/// different one, marking the bake stale and re-baking on every boot. Hashing the file makes
|
|
/// the fingerprint a pure function of the client's tile data, immune to those mutations.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Fingerprint_IgnoresRuntimeTileDataMutation()
|
|
{
|
|
const int mapId = 1; // Trammel — loaded by the test bootstrap.
|
|
|
|
var before = StepCacheFile.ComputeFingerprint(mapId);
|
|
|
|
const int probeId = 0x2A0;
|
|
var original = TileData.ItemTable[probeId].Flags;
|
|
try
|
|
{
|
|
// Mutate an in-memory item flag the way ItemFixes/CTF/etc. do at runtime. XOR
|
|
// guarantees the value actually changes regardless of the current flag state.
|
|
TileData.ItemTable[probeId].Flags ^= TileFlag.NoShoot;
|
|
Assert.NotEqual(original, TileData.ItemTable[probeId].Flags); // sanity: mutation took
|
|
|
|
var after = StepCacheFile.ComputeFingerprint(mapId);
|
|
|
|
Assert.Equal(before, after);
|
|
}
|
|
finally
|
|
{
|
|
TileData.ItemTable[probeId].Flags = original;
|
|
}
|
|
}
|
|
}
|