2026-02-02 12:24:50 -08:00
|
|
|
|
#include "rendering/minimap.hpp"
|
2026-02-21 19:41:21 -08:00
|
|
|
|
#include "rendering/vk_context.hpp"
|
|
|
|
|
|
#include "rendering/vk_texture.hpp"
|
|
|
|
|
|
#include "rendering/vk_render_target.hpp"
|
|
|
|
|
|
#include "rendering/vk_pipeline.hpp"
|
|
|
|
|
|
#include "rendering/vk_shader.hpp"
|
|
|
|
|
|
#include "rendering/vk_utils.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
|
#include "rendering/camera.hpp"
|
2026-02-04 20:06:27 -08:00
|
|
|
|
#include "pipeline/asset_manager.hpp"
|
|
|
|
|
|
#include "pipeline/blp_loader.hpp"
|
|
|
|
|
|
#include "core/coordinates.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
|
#include <glm/gtc/matrix_transform.hpp>
|
2026-02-04 20:06:27 -08:00
|
|
|
|
#include <sstream>
|
|
|
|
|
|
#include <cmath>
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
|
|
namespace wowee {
|
|
|
|
|
|
namespace rendering {
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Push constant for tile composite vertex shader
|
|
|
|
|
|
struct MinimapTilePush {
|
|
|
|
|
|
glm::vec2 gridOffset; // 8 bytes
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Push constant for display vertex + fragment shaders
|
|
|
|
|
|
struct MinimapDisplayPush {
|
|
|
|
|
|
glm::vec4 rect; // x, y, w, h in 0..1 screen space
|
|
|
|
|
|
glm::vec2 playerUV;
|
|
|
|
|
|
float rotation;
|
|
|
|
|
|
float arrowRotation;
|
|
|
|
|
|
float zoomRadius;
|
|
|
|
|
|
int32_t squareShape;
|
2026-02-23 08:01:20 -08:00
|
|
|
|
float opacity;
|
|
|
|
|
|
}; // 44 bytes
|
2026-02-21 19:41:21 -08:00
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
Minimap::Minimap() = default;
|
|
|
|
|
|
|
|
|
|
|
|
Minimap::~Minimap() {
|
|
|
|
|
|
shutdown();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
bool Minimap::initialize(VkContext* ctx, VkDescriptorSetLayout /*perFrameLayout*/, int size) {
|
|
|
|
|
|
vkCtx = ctx;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
mapSize = size;
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// --- Composite render target (768x768) ---
|
|
|
|
|
|
compositeTarget = std::make_unique<VkRenderTarget>();
|
|
|
|
|
|
if (!compositeTarget->create(*vkCtx, COMPOSITE_PX, COMPOSITE_PX)) {
|
|
|
|
|
|
LOG_ERROR("Minimap: failed to create composite render target");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// --- No-data fallback texture (dark blue-gray, 1x1) ---
|
|
|
|
|
|
noDataTexture = std::make_unique<VkTexture>();
|
|
|
|
|
|
uint8_t darkPixel[4] = { 12, 20, 30, 255 };
|
|
|
|
|
|
noDataTexture->upload(*vkCtx, darkPixel, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
|
|
|
|
|
|
noDataTexture->createSampler(device, VK_FILTER_NEAREST, VK_FILTER_NEAREST,
|
|
|
|
|
|
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// --- Shared quad vertex buffer (unit quad: pos2 + uv2) ---
|
2026-02-02 12:24:50 -08:00
|
|
|
|
float quadVerts[] = {
|
|
|
|
|
|
// pos (x,y), uv (u,v)
|
2026-02-04 20:06:27 -08:00
|
|
|
|
0.0f, 0.0f, 0.0f, 0.0f,
|
|
|
|
|
|
1.0f, 0.0f, 1.0f, 0.0f,
|
|
|
|
|
|
1.0f, 1.0f, 1.0f, 1.0f,
|
|
|
|
|
|
0.0f, 0.0f, 0.0f, 0.0f,
|
|
|
|
|
|
1.0f, 1.0f, 1.0f, 1.0f,
|
|
|
|
|
|
0.0f, 1.0f, 0.0f, 1.0f,
|
2026-02-02 12:24:50 -08:00
|
|
|
|
};
|
2026-02-21 19:41:21 -08:00
|
|
|
|
auto quadBuf = uploadBuffer(*vkCtx, quadVerts, sizeof(quadVerts),
|
|
|
|
|
|
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
|
|
|
|
|
|
quadVB = quadBuf.buffer;
|
|
|
|
|
|
quadVBAlloc = quadBuf.allocation;
|
|
|
|
|
|
|
|
|
|
|
|
// --- Descriptor set layout: 1 combined image sampler at binding 0 (fragment) ---
|
|
|
|
|
|
VkDescriptorSetLayoutBinding samplerBinding{};
|
|
|
|
|
|
samplerBinding.binding = 0;
|
|
|
|
|
|
samplerBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
samplerBinding.descriptorCount = 1;
|
|
|
|
|
|
samplerBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
|
|
|
|
samplerSetLayout = createDescriptorSetLayout(device, { samplerBinding });
|
|
|
|
|
|
|
|
|
|
|
|
// --- Descriptor pool ---
|
|
|
|
|
|
VkDescriptorPoolSize poolSize{};
|
|
|
|
|
|
poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
poolSize.descriptorCount = MAX_DESC_SETS;
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorPoolCreateInfo poolInfo{};
|
|
|
|
|
|
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
|
|
|
|
|
poolInfo.maxSets = MAX_DESC_SETS;
|
|
|
|
|
|
poolInfo.poolSizeCount = 1;
|
|
|
|
|
|
poolInfo.pPoolSizes = &poolSize;
|
|
|
|
|
|
vkCreateDescriptorPool(device, &poolInfo, nullptr, &descPool);
|
|
|
|
|
|
|
|
|
|
|
|
// --- Allocate all descriptor sets ---
|
|
|
|
|
|
// 18 tile sets (2 frames × 9 tiles) + 1 display set = 19 total
|
|
|
|
|
|
std::vector<VkDescriptorSetLayout> layouts(19, samplerSetLayout);
|
|
|
|
|
|
VkDescriptorSetAllocateInfo allocInfo{};
|
|
|
|
|
|
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
|
|
|
|
|
allocInfo.descriptorPool = descPool;
|
|
|
|
|
|
allocInfo.descriptorSetCount = 19;
|
|
|
|
|
|
allocInfo.pSetLayouts = layouts.data();
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorSet allSets[19];
|
|
|
|
|
|
vkAllocateDescriptorSets(device, &allocInfo, allSets);
|
|
|
|
|
|
|
|
|
|
|
|
for (int f = 0; f < 2; f++)
|
|
|
|
|
|
for (int t = 0; t < 9; t++)
|
|
|
|
|
|
tileDescSets[f][t] = allSets[f * 9 + t];
|
|
|
|
|
|
displayDescSet = allSets[18];
|
|
|
|
|
|
|
|
|
|
|
|
// --- Write display descriptor set → composite render target ---
|
|
|
|
|
|
VkDescriptorImageInfo compositeImgInfo = compositeTarget->descriptorInfo();
|
|
|
|
|
|
VkWriteDescriptorSet displayWrite{};
|
|
|
|
|
|
displayWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
|
|
|
|
displayWrite.dstSet = displayDescSet;
|
|
|
|
|
|
displayWrite.dstBinding = 0;
|
|
|
|
|
|
displayWrite.descriptorCount = 1;
|
|
|
|
|
|
displayWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
displayWrite.pImageInfo = &compositeImgInfo;
|
|
|
|
|
|
vkUpdateDescriptorSets(device, 1, &displayWrite, 0, nullptr);
|
|
|
|
|
|
|
|
|
|
|
|
// --- Tile pipeline layout: samplerSetLayout + 8-byte push constant (vertex) ---
|
|
|
|
|
|
VkPushConstantRange tilePush{};
|
|
|
|
|
|
tilePush.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
|
|
|
|
|
|
tilePush.offset = 0;
|
|
|
|
|
|
tilePush.size = sizeof(MinimapTilePush);
|
|
|
|
|
|
tilePipelineLayout = createPipelineLayout(device, { samplerSetLayout }, { tilePush });
|
|
|
|
|
|
|
|
|
|
|
|
// --- Display pipeline layout: samplerSetLayout + 40-byte push constant (vert+frag) ---
|
|
|
|
|
|
VkPushConstantRange displayPush{};
|
|
|
|
|
|
displayPush.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
|
|
|
|
displayPush.offset = 0;
|
|
|
|
|
|
displayPush.size = sizeof(MinimapDisplayPush);
|
|
|
|
|
|
displayPipelineLayout = createPipelineLayout(device, { samplerSetLayout }, { displayPush });
|
|
|
|
|
|
|
|
|
|
|
|
// --- Vertex input: pos2 (loc 0) + uv2 (loc 1), stride 16 ---
|
|
|
|
|
|
VkVertexInputBindingDescription binding{};
|
|
|
|
|
|
binding.binding = 0;
|
|
|
|
|
|
binding.stride = 4 * sizeof(float);
|
|
|
|
|
|
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<VkVertexInputAttributeDescription> attrs(2);
|
|
|
|
|
|
attrs[0] = { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 }; // aPos
|
|
|
|
|
|
attrs[1] = { 1, 0, VK_FORMAT_R32G32_SFLOAT, 2 * sizeof(float) }; // aUV
|
|
|
|
|
|
|
|
|
|
|
|
// --- Load tile shaders ---
|
|
|
|
|
|
{
|
|
|
|
|
|
VkShaderModule vs, fs;
|
|
|
|
|
|
if (!vs.loadFromFile(device, "assets/shaders/minimap_tile.vert.spv") ||
|
|
|
|
|
|
!fs.loadFromFile(device, "assets/shaders/minimap_tile.frag.spv")) {
|
|
|
|
|
|
LOG_ERROR("Minimap: failed to load tile shaders");
|
|
|
|
|
|
return false;
|
2026-02-04 20:06:27 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
tilePipeline = PipelineBuilder()
|
|
|
|
|
|
.setShaders(vs.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
|
fs.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
|
.setVertexInput({ binding }, attrs)
|
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
|
|
|
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
|
|
|
|
|
.setNoDepthTest()
|
|
|
|
|
|
.setColorBlendAttachment(PipelineBuilder::blendDisabled())
|
|
|
|
|
|
.setLayout(tilePipelineLayout)
|
|
|
|
|
|
.setRenderPass(compositeTarget->getRenderPass())
|
|
|
|
|
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
|
|
|
|
|
.build(device);
|
|
|
|
|
|
|
|
|
|
|
|
vs.destroy();
|
|
|
|
|
|
fs.destroy();
|
2026-02-04 20:06:27 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// --- Load display shaders ---
|
|
|
|
|
|
{
|
|
|
|
|
|
VkShaderModule vs, fs;
|
|
|
|
|
|
if (!vs.loadFromFile(device, "assets/shaders/minimap_display.vert.spv") ||
|
|
|
|
|
|
!fs.loadFromFile(device, "assets/shaders/minimap_display.frag.spv")) {
|
|
|
|
|
|
LOG_ERROR("Minimap: failed to load display shaders");
|
|
|
|
|
|
return false;
|
2026-02-04 20:06:27 -08:00
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
displayPipeline = PipelineBuilder()
|
|
|
|
|
|
.setShaders(vs.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
|
fs.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
|
.setVertexInput({ binding }, attrs)
|
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
|
|
|
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
|
|
|
|
|
.setNoDepthTest()
|
|
|
|
|
|
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
2026-02-22 03:49:44 -08:00
|
|
|
|
.setMultisample(vkCtx->getMsaaSamples())
|
2026-02-21 19:41:21 -08:00
|
|
|
|
.setLayout(displayPipelineLayout)
|
|
|
|
|
|
.setRenderPass(vkCtx->getImGuiRenderPass())
|
|
|
|
|
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
|
|
|
|
|
.build(device);
|
|
|
|
|
|
|
|
|
|
|
|
vs.destroy();
|
|
|
|
|
|
fs.destroy();
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (!tilePipeline || !displayPipeline) {
|
|
|
|
|
|
LOG_ERROR("Minimap: failed to create pipelines");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 20:06:27 -08:00
|
|
|
|
LOG_INFO("Minimap initialized (", mapSize, "x", mapSize, " screen, ",
|
|
|
|
|
|
COMPOSITE_PX, "x", COMPOSITE_PX, " composite)");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Minimap::shutdown() {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (!vkCtx) return;
|
|
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
|
|
|
|
|
VmaAllocator alloc = vkCtx->getAllocator();
|
|
|
|
|
|
|
|
|
|
|
|
vkDeviceWaitIdle(device);
|
|
|
|
|
|
|
|
|
|
|
|
if (tilePipeline) { vkDestroyPipeline(device, tilePipeline, nullptr); tilePipeline = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (displayPipeline) { vkDestroyPipeline(device, displayPipeline, nullptr); displayPipeline = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (tilePipelineLayout) { vkDestroyPipelineLayout(device, tilePipelineLayout, nullptr); tilePipelineLayout = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (displayPipelineLayout) { vkDestroyPipelineLayout(device, displayPipelineLayout, nullptr); displayPipelineLayout = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (descPool) { vkDestroyDescriptorPool(device, descPool, nullptr); descPool = VK_NULL_HANDLE; }
|
|
|
|
|
|
if (samplerSetLayout) { vkDestroyDescriptorSetLayout(device, samplerSetLayout, nullptr); samplerSetLayout = VK_NULL_HANDLE; }
|
|
|
|
|
|
|
|
|
|
|
|
if (quadVB) { vmaDestroyBuffer(alloc, quadVB, quadVBAlloc); quadVB = VK_NULL_HANDLE; }
|
|
|
|
|
|
|
2026-02-04 20:06:27 -08:00
|
|
|
|
for (auto& [hash, tex] : tileTextureCache) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (tex) tex->destroy(device, alloc);
|
2026-02-04 20:06:27 -08:00
|
|
|
|
}
|
|
|
|
|
|
tileTextureCache.clear();
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (noDataTexture) { noDataTexture->destroy(device, alloc); noDataTexture.reset(); }
|
|
|
|
|
|
if (compositeTarget) { compositeTarget->destroy(device, alloc); compositeTarget.reset(); }
|
|
|
|
|
|
|
|
|
|
|
|
vkCtx = nullptr;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 03:49:44 -08:00
|
|
|
|
void Minimap::recreatePipelines() {
|
|
|
|
|
|
if (!vkCtx || !displayPipelineLayout) return;
|
|
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
|
|
|
|
|
|
|
|
|
|
|
if (displayPipeline) { vkDestroyPipeline(device, displayPipeline, nullptr); displayPipeline = VK_NULL_HANDLE; }
|
|
|
|
|
|
|
|
|
|
|
|
VkShaderModule vs, fs;
|
|
|
|
|
|
if (!vs.loadFromFile(device, "assets/shaders/minimap_display.vert.spv") ||
|
|
|
|
|
|
!fs.loadFromFile(device, "assets/shaders/minimap_display.frag.spv")) {
|
|
|
|
|
|
LOG_ERROR("Minimap: failed to reload display shaders for pipeline recreation");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
VkVertexInputBindingDescription binding{};
|
|
|
|
|
|
binding.binding = 0;
|
|
|
|
|
|
binding.stride = 4 * sizeof(float);
|
|
|
|
|
|
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<VkVertexInputAttributeDescription> attrs(2);
|
|
|
|
|
|
attrs[0] = { 0, 0, VK_FORMAT_R32G32_SFLOAT, 0 };
|
|
|
|
|
|
attrs[1] = { 1, 0, VK_FORMAT_R32G32_SFLOAT, 2 * sizeof(float) };
|
|
|
|
|
|
|
|
|
|
|
|
displayPipeline = PipelineBuilder()
|
|
|
|
|
|
.setShaders(vs.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
|
|
|
|
|
|
fs.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
|
|
|
|
|
|
.setVertexInput({ binding }, attrs)
|
|
|
|
|
|
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
|
|
|
|
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
|
|
|
|
|
|
.setNoDepthTest()
|
|
|
|
|
|
.setColorBlendAttachment(PipelineBuilder::blendAlpha())
|
|
|
|
|
|
.setMultisample(vkCtx->getMsaaSamples())
|
|
|
|
|
|
.setLayout(displayPipelineLayout)
|
|
|
|
|
|
.setRenderPass(vkCtx->getImGuiRenderPass())
|
|
|
|
|
|
.setDynamicStates({ VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR })
|
|
|
|
|
|
.build(device);
|
|
|
|
|
|
|
|
|
|
|
|
vs.destroy();
|
|
|
|
|
|
fs.destroy();
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Minimap: display pipeline recreated with MSAA ", static_cast<int>(vkCtx->getMsaaSamples()), "x");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 20:06:27 -08:00
|
|
|
|
void Minimap::setMapName(const std::string& name) {
|
|
|
|
|
|
if (mapName != name) {
|
|
|
|
|
|
mapName = name;
|
|
|
|
|
|
hasCachedFrame = false;
|
|
|
|
|
|
lastCenterTileX = -1;
|
|
|
|
|
|
lastCenterTileY = -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-04 20:06:27 -08:00
|
|
|
|
// --------------------------------------------------------
|
|
|
|
|
|
// TRS parsing
|
|
|
|
|
|
// --------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
void Minimap::parseTRS() {
|
|
|
|
|
|
if (trsParsed || !assetManager) return;
|
|
|
|
|
|
trsParsed = true;
|
|
|
|
|
|
|
2026-02-12 20:32:14 -08:00
|
|
|
|
auto data = assetManager->readFile("Textures\\Minimap\\md5translate.trs");
|
2026-02-04 20:06:27 -08:00
|
|
|
|
if (data.empty()) {
|
|
|
|
|
|
LOG_WARNING("Failed to load md5translate.trs");
|
|
|
|
|
|
return;
|
2026-02-03 17:21:04 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 20:06:27 -08:00
|
|
|
|
std::string content(reinterpret_cast<const char*>(data.data()), data.size());
|
|
|
|
|
|
std::istringstream stream(content);
|
|
|
|
|
|
std::string line;
|
|
|
|
|
|
int count = 0;
|
|
|
|
|
|
|
|
|
|
|
|
while (std::getline(stream, line)) {
|
|
|
|
|
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
|
|
|
|
|
if (line.empty() || line.substr(0, 4) == "dir:") continue;
|
|
|
|
|
|
|
|
|
|
|
|
auto tabPos = line.find('\t');
|
|
|
|
|
|
if (tabPos == std::string::npos) continue;
|
|
|
|
|
|
|
|
|
|
|
|
std::string key = line.substr(0, tabPos);
|
|
|
|
|
|
std::string hashFile = line.substr(tabPos + 1);
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (key.size() > 4 && key.substr(key.size() - 4) == ".blp")
|
2026-02-04 20:06:27 -08:00
|
|
|
|
key = key.substr(0, key.size() - 4);
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (hashFile.size() > 4 && hashFile.substr(hashFile.size() - 4) == ".blp")
|
2026-02-04 20:06:27 -08:00
|
|
|
|
hashFile = hashFile.substr(0, hashFile.size() - 4);
|
|
|
|
|
|
|
|
|
|
|
|
trsLookup[key] = hashFile;
|
|
|
|
|
|
count++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Parsed md5translate.trs: ", count, " entries");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------------
|
|
|
|
|
|
// Tile texture loading
|
|
|
|
|
|
// --------------------------------------------------------
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkTexture* Minimap::getOrLoadTileTexture(int tileX, int tileY) {
|
|
|
|
|
|
if (!trsParsed) parseTRS();
|
|
|
|
|
|
|
2026-02-04 20:06:27 -08:00
|
|
|
|
std::string key = mapName + "\\map" + std::to_string(tileX) + "_" + std::to_string(tileY);
|
|
|
|
|
|
|
|
|
|
|
|
auto trsIt = trsLookup.find(key);
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (trsIt == trsLookup.end())
|
|
|
|
|
|
return noDataTexture.get();
|
2026-02-04 20:06:27 -08:00
|
|
|
|
|
|
|
|
|
|
const std::string& hash = trsIt->second;
|
|
|
|
|
|
|
|
|
|
|
|
auto cacheIt = tileTextureCache.find(hash);
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (cacheIt != tileTextureCache.end())
|
|
|
|
|
|
return cacheIt->second.get();
|
2026-02-04 20:06:27 -08:00
|
|
|
|
|
|
|
|
|
|
// Load from MPQ
|
|
|
|
|
|
std::string blpPath = "Textures\\Minimap\\" + hash + ".blp";
|
|
|
|
|
|
auto blpImage = assetManager->loadTexture(blpPath);
|
|
|
|
|
|
if (!blpImage.isValid()) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
tileTextureCache[hash] = nullptr; // Mark as failed
|
|
|
|
|
|
return noDataTexture.get();
|
2026-02-03 17:21:04 -08:00
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
auto tex = std::make_unique<VkTexture>();
|
|
|
|
|
|
tex->upload(*vkCtx, blpImage.data.data(), blpImage.width, blpImage.height,
|
|
|
|
|
|
VK_FORMAT_R8G8B8A8_UNORM, false);
|
|
|
|
|
|
tex->createSampler(vkCtx->getDevice(), VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
|
|
|
|
|
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
VkTexture* ptr = tex.get();
|
|
|
|
|
|
tileTextureCache[hash] = std::move(tex);
|
|
|
|
|
|
return ptr;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-04 20:06:27 -08:00
|
|
|
|
// --------------------------------------------------------
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Update tile descriptor sets for composite pass
|
2026-02-04 20:06:27 -08:00
|
|
|
|
// --------------------------------------------------------
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
void Minimap::updateTileDescriptors(uint32_t frameIdx, int centerTileX, int centerTileY) {
|
|
|
|
|
|
VkDevice device = vkCtx->getDevice();
|
|
|
|
|
|
int slot = 0;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-04 20:06:27 -08:00
|
|
|
|
for (int dr = -1; dr <= 1; dr++) {
|
|
|
|
|
|
for (int dc = -1; dc <= 1; dc++) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
int tx = centerTileX + dr;
|
|
|
|
|
|
int ty = centerTileY + dc;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkTexture* tileTex = getOrLoadTileTexture(tx, ty);
|
|
|
|
|
|
if (!tileTex || !tileTex->isValid())
|
|
|
|
|
|
tileTex = noDataTexture.get();
|
2026-02-04 20:06:27 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkDescriptorImageInfo imgInfo = tileTex->descriptorInfo();
|
2026-02-04 20:06:27 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
VkWriteDescriptorSet write{};
|
|
|
|
|
|
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
|
|
|
|
write.dstSet = tileDescSets[frameIdx][slot];
|
|
|
|
|
|
write.dstBinding = 0;
|
|
|
|
|
|
write.descriptorCount = 1;
|
|
|
|
|
|
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
|
|
|
|
write.pImageInfo = &imgInfo;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
|
|
|
|
|
|
slot++;
|
2026-02-04 20:06:27 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------------
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// Off-screen composite pass (call BEFORE main render pass)
|
2026-02-04 20:06:27 -08:00
|
|
|
|
// --------------------------------------------------------
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
void Minimap::compositePass(VkCommandBuffer cmd, const glm::vec3& centerWorldPos) {
|
|
|
|
|
|
if (!enabled || !assetManager || !compositeTarget || !compositeTarget->isValid()) return;
|
2026-02-04 20:06:27 -08:00
|
|
|
|
|
|
|
|
|
|
if (!trsParsed) parseTRS();
|
|
|
|
|
|
|
|
|
|
|
|
// Check if composite needs refresh
|
|
|
|
|
|
const auto now = std::chrono::steady_clock::now();
|
|
|
|
|
|
bool needsRefresh = !hasCachedFrame;
|
|
|
|
|
|
if (!needsRefresh) {
|
|
|
|
|
|
float moved = glm::length(glm::vec2(centerWorldPos.x - lastUpdatePos.x,
|
|
|
|
|
|
centerWorldPos.y - lastUpdatePos.y));
|
|
|
|
|
|
float elapsed = std::chrono::duration<float>(now - lastUpdateTime).count();
|
|
|
|
|
|
needsRefresh = (moved >= updateDistance) || (elapsed >= updateIntervalSec);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Also refresh if player crossed a tile boundary
|
|
|
|
|
|
auto [curTileX, curTileY] = core::coords::worldToTile(centerWorldPos.x, centerWorldPos.y);
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (curTileX != lastCenterTileX || curTileY != lastCenterTileY)
|
2026-02-04 20:06:27 -08:00
|
|
|
|
needsRefresh = true;
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (!needsRefresh) return;
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t frameIdx = vkCtx->getCurrentFrame();
|
|
|
|
|
|
|
|
|
|
|
|
// Update tile descriptor sets
|
|
|
|
|
|
updateTileDescriptors(frameIdx, curTileX, curTileY);
|
|
|
|
|
|
|
|
|
|
|
|
// Begin off-screen render pass
|
|
|
|
|
|
VkClearColorValue clearColor = {{ 0.05f, 0.08f, 0.12f, 1.0f }};
|
|
|
|
|
|
compositeTarget->beginPass(cmd, clearColor);
|
|
|
|
|
|
|
|
|
|
|
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, tilePipeline);
|
|
|
|
|
|
|
|
|
|
|
|
VkDeviceSize offset = 0;
|
|
|
|
|
|
vkCmdBindVertexBuffers(cmd, 0, 1, &quadVB, &offset);
|
|
|
|
|
|
|
|
|
|
|
|
// Draw 3x3 tile grid
|
|
|
|
|
|
int slot = 0;
|
|
|
|
|
|
for (int dr = -1; dr <= 1; dr++) {
|
|
|
|
|
|
for (int dc = -1; dc <= 1; dc++) {
|
|
|
|
|
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
|
|
|
|
|
tilePipelineLayout, 0, 1,
|
|
|
|
|
|
&tileDescSets[frameIdx][slot], 0, nullptr);
|
|
|
|
|
|
|
|
|
|
|
|
MinimapTilePush push{};
|
|
|
|
|
|
push.gridOffset = glm::vec2(static_cast<float>(dc + 1),
|
|
|
|
|
|
static_cast<float>(dr + 1));
|
|
|
|
|
|
vkCmdPushConstants(cmd, tilePipelineLayout, VK_SHADER_STAGE_VERTEX_BIT,
|
|
|
|
|
|
0, sizeof(push), &push);
|
|
|
|
|
|
|
|
|
|
|
|
vkCmdDraw(cmd, 6, 1, 0, 0);
|
|
|
|
|
|
slot++;
|
|
|
|
|
|
}
|
2026-02-04 20:06:27 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
compositeTarget->endPass(cmd);
|
|
|
|
|
|
|
|
|
|
|
|
// Update tracking
|
|
|
|
|
|
lastCenterTileX = curTileX;
|
|
|
|
|
|
lastCenterTileY = curTileY;
|
|
|
|
|
|
lastUpdateTime = now;
|
|
|
|
|
|
lastUpdatePos = centerWorldPos;
|
|
|
|
|
|
hasCachedFrame = true;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
// --------------------------------------------------------
|
|
|
|
|
|
// Display quad (call INSIDE main render pass)
|
|
|
|
|
|
// --------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
void Minimap::render(VkCommandBuffer cmd, const Camera& playerCamera,
|
|
|
|
|
|
const glm::vec3& centerWorldPos,
|
2026-02-22 08:44:16 -08:00
|
|
|
|
int screenWidth, int screenHeight,
|
|
|
|
|
|
float playerOrientation, bool hasPlayerOrientation) {
|
2026-02-21 19:41:21 -08:00
|
|
|
|
if (!enabled || !hasCachedFrame || !displayPipeline) return;
|
|
|
|
|
|
|
|
|
|
|
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, displayPipeline);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
|
|
|
|
|
displayPipelineLayout, 0, 1,
|
|
|
|
|
|
&displayDescSet, 0, nullptr);
|
|
|
|
|
|
|
|
|
|
|
|
VkDeviceSize offset = 0;
|
|
|
|
|
|
vkCmdBindVertexBuffers(cmd, 0, 1, &quadVB, &offset);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-04 20:06:27 -08:00
|
|
|
|
// Position minimap in top-right corner
|
2026-02-02 12:24:50 -08:00
|
|
|
|
float margin = 10.0f;
|
|
|
|
|
|
float pixelW = static_cast<float>(mapSize) / screenWidth;
|
|
|
|
|
|
float pixelH = static_cast<float>(mapSize) / screenHeight;
|
|
|
|
|
|
float x = 1.0f - pixelW - margin / screenWidth;
|
2026-02-22 02:59:24 -08:00
|
|
|
|
float y = margin / screenHeight; // top edge in Vulkan (y=0 is top)
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-04 20:06:27 -08:00
|
|
|
|
// Compute player's UV in the composite texture
|
|
|
|
|
|
constexpr float TILE_SIZE = core::coords::TILE_SIZE;
|
|
|
|
|
|
auto [tileX, tileY] = core::coords::worldToTile(centerWorldPos.x, centerWorldPos.y);
|
|
|
|
|
|
|
|
|
|
|
|
float fracNS = 32.0f - static_cast<float>(tileX) - centerWorldPos.y / TILE_SIZE;
|
|
|
|
|
|
float fracEW = 32.0f - static_cast<float>(tileY) - centerWorldPos.x / TILE_SIZE;
|
|
|
|
|
|
|
|
|
|
|
|
float playerU = (1.0f + fracEW) / 3.0f;
|
|
|
|
|
|
float playerV = (1.0f + fracNS) / 3.0f;
|
|
|
|
|
|
|
|
|
|
|
|
float zoomRadius = viewRadius / (TILE_SIZE * 3.0f);
|
|
|
|
|
|
|
2026-02-07 20:51:53 -08:00
|
|
|
|
float rotation = 0.0f;
|
|
|
|
|
|
if (rotateWithCamera) {
|
|
|
|
|
|
glm::vec3 fwd = playerCamera.getForward();
|
|
|
|
|
|
rotation = std::atan2(-fwd.x, fwd.y);
|
|
|
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
|
|
2026-02-07 20:51:53 -08:00
|
|
|
|
float arrowRotation = 0.0f;
|
|
|
|
|
|
if (!rotateWithCamera) {
|
2026-02-22 09:34:27 -08:00
|
|
|
|
// Prefer authoritative orientation if provided. This value is expected
|
|
|
|
|
|
// to already match minimap shader rotation convention.
|
2026-02-22 08:44:16 -08:00
|
|
|
|
if (hasPlayerOrientation) {
|
|
|
|
|
|
arrowRotation = playerOrientation;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
glm::vec3 fwd = playerCamera.getForward();
|
|
|
|
|
|
arrowRotation = std::atan2(-fwd.x, fwd.y);
|
|
|
|
|
|
}
|
2026-02-07 20:51:53 -08:00
|
|
|
|
}
|
2026-02-04 20:06:27 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
MinimapDisplayPush push{};
|
|
|
|
|
|
push.rect = glm::vec4(x, y, pixelW, pixelH);
|
|
|
|
|
|
push.playerUV = glm::vec2(playerU, playerV);
|
|
|
|
|
|
push.rotation = rotation;
|
|
|
|
|
|
push.arrowRotation = arrowRotation;
|
|
|
|
|
|
push.zoomRadius = zoomRadius;
|
|
|
|
|
|
push.squareShape = squareShape ? 1 : 0;
|
2026-02-23 08:01:20 -08:00
|
|
|
|
push.opacity = opacity_;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
vkCmdPushConstants(cmd, displayPipelineLayout,
|
|
|
|
|
|
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
|
|
|
|
|
0, sizeof(push), &push);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
|
vkCmdDraw(cmd, 6, 1, 0, 0);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} // namespace rendering
|
|
|
|
|
|
} // namespace wowee
|