From 8544a785d7bc22cbf7a94b14cd578deea176f4f4 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 15 May 2026 06:49:46 +0200 Subject: [PATCH] =?UTF-8?q?feat(B.7):=20RadarBlipColors=20=E2=80=94=20port?= =?UTF-8?q?=20of=20gmRadarUI::GetBlipColor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Static helper resolving a target indicator / radar blip colour from ItemType + the raw PublicWeenieDesc._bitfield acdream already parses onto EntitySpawn. Dispatch order matches retail decomp at 0x004d76f0: Portal (BF_PORTAL = 0x40000) → cyan Vendor (BF_VENDOR = 0x200) → green Creature && !IsPlayer → yellow Player + IsPK (BF_PLAYER_KILLER = 0x20) → red Player + IsPKLite (= 0x2000000) → pink Player (other) → white (Default) Otherwise (item / object) → light grey RGBA values are hand-tuned to visually match retail screenshots; the real RGBAColor_Radar* constants live in retail static data and can be swapped in later without breaking call sites. 8 unit tests cover the full type/flag matrix (item, NPC, friendly player, PK, PKLite, vendor, portal-priority-over-flags). Next: TargetIndicatorPanel (App, ImGui draw) that uses this lookup. --- src/AcDream.Core/Ui/RadarBlipColors.cs | 93 +++++++++++++++++++ .../Ui/RadarBlipColorsTests.cs | 83 +++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 src/AcDream.Core/Ui/RadarBlipColors.cs create mode 100644 tests/AcDream.Core.Tests/Ui/RadarBlipColorsTests.cs diff --git a/src/AcDream.Core/Ui/RadarBlipColors.cs b/src/AcDream.Core/Ui/RadarBlipColors.cs new file mode 100644 index 0000000..685f156 --- /dev/null +++ b/src/AcDream.Core/Ui/RadarBlipColors.cs @@ -0,0 +1,93 @@ +using AcDream.Core.Items; + +namespace AcDream.Core.Ui; + +/// +/// B.7 (2026-05-15) — port of retail's gmRadarUI::GetBlipColor +/// (named decomp 0x004d76f0). Returns the radar / target-indicator +/// colour for an entity based on its and the +/// raw PublicWeenieDesc._bitfield we already parse out of +/// CreateObject. +/// +/// +/// Used by the Vivid Target Indicator (Phase B.7) to colour the four +/// corner triangles around the selected entity. Same value retail +/// would have shown on the radar blip — so the indicator + radar agree. +/// +/// +/// +/// Dispatch order matches the retail decomp at lines 219913+ of +/// docs/research/named-retail/acclient_2013_pseudo_c.txt. The +/// PWD bit layout matches acclient.h:6431-6463: +/// +/// BF_PLAYER = 0x8 +/// BF_PLAYER_KILLER = 0x20 +/// BF_VENDOR = 0x200 (byte[1] & 0x02 in retail) +/// BF_PORTAL = 0x40000 +/// BF_FREE_PKSTATUS = 0x200000 (hostile-flagged player) +/// BF_PKLITE_PKSTATUS = 0x2000000 +/// +/// +/// +/// +/// RGBA values are hand-tuned to visually match retail screenshots +/// (yellow creature, red PK, pink PKLite, green vendor, cyan portal, +/// white default). Real RGBAColor_Radar* constants live in retail +/// static data — if they're ever recovered the table can be tightened. +/// +/// +public static class RadarBlipColors +{ + public readonly record struct Rgba(byte R, byte G, byte B, byte A); + + public static readonly Rgba Item = new(220, 220, 220, 255); // light grey (default object) + public static readonly Rgba Default = new(255, 255, 255, 255); // white (friendly player) + public static readonly Rgba Creature = new(255, 220, 80, 255); // yellow (NPC / monster) + public static readonly Rgba PlayerKiller = new(255, 64, 64, 255); // red (PK) + public static readonly Rgba PKLite = new(255, 128, 192, 255); // pink (PKLite) + public static readonly Rgba Vendor = new( 64, 192, 64, 255); // green (vendor NPC) + public static readonly Rgba Portal = new( 64, 192, 255, 255); // cyan (portal) + + /// + /// Resolve the radar-blip colour for an entity. Caller supplies the + /// raw (from CreateObject.ItemType) and + /// (from + /// CreateObject.ObjectDescriptionFlags) — both are already + /// parsed and stashed on EntitySpawn at spawn time. + /// + /// + /// Returns for friendly players, + /// for NPCs/monsters, for everything else. + /// Special types (PK, PKLite, Vendor, Portal) win over the base + /// type when their flag is set. + /// + /// + public static Rgba For(uint itemType, uint pwdBitfield) + { + // Special-type early returns. Order matches retail dispatch + // (Portal first, then Vendor) — same target can't logically + // be both, but in case of bit collision retail's order wins. + if ((pwdBitfield & 0x40000u) != 0) return Portal; + if ((pwdBitfield & 0x200u) != 0) return Vendor; + + bool isCreature = (itemType & (uint)ItemType.Creature) != 0; + bool isPlayer = (pwdBitfield & 0x8u) != 0; + + // Creature that isn't a player → NPC / monster → yellow. + if (isCreature && !isPlayer) + return Creature; + + if (isPlayer) + { + bool isPK = (pwdBitfield & 0x20u) != 0; + bool isPKLite = (pwdBitfield & 0x2000000u) != 0; + if (isPK) return PlayerKiller; + if (isPKLite) return PKLite; + return Default; + } + + // Not a special type, not a creature, not a player → an item / + // object on the ground or in the world. + return Item; + } +} diff --git a/tests/AcDream.Core.Tests/Ui/RadarBlipColorsTests.cs b/tests/AcDream.Core.Tests/Ui/RadarBlipColorsTests.cs new file mode 100644 index 0000000..482ea3e --- /dev/null +++ b/tests/AcDream.Core.Tests/Ui/RadarBlipColorsTests.cs @@ -0,0 +1,83 @@ +using AcDream.Core.Items; +using AcDream.Core.Ui; +using Xunit; + +namespace AcDream.Core.Tests.Ui; + +public sealed class RadarBlipColorsTests +{ + // PWD bit constants per docs/research/named-retail/acclient.h:6431-6463 + private const uint BF_PLAYER = 0x8u; + private const uint BF_PLAYER_KILLER = 0x20u; + private const uint BF_VENDOR = 0x200u; + private const uint BF_PORTAL = 0x40000u; + private const uint BF_PKLITE_PKSTATUS = 0x2000000u; + + [Fact] + public void Item_NoFlags_ReturnsItemColor() + { + // SpellComponents is itemType=0x1000 (e.g. a Taper) — not a creature. + var result = RadarBlipColors.For(itemType: (uint)ItemType.SpellComponents, pwdBitfield: 0); + Assert.Equal(RadarBlipColors.Item, result); + } + + [Fact] + public void Misc_NoFlags_ReturnsItemColor() + { + var result = RadarBlipColors.For((uint)ItemType.Misc, pwdBitfield: 0); + Assert.Equal(RadarBlipColors.Item, result); + } + + [Fact] + public void Creature_NotPlayer_ReturnsCreatureColor() + { + // NPC: itemType has Creature bit, no Player flag. + var result = RadarBlipColors.For((uint)ItemType.Creature, pwdBitfield: 0); + Assert.Equal(RadarBlipColors.Creature, result); + } + + [Fact] + public void FriendlyPlayer_ReturnsDefaultColor() + { + // Friendly player: Creature itemType + Player flag, no PK bits. + var result = RadarBlipColors.For((uint)ItemType.Creature, pwdBitfield: BF_PLAYER); + Assert.Equal(RadarBlipColors.Default, result); + } + + [Fact] + public void PK_Player_ReturnsPlayerKillerColor() + { + var result = RadarBlipColors.For((uint)ItemType.Creature, + pwdBitfield: BF_PLAYER | BF_PLAYER_KILLER); + Assert.Equal(RadarBlipColors.PlayerKiller, result); + } + + [Fact] + public void PKLite_Player_ReturnsPKLiteColor() + { + var result = RadarBlipColors.For((uint)ItemType.Creature, + pwdBitfield: BF_PLAYER | BF_PKLITE_PKSTATUS); + Assert.Equal(RadarBlipColors.PKLite, result); + } + + [Fact] + public void Vendor_BeatsCreatureFlag() + { + // A vendor NPC: Creature itemType + Vendor flag. Vendor wins per + // retail's dispatch order (vendor check happens before creature + // check at 0x004d7946-004d7973). + var result = RadarBlipColors.For((uint)ItemType.Creature, + pwdBitfield: BF_VENDOR); + Assert.Equal(RadarBlipColors.Vendor, result); + } + + [Fact] + public void Portal_TopPriority() + { + // Portal flag wins over everything else (retail dispatch order + // checks BF_PORTAL first). + var result = RadarBlipColors.For((uint)ItemType.Creature, + pwdBitfield: BF_PORTAL | BF_PLAYER | BF_PLAYER_KILLER); + Assert.Equal(RadarBlipColors.Portal, result); + } +}