ModernUO/Projects/UOContent
SynPDX 86df62fd3e
fix(housing): register doors, and stop crashing on client component sheets (#2557)
## Summary

Players could not place **any door** while customizing a house, and placing other pieces could disconnect them outright. Staff saw neither problem: `HouseFoundation.Designer_Build` only enforces `ValidPiece` below `GameMaster`.

Original report and diagnosis by @SynPDX.

## Root cause 1 — no door is ever registered

The retail client's `doors.txt` separates its header rows with lines of **bare tabs** (it is the only sheet that does):

```
int<TAB>int<TAB>...<TAB>string
<TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB>      <-- 10 tabs, not an empty line
Category<TAB>Piece1<TAB>...<TAB>FeatureMask<TAB>Comment
<TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB>
0<TAB>1657<TAB>1659<TAB>...
```

`Spreadsheet.ReadLine` skipped a line only when `line.Length > 0`. A 10-tab line has length 10, so it was returned as the **names row** — every column ended up named `""`, `GetColumnID("Piece1")` and friends returned `-1`, and not one of the 230 door graphics was registered. Unregistered item IDs keep the `-1` sentinel, and `CheckValidity` rejects those, so `ValidPiece` refused every door.

ClassicUO skips these lines (`string.IsNullOrWhiteSpace` in `HouseCustomizationManager.ParseFile`), which is why the client happily offers doors the server then rejects.

Measured against a retail 7.0.x `doors.txt` using the shipped `Spreadsheet`:

| | `FeatureMask` column | door graphics registered |
|---|---|---|
| before | `-1` | **0** |
| after | `9` | **230** |

## Root cause 2 — `IndexOutOfRangeException` out of the packet handler

Every sheet ends in a cosmetic `Comment` column that ModernUO never reads, and client sheets write an empty comment as a plain newline with no trailing tab. `Split('\t')` then returns one field fewer than the header declares, and the parser indexed past the end:

```
System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Server.Multis.Spreadsheet..ctor(String path)
   at Server.Multis.ComponentVerification.LoadSpreadsheet(...)
   at Server.Multis.ComponentVerification.IsItemValid(Int32 itemID)
   at Server.Multis.HouseFoundation.ValidPiece(Int32 itemID, Boolean roof)
   at Server.Multis.HouseFoundation.Designer_Build(NetState state, ...)
```

The client's own parser only requires the columns up to `FeatureMask` — ClassicUO's `CustomHouseMisc.Parse` guards on `scanf.Length >= 12` for a 13-column `misc.txt` — so such a row is valid data listing real pieces. Missing trailing fields are now treated as empty rather than dropping the row, which would unregister every piece the row lists and reproduce the door symptom.

`EnsureLoaded` also set `_loaded` before loading, so once the throw escaped, an all `-1` table stayed cached and rejected everything for players from then on — the same player-visible symptom as #2500.

## Also made explicit rather than accidental

- **Named the table sentinels.** `NotAComponent` (-1) is the anti-cheat guard and the initial state; `NoFeatureRequired` (0) is a piece with no expansion gate — how `walls.txt` encodes pre-AOS base pieces and what `housing.bin` collapses to under `HousingTierMask` (#2500).
- **A sheet with no `FeatureMask` column is refused and logged.** `GetInt32` on a missing column returns 0 = `NoFeatureRequired`, which would have silently marked every piece in that sheet unconditionally placeable regardless of expansion. This was previously only harmless by accident.
- **A sheet matching none of its expected tile columns is refused and logged** — that is what `doors.txt` was doing silently. Individual missing columns stay tolerated, since older sheets predate columns such as `walls.txt`'s `SecondAltWindowS`/`E`.
- **Catch per sheet**, so one unreadable file no longer costs the other six.
- **Header guards**: an empty file or a types-only file raised a `NullReferenceException`; a names row shorter than the types row indexed past the end.
- **Fall back to the component sheets when `housing.bin` cannot be read**, instead of passing `null` into a `SpanReader`.

`_loaded` is still set before loading, deliberately: this runs from the design packet handler, and retrying would re-read every sheet on each subsequent placement attempt.

Sheet precedence is **unchanged** — the client's copies stay authoritative and `Data/Components` remains the fallback.

## Verification

- Retail 7.0.x client `doors.txt` through the shipped `Spreadsheet`: 0 door graphics before, 230 after.
- 5 new tests in `SpreadsheetTests` covering the tab separators, the omitted trailing field, per-row recovery, and both header guards. All 5 fail against `main` and pass here.
- `dotnet build` clean (0 warnings, 0 errors); `UOContent.Tests` 642/642.
2026-08-04 20:05:28 -07:00
..
Accounting feat(network): allowlist false-positive IPs, escalate on behavior (#2556) 2026-07-30 23:12:17 -07:00
Assistants chore: Use var everywhere (#2294) 2025-12-27 16:47:28 -08:00
Commands feat(network): allowlist false-positive IPs, escalate on behavior (#2556) 2026-07-30 23:12:17 -07:00
Compression feat: Implements new robust/pluggable backup/archive system. (#2388) 2026-03-22 19:51:20 -07:00
Configuration fix: Streamlines insurance. Insurance only executes when enabled. (#2550) 2026-07-26 09:47:49 -07:00
Console feat: Adds console commands (#1714) 2024-03-30 09:40:43 -07:00
Context Menus fix: Moves ContextMenu out of core, streamlines code, fixes bugs (#1873) 2024-07-20 21:33:23 -07:00
Engines fix(opl): refuse property list invalidation raised from inside GetProperties (#2555) 2026-07-28 21:29:03 -07:00
Gumps feat(network): allowlist false-positive IPs, escalate on behavior (#2556) 2026-07-30 23:12:17 -07:00
Holiday Stuff perf: Migrate Veteran Reward gumps to DynamicGump/StaticGump (#2415) 2026-04-25 20:09:27 -07:00
Items fix: Streamlines insurance. Insurance only executes when enabled. (#2550) 2026-07-26 09:47:49 -07:00
Migrations feat(throwing): add remaining SA throwing artifacts (add-only) (#2516) 2026-07-03 08:50:16 -07:00
Misc feat(network): allowlist false-positive IPs, escalate on behavior (#2556) 2026-07-30 23:12:17 -07:00
Mobiles fix: Streamlines insurance. Insurance only executes when enabled. (#2550) 2026-07-26 09:47:49 -07:00
Multis fix(housing): register doors, and stop crashing on client component sheets (#2557) 2026-08-04 20:05:28 -07:00
Network feat(network): allowlist false-positive IPs, escalate on behavior (#2556) 2026-07-30 23:12:17 -07:00
Regions fix: Fixes criminals having guards called on them (#2348) 2026-03-13 17:35:47 -07:00
Skills feat: Pre-Publish 14 Crafting (supersedes #2181, #2381) (#2476) 2026-06-07 20:27:22 -07:00
Special Systems feat: Add zero-alloc interpolation handler to ValueStringBuilder, replace all StringBuilder usage (#2387) 2026-03-22 14:23:44 -07:00
Spells fix(necromancy): correct Blood Oath duration, reflection, and expiry timing (#1690) (#2480) 2026-06-08 23:42:28 -07:00
Systems/JailSystem fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Targets chore: Use var everywhere (#2294) 2025-12-27 16:47:28 -08:00
Text fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Utilities fix: Harden Advanced Search: crash-safety, autosave, correct results & worker fixes (#2543) 2026-07-21 07:51:06 -07:00
World Saves perf(messages): mechanical interpolation cleanups (#2436) 2026-05-03 18:26:49 -07:00
Module.cs fix: Fixes sending packets and sidesteps a major issue with stackalloc and PGO in .NET 8 (#1607) 2023-11-21 12:18:20 -08:00
UOContent.csproj fix: Bumps dependencies. (#2531) 2026-07-14 15:17:55 -07:00