using AcDream.Core.Items; using AcDream.Core.Textures; using AcDream.Content; using DatReaderWriter; using DatReaderWriter.DBObjs; namespace AcDream.App.UI.Layout; /// /// CPU-side body-part map used by retail's paperdoll drag mask. Ports /// gmPaperDollUI::CreateClickMap @ 0x004A4850 and /// gmPaperDollUI::GetPaperDollItemUnderMouse @ 0x004A4920. /// public sealed class PaperdollClickMap { public const uint ClickMapEnum = 0x1000000Cu; public const uint InterfaceEnumCategory = 7u; private readonly byte[] _rgba; public int Width { get; } public int Height { get; } public PaperdollClickMap(byte[] rgba, int width, int height) { ArgumentNullException.ThrowIfNull(rgba); if (width <= 0) throw new ArgumentOutOfRangeException(nameof(width)); if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height)); if (rgba.Length < checked(width * height * 4)) throw new ArgumentException("The RGBA buffer is smaller than the declared map.", nameof(rgba)); _rgba = rgba; Width = width; Height = height; } /// /// Resolves DBObj::GetDIDByEnum(0x1000000C, 7) through the portal master map /// and decodes the resulting RenderSurface for exact per-pixel sampling. /// The caller owns synchronization around . /// public static PaperdollClickMap? Load(IDatReaderWriter dats) { ArgumentNullException.ThrowIfNull(dats); uint masterDid = (uint)dats.Portal.Db.Header.MasterMapId; if (masterDid == 0 || !dats.Portal.TryGet(masterDid, out var master) || master is null || !master.ClientEnumToID.TryGetValue(InterfaceEnumCategory, out uint subMapDid) || !dats.Portal.TryGet(subMapDid, out var subMap) || subMap is null || !subMap.ClientEnumToID.TryGetValue(ClickMapEnum, out uint surfaceDid)) return null; if (!dats.Portal.TryGet(surfaceDid, out var surface) && !dats.HighRes.TryGet(surfaceDid, out surface)) return null; Palette? palette = surface.DefaultPaletteId != 0 ? dats.Get(surface.DefaultPaletteId) : null; DecodedTexture decoded = SurfaceDecoder.DecodeRenderSurface(surface, palette); return decoded.Width > 1 && decoded.Height > 1 ? new PaperdollClickMap(decoded.Rgba8, decoded.Width, decoded.Height) : null; } public EquipMask GetBodyLocation(int x, int y) { if ((uint)x >= (uint)Width || (uint)y >= (uint)Height) return EquipMask.None; int pixel = (y * Width + x) * 4; byte r = _rgba[pixel]; byte g = _rgba[pixel + 1]; byte b = _rgba[pixel + 2]; return (r, g, b) switch { (0x00, 0x00, 0xFF) => EquipMask.HeadWear, (0x00, 0xFF, 0x00) => EquipMask.ChestWear | EquipMask.ChestArmor, (0xFF, 0x00, 0x00) => EquipMask.AbdomenWear | EquipMask.AbdomenArmor, (0x00, 0xFF, 0xFF) => EquipMask.UpperArmWear | EquipMask.UpperArmArmor, (0xFF, 0x00, 0xFF) => EquipMask.LowerArmWear | EquipMask.LowerArmArmor, (0xFF, 0xFF, 0x00) => EquipMask.UpperLegWear | EquipMask.UpperLegArmor, (0x00, 0x00, 0x80) => EquipMask.LowerLegWear | EquipMask.LowerLegArmor, (0x00, 0x80, 0x00) => EquipMask.HandWear, (0x80, 0x00, 0x00) => EquipMask.FootWear, _ => EquipMask.None, }; } }