From 70276dcf52997c3a2c2ff93402fd9111897280b6 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Mon, 22 Jun 2026 08:47:10 -0700 Subject: [PATCH] fix(housing): allow non-staff to place classic house pieces in customization (#2500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary House customization rejected ~40% of components — all classic/base tiles such as **sandstone** — for non-staff players. The pieces appeared briefly in the editor, then vanished before commit; only staff (GM+) could add them. ## Root cause A data-convention mismatch introduced when housing.bin support was added (#2329). - The OSI `housing.bin` encodes pre-AOS base pieces with the client **T2A** feature bit (`0x1`). - `walls.txt` (and RunUO) encode the same pieces as `FeatureMask = 0` (always valid). - `ComponentVerification.CheckValidity` validates against `ExpansionInfo.HousingFlags`, whose enum has no `0x1` bit, so base pieces failed `(HousingFlags & 0x1) != 0`. - `HouseFoundation.Designer_Build` only enforces `ValidPiece` for `AccessLevel < GameMaster`, so staff bypassed validation while players had placements rejected — the server re-sends the design state and the client rebuilds the house from it, erasing the just-placed piece. This only affects servers loading `housing.bin` from a UOP client; the old txt-only path (pre-#2329, like RunUO) was unaffected because base pieces are `0` there. ## Fix Normalize the `housing.bin` feature mask to the housing-tier bits on load (`& HousingFlags.HousingEJ`). Base pieces collapse to `0` (always valid, exactly as `walls.txt` encodes them); `AOS`/`SE`/`ML`/... pass through unchanged. Both data sources now produce an identical validity table, matching RunUO behavior. `CheckValidity` and the `val != -1` anti-cheat guard are unchanged. ## Verification - Parsed the real `housing.bin` from a 7.0.x client: sandstone (`0x345`) loads as `0x1` → `& HousingEJ` → `0` → valid; tier pieces (`0x40` SE, etc.) pass through; unregistered tiles stay `-1` → rejected. - Confirmed OSI's own files disagree for the same pieces: `walls.txt` base `FeatureMask = 0` vs `housing.bin` `0x1`. - `dotnet build` clean (0 warnings, 0 errors). --- Projects/UOContent/Multis/ComponentVerification.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Projects/UOContent/Multis/ComponentVerification.cs b/Projects/UOContent/Multis/ComponentVerification.cs index 3246560a9..967fed1bc 100644 --- a/Projects/UOContent/Multis/ComponentVerification.cs +++ b/Projects/UOContent/Multis/ComponentVerification.cs @@ -13,6 +13,13 @@ public static class ComponentVerification private static int[] _multiTable; private static bool _loaded; + // housing.bin stores each component's feature mask in the client's feature-flag bit space, which + // tags pre-AOS base pieces (e.g. sandstone) with low bits - notably T2A (0x1) - that HousingFlags + // does not model. CheckValidity validates against HousingFlags, so strip everything except the + // housing-tier bits when loading: base pieces collapse to 0 (always valid, exactly as walls.txt + // encodes them) while AOS/SE/ML/... line up unchanged. + private const int HousingTierMask = (int)HousingFlags.HousingEJ; + public static bool IsItemValid(int itemID) { EnsureLoaded(); @@ -93,7 +100,7 @@ public static class ComponentVerification { reader.ReadUInt32LE(); // category_id reader.ReadUInt32LE(); // subcategory_id - var featureMask = (int)reader.ReadUInt32LE(); + var featureMask = (int)reader.ReadUInt32LE() & HousingTierMask; reader.ReadUInt32LE(); // cliloc_id // fields_1