diff --git a/src/minimapicons/addon/MinimapIcons.lua b/src/minimapicons/addon/MinimapIcons.lua index f7848bf..2f33a38 100644 --- a/src/minimapicons/addon/MinimapIcons.lua +++ b/src/minimapicons/addon/MinimapIcons.lua @@ -95,17 +95,17 @@ local NPC_CATEGORIES = { getFilter = function() return getLocaleFilter(REAGENT_FILTERS) end }, { name = "Poison Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Poison", getFilter = function() return getLocaleFilter(POISON_FILTERS) end }, - { name = "Trade Goods", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Profession", + { name = "Trade Goods", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Trade", getFilter = function() return getLocaleFilter(TRADE_FILTERS) end }, { name = "Ammunition", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Ammunition", getFilter = function() return getLocaleFilter(AMMO_FILTERS) end }, { name = "General Vendor", trackingType = "vendor", icon = "Interface\\Minimap\\Tracking\\Food" }, + { name = "Stable Master", trackingType = "stablemaster", icon = "Interface\\Minimap\\Tracking\\StableMaster" }, + { name = "Battle Master", trackingType = "battlemaster", icon = "Interface\\Minimap\\Tracking\\BattleMaster" }, { name = "Class Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Class", getFilter = function() return (UnitClass("player")) end }, { name = "Profession Trainer", trackingType = "trainer", icon = "Interface\\Minimap\\Tracking\\Profession", getExclude = function() return UnitClass("player") end }, - { name = "Battle Master", trackingType = "battlemaster", icon = "Interface\\Minimap\\Tracking\\BattleMaster" }, - { name = "Stable Master", trackingType = "stablemaster", icon = "Interface\\Minimap\\Tracking\\StableMaster" }, } -- ============================================================================= diff --git a/src/minimapicons/assets/Interface/Minimap/Tracking/Brainwasher.blp b/src/minimapicons/assets/Interface/Minimap/Tracking/Brainwasher.blp index 6d4b0dd..4a1caa7 100644 Binary files a/src/minimapicons/assets/Interface/Minimap/Tracking/Brainwasher.blp and b/src/minimapicons/assets/Interface/Minimap/Tracking/Brainwasher.blp differ diff --git a/src/minimapicons/assets/Interface/Minimap/Tracking/Trade.blp b/src/minimapicons/assets/Interface/Minimap/Tracking/Trade.blp new file mode 100644 index 0000000..4b263f0 Binary files /dev/null and b/src/minimapicons/assets/Interface/Minimap/Tracking/Trade.blp differ diff --git a/src/minimapicons/minimapicons.zig b/src/minimapicons/minimapicons.zig index 3344bcb..69c879e 100644 --- a/src/minimapicons/minimapicons.zig +++ b/src/minimapicons/minimapicons.zig @@ -259,10 +259,66 @@ var g_go_id_tracking: [MAX_GO_ID_ENTRIES]GoEntryEntry = .{GoEntryEntry{}} ** MAX var g_blips: [MAX_BLIPS]TrackedBlip = undefined; var g_blip_count: u32 = 0; var g_has_active_tracking: bool = false; +var g_has_active_unit_tracking: bool = false; +var g_has_active_go_tracking: bool = false; +var g_has_any_filters: bool = false; +var g_active_flag_mask: u32 = 0; // OR of all active flag entry flags var g_tex_cache: [MAX_TEXTURES]TextureEntry = .{TextureEntry{}} ** MAX_TEXTURES; var g_tex_cache_count: u32 = 0; var g_default_tex_flags: u32 = 0; +// GUID → blip result cache. NPC flags/subnames don't change at runtime, +// so a matched GUID always produces the same blip. Cleared when tracking +// config changes (refreshActiveTrackingCache). +const GUID_CACHE_SIZE = 256; // must be power of 2 +const GuidCacheEntry = struct { + guid_lo: u32 = 0, + guid_hi: u32 = 0, + blip: Blip = .{ .texture = 0, .scale = 1.0 }, + valid: bool = false, // entry is populated + matched: bool = false, // true = has blip, false = no match (negative cache) +}; +var g_guid_cache: [GUID_CACHE_SIZE]GuidCacheEntry = .{GuidCacheEntry{}} ** GUID_CACHE_SIZE; + +fn guidCacheIndex(guid_lo: u32, guid_hi: u32) usize { + // Simple hash: XOR fold both halves + return @intCast((guid_lo ^ guid_hi) & (GUID_CACHE_SIZE - 1)); +} + +fn guidCacheLookup(guid_lo: u32, guid_hi: u32) ?GuidCacheEntry { + const idx = guidCacheIndex(guid_lo, guid_hi); + const entry = &g_guid_cache[idx]; + if (entry.valid and entry.guid_lo == guid_lo and entry.guid_hi == guid_hi) { + return entry.*; + } + return null; +} + +fn guidCacheStore(guid_lo: u32, guid_hi: u32, blip: ?Blip) void { + const idx = guidCacheIndex(guid_lo, guid_hi); + g_guid_cache[idx] = .{ + .guid_lo = guid_lo, + .guid_hi = guid_hi, + .blip = blip orelse .{ .texture = 0, .scale = 1.0 }, + .valid = true, + .matched = blip != null, + }; +} + +fn guidCacheClear() void { + g_guid_cache = .{GuidCacheEntry{}} ** GUID_CACHE_SIZE; +} + +// Per-frame minimap info cache (same for all objects in one enumeration cycle) +const MinimapInfoCache = struct { + cur: C3Vector = .{ .x = 0, .y = 0, .z = 0 }, + radius: f32 = 0, + layout_scale: f32 = 0, + unk_scale: f32 = 0, + valid: bool = false, +}; +var g_minimap_info: MinimapInfoCache = .{}; + var g_mutex: ?*anyopaque = null; var g_is_hook_owner: bool = false; @@ -529,23 +585,8 @@ fn initTexFlags() u32 { // Blip drawing (port of VanillaHelpers DrawMinimapTexture) // ============================================================================= -fn drawMinimapTexture(texture: u32, pos: C2Vector, scale: f32) void { - if (texture == 0) return; - - const color: CImVector = .{ .b = 0xFF, .g = 0xFF, .r = 0xFF, .a = 0xFF }; - - // Scale static blip vertex template by blip scale and offset by minimap position - var vertices: [4]C3Vector = undefined; - for (0..4) |i| { - const src = ADDR.BlipVertices + i * 12; - vertices[i] = .{ - .x = pos.x + scale * hook.readMem(f32, src), - .y = pos.y + scale * hook.readMem(f32, src + 4), - .z = scale * hook.readMem(f32, src + 8), - }; - } - - // TextureGetGxTex: __fastcall(texture_ECX, flag_EDX, status*_stack) → CGxTex* +/// Resolve HTEXTURE to CGxTex* for binding. Returns 0 on failure. +fn getGxTex(texture: u32) u32 { var status: CStatus = undefined; status.init(); defer status.deinit(); @@ -560,11 +601,24 @@ fn drawMinimapTexture(texture: u32, pos: C2Vector, scale: f32) void { [func] "r" (@as(u32, ADDR.TextureGetGxTex)), : .{ .ecx = true, .edx = true, .memory = true, .cc = true }); - if (!status.ok() or gx_tex == 0) return; + if (!status.ok()) return 0; + return gx_tex; +} - // GxRsSet(GxRs_Texture0=23, gxTex) - const gxRsSet: *const fn (u32, u32) callconv(fc) void = @ptrFromInt(ADDR.GxRsSet); - gxRsSet(23, gx_tex); +/// Draw a single blip quad. Caller must have already bound the texture via GxRsSet. +fn drawMinimapBlip(pos: C2Vector, scale: f32) void { + const color: CImVector = .{ .b = 0xFF, .g = 0xFF, .r = 0xFF, .a = 0xFF }; + + // Scale static blip vertex template by blip scale and offset by minimap position + var vertices: [4]C3Vector = undefined; + for (0..4) |i| { + const src = ADDR.BlipVertices + i * 12; + vertices[i] = .{ + .x = pos.x + scale * hook.readMem(f32, src), + .y = pos.y + scale * hook.readMem(f32, src + 4), + .z = scale * hook.readMem(f32, src + 8), + }; + } // GxPrimLockVertexPtrs(count=4, vertices, vertStride=12, normal, 0, color, 0, // null, 0, texCoords, 8, null, 0) @@ -615,21 +669,40 @@ fn drawMinimapTexture(texture: u32, pos: C2Vector, scale: f32) void { // ============================================================================= fn checkObject(info: u32, guid_lo: u32, guid_hi: u32) bool { + // Check GUID cache first — avoids all classification work for known objects + if (guidCacheLookup(guid_lo, guid_hi)) |cached| { + if (cached.matched) { + trackObject(info, guid_lo, guid_hi, cached.blip); + return true; + } + return false; // negative cache hit + } + const obj = getObjectByGUID(guid_lo, guid_hi); if (obj == 0 or !isValidPtr(obj)) return false; const obj_type = getObjectType(obj); - if (obj_type == OBJ_TYPE_UNIT) { + if (obj_type == OBJ_TYPE_UNIT and g_has_active_unit_tracking) { const npc_flags = getNpcFlags(obj); - if (npc_flags == 0) return false; + if (npc_flags == 0) { + guidCacheStore(guid_lo, guid_hi, null); + return false; + } + + // Quick bitmask check — skip loop if no active entries match any of this NPC's flags + if (npc_flags & g_active_flag_mask == 0) { + guidCacheStore(guid_lo, guid_hi, null); + return false; + } // Match entries by NPC flag + subname filter. Include filters take // priority over exclude-only filters, which take priority over unfiltered. // Within each tier, higher flag value = higher priority. - const subname = getCreatureSubName(obj); - const entry_id = getObjectEntry(obj); - const is_reagent_override = isReagentVendorEntry(entry_id); + // Only read subname/entry when filters are active (expensive per-object reads). + const has_filters = g_has_any_filters; + const subname = if (has_filters) getCreatureSubName(obj) else @as(?[*:0]const u8, null); + const is_reagent_override = if (has_filters) isReagentVendorEntry(getObjectEntry(obj)) else false; var best_priority: u8 = 0; // 0=none, 1=unfiltered, 2=exclude-only, 3=include var best_flag: u32 = 0; @@ -667,16 +740,18 @@ fn checkObject(info: u32, guid_lo: u32, guid_hi: u32) bool { } if (best_flag != 0) { - trackObject(info, obj, best_blip); + guidCacheStore(guid_lo, guid_hi, best_blip); + trackObject(info, guid_lo, guid_hi, best_blip); return true; } - } else if (obj_type == OBJ_TYPE_GAMEOBJECT) { + } else if (obj_type == OBJ_TYPE_GAMEOBJECT and g_has_active_go_tracking) { // Check by specific entry ID first const go_entry_id = getObjectEntry(obj); for (&g_go_id_tracking) |*entry| { if (!entry.active) continue; if (entry.entry_id == go_entry_id) { - trackObject(info, obj, entry.blip); + guidCacheStore(guid_lo, guid_hi, entry.blip); + trackObject(info, guid_lo, guid_hi, entry.blip); return true; } } @@ -685,31 +760,42 @@ fn checkObject(info: u32, guid_lo: u32, guid_hi: u32) bool { for (&g_go_tracking) |*entry| { if (!entry.active) continue; if (entry.go_type == go_type) { - trackObject(info, obj, entry.blip); + guidCacheStore(guid_lo, guid_hi, entry.blip); + trackObject(info, guid_lo, guid_hi, entry.blip); return true; } } } + guidCacheStore(guid_lo, guid_hi, null); return false; } -fn trackObject(info: u32, obj: u32, blip: Blip) void { +fn trackObject(info: u32, guid_lo: u32, guid_hi: u32, blip: Blip) void { if (g_blip_count >= MAX_BLIPS) return; + const obj = getObjectByGUID(guid_lo, guid_hi); + if (obj == 0 or !isValidPtr(obj)) return; + const pos = getObjectPosition(obj) orelse return; - const cur = C3Vector{ - .x = hook.readMem(f32, info + ADDR.MI_POS), - .y = hook.readMem(f32, info + ADDR.MI_POS + 4), - .z = hook.readMem(f32, info + ADDR.MI_POS + 8), - }; - const radius = hook.readMem(f32, info + ADDR.MI_RADIUS); - const layout_scale = hook.readMem(f32, info + ADDR.MI_LAYOUT_SCALE); - const unk_scale = getFrameUnkScale(info); + // Cache minimap info per frame — same for all objects in one enumeration cycle + if (!g_minimap_info.valid) { + g_minimap_info = .{ + .cur = .{ + .x = hook.readMem(f32, info + ADDR.MI_POS), + .y = hook.readMem(f32, info + ADDR.MI_POS + 4), + .z = hook.readMem(f32, info + ADDR.MI_POS + 8), + }, + .radius = hook.readMem(f32, info + ADDR.MI_RADIUS), + .layout_scale = hook.readMem(f32, info + ADDR.MI_LAYOUT_SCALE), + .unk_scale = getFrameUnkScale(info), + .valid = true, + }; + } var minimap_pos: C2Vector = undefined; - worldPosToMinimapCoords(&minimap_pos, cur, radius, pos.x, pos.y, layout_scale, unk_scale); + worldPosToMinimapCoords(&minimap_pos, g_minimap_info.cur, g_minimap_info.radius, pos.x, pos.y, g_minimap_info.layout_scale, g_minimap_info.unk_scale); g_blips[g_blip_count] = .{ .pos = minimap_pos, @@ -719,25 +805,31 @@ fn trackObject(info: u32, obj: u32, blip: Blip) void { } fn refreshActiveTrackingCache() void { + var has_unit = false; + var has_go = false; + var has_filters = false; + var flag_mask: u32 = 0; for (&g_flag_tracking) |*entry| { if (entry.active) { - g_has_active_tracking = true; - return; + has_unit = true; + flag_mask |= entry.flag; + if (entry.hasFilter()) has_filters = true; } } for (&g_go_tracking) |*entry| { - if (entry.active) { - g_has_active_tracking = true; - return; - } + if (entry.active) has_go = true; } for (&g_go_id_tracking) |*entry| { - if (entry.active) { - g_has_active_tracking = true; - return; - } + if (entry.active) has_go = true; } - g_has_active_tracking = false; + g_has_active_unit_tracking = has_unit; + g_has_active_go_tracking = has_go; + g_has_active_tracking = has_unit or has_go; + g_has_any_filters = has_filters; + g_active_flag_mask = flag_mask; + + // Config changed — cached match results may be stale + guidCacheClear(); } // ============================================================================= @@ -767,13 +859,41 @@ var render_blips_hook: hook.Detour(RenderBlipsFn) = .{}; fn renderObjectBlipsDetour(thisptr: u32, _edx: u32, dn_info: u32) callconv(fc) void { render_blips_hook.callOriginal(.{ thisptr, _edx, dn_info }); - // Draw our custom blips after the original ones - for (0..g_blip_count) |i| { - drawMinimapTexture( - g_blips[i].blip.texture, - g_blips[i].pos, - g_blips[i].blip.scale, - ); + const count = g_blip_count; + if (count == 0) return; + + // Sort blips by texture handle to batch draw calls + const blips = g_blips[0..count]; + std.mem.sort(TrackedBlip, blips, {}, struct { + fn lessThan(_: void, a: TrackedBlip, b: TrackedBlip) bool { + return a.blip.texture < b.blip.texture; + } + }.lessThan); + + // Draw batched by texture — one GxRsSet per unique texture + var i: usize = 0; + while (i < count) { + const tex = blips[i].blip.texture; + if (tex == 0) { + i += 1; + continue; + } + + // Bind texture once for this group + const gx_tex = getGxTex(tex); + if (gx_tex == 0) { + // Skip all blips with this texture + while (i < count and blips[i].blip.texture == tex) : (i += 1) {} + continue; + } + + const gxRsSet: *const fn (u32, u32) callconv(fc) void = @ptrFromInt(ADDR.GxRsSet); + gxRsSet(23, gx_tex); + + // Draw all blips sharing this texture + while (i < count and blips[i].blip.texture == tex) : (i += 1) { + drawMinimapBlip(blips[i].pos, blips[i].blip.scale); + } } } @@ -785,6 +905,7 @@ fn enumVisibleObjectsDetour(callback: u32, context: u32) callconv(fc) i32 { // Clear tracked blips when the minimap's own callback is about to enumerate if (callback == ADDR.ObjectEnumProc) { g_blip_count = 0; + g_minimap_info.valid = false; } return enum_vis_hook.callOriginal(.{ callback, context }); } diff --git a/src/minimapicons/png2blp.py b/src/minimapicons/png2blp.py new file mode 100644 index 0000000..abb991c --- /dev/null +++ b/src/minimapicons/png2blp.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Convert PNG to BLP2 (DXT3 compressed, with mipmaps).""" + +import struct +import sys +from PIL import Image + + +def rgb_to_565(r, g, b): + return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3) + + +def color_565_to_rgb(c): + r = ((c >> 11) & 0x1F) << 3 + g = ((c >> 5) & 0x3F) << 2 + b = (c & 0x1F) << 3 + return r, g, b + + +def color_distance(r0, g0, b0, r1, g1, b1): + return (r0 - r1) ** 2 + (g0 - g1) ** 2 + (b0 - b1) ** 2 + + +def encode_dxt1_block(pixels): + """Encode a 4x4 block of (r,g,b) tuples into 8-byte DXT1 color block. + pixels: list of 16 (r,g,b) tuples in row-major order. + """ + # Find min/max colors by luminance + min_c = min(pixels, key=lambda c: c[0] * 299 + c[1] * 587 + c[2] * 114) + max_c = max(pixels, key=lambda c: c[0] * 299 + c[1] * 587 + c[2] * 114) + + color0 = rgb_to_565(*max_c) + color1 = rgb_to_565(*min_c) + + # Ensure color0 > color1 for 4-color mode + if color0 == color1: + # All same color, indices all 0 + return struct.pack('> 4) << (i * 4) + + alpha_data = struct.pack(' 1 or h > 1: + w = max(1, w // 2) + h = max(1, h // 2) + levels.append(img.resize((w, h), Image.LANCZOS)) + return [(l.size[0], l.size[1], l.tobytes()) for l in levels] + + +def png_to_blp2(png_path, blp_path): + img = Image.open(png_path).convert('RGBA') + width, height = img.size + + mipmaps = generate_mipmaps(img) + num_mipmaps = min(len(mipmaps), 16) + + # Encode each mipmap level as DXT3 + encoded = [] + for w, h, data in mipmaps[:num_mipmaps]: + encoded.append(encode_dxt3_image(data, w, h)) + + # BLP2 header: 1172 bytes + # 4 (magic) + 4 (type) + 1 (compression) + 1 (alpha_depth) + 1 (alpha_type) + # + 1 (has_mips) + 4 (width) + 4 (height) + 64 (offsets) + 64 (lengths) + # + 1024 (palette) = 1172 + header_size = 4 + 4 + 1 + 1 + 1 + 1 + 4 + 4 + 64 + 64 + 1024 + + # Calculate offsets + offsets = [0] * 16 + lengths = [0] * 16 + offset = header_size + for i, data in enumerate(encoded): + offsets[i] = offset + lengths[i] = len(data) + offset += len(data) + + # Build header + header = bytearray() + header.extend(b'BLP2') + header.extend(struct.pack(' {blp_path}') + print(f' {width}x{height}, {num_mipmaps} mipmaps, DXT3') + print(f' {offset} bytes total') + + +if __name__ == '__main__': + if len(sys.argv) != 3: + print(f'Usage: {sys.argv[0]} input.png output.blp') + sys.exit(1) + png_to_blp2(sys.argv[1], sys.argv[2])