Compare commits

...

2 commits

Author SHA1 Message Date
Kelsi
cefb05c027 Extend Deeprun Tram portal trigger range to bypass WMO collision walls
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
2026-03-05 21:17:45 -08:00
Kelsi
d763d71bf3 Fix NPC clothing geosets, preserve armor textures, bald scalp color
- Equipment-driven geoset selection: read GeosetGroup1 from ItemDisplayInfo
  for legs/feet/chest to pick covered mesh variants (1302+ pants, 402+ boots,
  802+ sleeves) instead of always defaulting to bare geosets
- Prevent per-instance skin override from replacing baked/composited armor
  textures on equipped NPCs
- Set bald NPC hair texture slot to skin texture so scalp isn't white
- Skip white fallback textures in per-instance hair overrides
- Remove debug texture dump, reduce NPC logging to DEBUG level
2026-03-05 21:13:55 -08:00
4 changed files with 259 additions and 102 deletions

View file

@ -4077,18 +4077,24 @@ void Application::buildCreatureDisplayLookups() {
// Col 5: HairStyleID
// Col 6: HairColorID
// Col 7: FacialHairID
// Turtle/Vanilla: 19 fields — 10 equip slots (8-17), BakeName=18 (no Flags field)
// WotLK/TBC/Classic: 21 fields — 11 equip slots (8-18), Flags=19, BakeName=20
// CreatureDisplayInfoExtra.dbc field layout depends on actual field count:
// 19 fields: 10 equip slots (8-17), BakeName=18 (no Flags field)
// 21 fields: 11 equip slots (8-18), Flags=19, BakeName=20
if (auto cdie = assetManager->loadDBC("CreatureDisplayInfoExtra.dbc"); cdie && cdie->isLoaded()) {
const auto* cdieL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CreatureDisplayInfoExtra") : nullptr;
const uint32_t cdieEquip0 = cdieL ? (*cdieL)["EquipDisplay0"] : 8;
const uint32_t bakeField = cdieL ? (*cdieL)["BakeName"] : 20;
// Count equipment slots: Vanilla/Turtle has 10, WotLK/TBC has 11
int numEquipSlots = 10;
if (cdieL && cdieL->field("EquipDisplay10") != 0xFFFFFFFF) {
// Detect actual field count to determine equip slot count and BakeName position
const uint32_t dbcFieldCount = cdie->getFieldCount();
int numEquipSlots;
uint32_t bakeField;
if (dbcFieldCount <= 19) {
// 19 fields: 10 equip slots (8-17), BakeName at 18
numEquipSlots = 10;
bakeField = 18;
} else {
// 21 fields: 11 equip slots (8-18), Flags=19, BakeName=20
numEquipSlots = 11;
} else if (!cdieL) {
numEquipSlots = 11; // Default (WotLK) has 11
bakeField = cdieL ? (*cdieL)["BakeName"] : 20;
}
uint32_t withBakeName = 0;
for (uint32_t i = 0; i < cdie->getRecordCount(); i++) {
@ -4107,8 +4113,9 @@ void Application::buildCreatureDisplayLookups() {
if (!extra.bakeName.empty()) withBakeName++;
humanoidExtraMap_[cdie->getUInt32(i, cdieL ? (*cdieL)["ID"] : 0)] = extra;
}
LOG_INFO("Loaded ", humanoidExtraMap_.size(), " humanoid display extra entries (",
withBakeName, " with baked textures, ", numEquipSlots, " equip slots)");
LOG_WARNING("Loaded ", humanoidExtraMap_.size(), " humanoid display extra entries (",
withBakeName, " with baked textures, ", numEquipSlots, " equip slots, ",
dbcFieldCount, " DBC fields, bakeField=", bakeField, ")");
}
// CreatureModelData.dbc: modelId (col 0) → modelPath (col 2, .mdx → .m2)
@ -4508,12 +4515,11 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
LOG_DEBUG(" Found humanoid extra: raceId=", (int)extra.raceId, " sexId=", (int)extra.sexId,
" hairStyle=", (int)extra.hairStyleId, " hairColor=", (int)extra.hairColorId,
" bakeName='", extra.bakeName, "'");
LOG_DEBUG(" Equipment: helm=", extra.equipDisplayId[0], " shoulder=", extra.equipDisplayId[1],
" shirt=", extra.equipDisplayId[2], " chest=", extra.equipDisplayId[3],
" belt=", extra.equipDisplayId[4], " legs=", extra.equipDisplayId[5],
" feet=", extra.equipDisplayId[6], " wrist=", extra.equipDisplayId[7],
" hands=", extra.equipDisplayId[8], " tabard=", extra.equipDisplayId[9],
" cape=", extra.equipDisplayId[10]);
LOG_DEBUG("NPC equip: chest=", extra.equipDisplayId[3],
" legs=", extra.equipDisplayId[5],
" feet=", extra.equipDisplayId[6],
" hands=", extra.equipDisplayId[8],
" bake='", extra.bakeName, "'");
// Build equipment texture region layers from NPC equipment display IDs
// (texture-only compositing — no geoset changes to avoid invisibility bugs)
@ -4574,8 +4580,11 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
}
};
auto regionAllowedForNpcSlotCtx = [&](int eqSlot, int region) -> bool {
// Avoid painting arm regions from shirt/chest when NPC has no arm armor.
if ((eqSlot == 2 || eqSlot == 3) && !npcHasArmArmor) {
// Shirt (slot 2) without arm armor: restrict to torso only
// to avoid bare-skin shirt textures bleeding onto arms.
// Chest (slot 3) always paints arms — plate/mail chest armor
// must cover the full upper body even without separate gloves.
if (eqSlot == 2 && !npcHasArmArmor) {
return (region == 3 || region == 4);
}
return regionAllowedForNpcSlot(eqSlot, region);
@ -4684,35 +4693,26 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
// Use baked texture for body skin (types 1, 2)
// Type 6 (hair) needs its own texture from CharSections.dbc
const bool allowNpcRegionComposite = true;
rendering::VkTexture* bakedSkinTex = nullptr;
if (!extra.bakeName.empty()) {
std::string bakePath = "Textures\\BakedNpcTextures\\" + extra.bakeName;
// Composite equipment textures over baked NPC texture, or just load baked texture
rendering::VkTexture* finalTex = nullptr;
if (allowNpcRegionComposite && !npcRegionLayers.empty()) {
finalTex = charRenderer->compositeWithRegions(bakePath, {}, npcRegionLayers);
LOG_DEBUG("Composited NPC baked texture with ", npcRegionLayers.size(),
" equipment regions: ", bakePath);
} else {
finalTex = charRenderer->loadTexture(bakePath);
}
rendering::VkTexture* finalTex = charRenderer->loadTexture(bakePath);
bakedSkinTex = finalTex;
if (finalTex && modelData) {
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
uint32_t texType = modelData->textures[ti].type;
// Humanoid NPCs typically use creature-skin texture types (11-13).
// Keep type 2 (object skin) untouched so cloak/cape slots do not get face/body textures.
if (texType == 1 || texType == 11 || texType == 12 || texType == 13) {
if (texType == 1) {
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), finalTex);
LOG_DEBUG("Applied baked NPC texture to slot ", ti, " (type ", texType, "): ", bakePath);
hasHumanoidTexture = true;
LOG_DEBUG("NPC baked type1 slot=", ti, " modelId=", modelId,
" tex=", bakePath);
}
}
} else {
LOG_WARNING("Failed to load baked NPC texture: ", bakePath);
}
} else {
LOG_DEBUG(" Humanoid extra has empty bakeName, trying CharSections fallback");
}
// Fallback: if baked texture failed or bakeName was empty, build from CharSections
if (!hasHumanoidTexture) {
LOG_DEBUG(" Trying CharSections fallback for NPC skin");
// Build skin texture from CharSections.dbc (same as player character)
auto csFallbackDbc = assetManager->loadDBC("CharSections.dbc");
@ -4755,6 +4755,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
}
}
LOG_DEBUG("NPC CharSections lookup: race=", npcRace, " sex=", npcSex,
" skin=", npcSkin, " face=", npcFace,
" skinPath='", npcSkinPath, "' faceLower='", npcFaceLower, "'");
if (!npcSkinPath.empty()) {
// Composite skin + face + underwear
std::vector<std::string> skinLayers;
@ -4775,14 +4778,19 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
}
if (npcSkinTex && modelData) {
int slotsSet = 0;
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
uint32_t texType = modelData->textures[ti].type;
if (texType == 1 || texType == 11 || texType == 12 || texType == 13) {
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), npcSkinTex);
hasHumanoidTexture = true;
slotsSet++;
}
}
LOG_DEBUG("Applied CharSections skin to NPC: ", npcSkinPath);
LOG_DEBUG("NPC CharSections: skin='", npcSkinPath, "' regions=",
npcRegionLayers.size(), " applied=", hasHumanoidTexture,
" slots=", slotsSet,
" modelId=", modelId, " texCount=", modelData->textures.size());
}
}
}
@ -4814,15 +4822,24 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
if (!hairTexPath.empty()) {
rendering::VkTexture* hairTex = charRenderer->loadTexture(hairTexPath);
if (hairTex && modelData) {
rendering::VkTexture* whTex = charRenderer->loadTexture("");
if (hairTex && hairTex != whTex && modelData) {
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
if (modelData->textures[ti].type == 6) {
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), hairTex);
LOG_DEBUG("Applied hair texture to slot ", ti, ": ", hairTexPath);
}
}
}
}
// Bald NPCs (hairStyle=0 or no CharSections match): set type-6 to
// the skin/baked texture so the scalp cap renders with skin color.
if (hairTexPath.empty() && bakedSkinTex && modelData) {
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
if (modelData->textures[ti].type == 6) {
charRenderer->setModelTexture(modelId, static_cast<uint32_t>(ti), bakedSkinTex);
}
}
}
}
// Do not apply cape textures at model scope here. Type-2 texture slots are
@ -4959,6 +4976,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
uint32_t tgtSex = static_cast<uint32_t>(extra.sexId);
// Look up hair texture (section 3)
rendering::VkTexture* whiteTex = charRenderer->loadTexture("");
for (uint32_t r = 0; r < charSectionsDbc2->getRecordCount(); r++) {
uint32_t rId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["SexID"] : 2);
@ -4972,7 +4990,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
std::string hairPath = charSectionsDbc2->getString(r, csL ? (*csL)["Texture1"] : 6);
if (!hairPath.empty()) {
rendering::VkTexture* hairTex = charRenderer->loadTexture(hairPath);
if (hairTex) {
if (hairTex && hairTex != whiteTex) {
for (size_t ti = 0; ti < md->textures.size(); ti++) {
if (md->textures[ti].type == 6) {
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(ti), hairTex);
@ -4983,28 +5001,37 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
break;
}
// Look up skin texture (section 0) for per-instance skin color
for (uint32_t r = 0; r < charSectionsDbc2->getRecordCount(); r++) {
uint32_t rId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["SexID"] : 2);
if (rId != tgtRace || sId != tgtSex) continue;
uint32_t sec = charSectionsDbc2->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
if (sec != 0) continue;
uint32_t col = charSectionsDbc2->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
if (col != static_cast<uint32_t>(extra.skinId)) continue;
std::string skinPath = charSectionsDbc2->getString(r, csL ? (*csL)["Texture1"] : 6);
if (!skinPath.empty()) {
rendering::VkTexture* skinTex = charRenderer->loadTexture(skinPath);
if (skinTex) {
for (size_t ti = 0; ti < md->textures.size(); ti++) {
uint32_t tt = md->textures[ti].type;
if (tt == 1 || tt == 11) {
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(ti), skinTex);
// Look up skin texture (section 0) for per-instance skin color.
// Skip when the NPC has a baked texture or composited equipment —
// those already encode armor over skin and must not be replaced.
bool hasEquipOrBake = !extra.bakeName.empty();
if (!hasEquipOrBake) {
for (int s = 0; s < 11 && !hasEquipOrBake; s++)
if (extra.equipDisplayId[s] != 0) hasEquipOrBake = true;
}
if (!hasEquipOrBake) {
for (uint32_t r = 0; r < charSectionsDbc2->getRecordCount(); r++) {
uint32_t rId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
uint32_t sId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["SexID"] : 2);
if (rId != tgtRace || sId != tgtSex) continue;
uint32_t sec = charSectionsDbc2->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
if (sec != 0) continue;
uint32_t col = charSectionsDbc2->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
if (col != static_cast<uint32_t>(extra.skinId)) continue;
std::string skinPath = charSectionsDbc2->getString(r, csL ? (*csL)["Texture1"] : 6);
if (!skinPath.empty()) {
rendering::VkTexture* skinTex = charRenderer->loadTexture(skinPath);
if (skinTex) {
for (size_t ti = 0; ti < md->textures.size(); ti++) {
uint32_t tt = md->textures[ti].type;
if (tt == 1 || tt == 11) {
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(ti), skinTex);
}
}
}
}
break;
}
break;
}
}
}
@ -5111,7 +5138,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
// aggressive and can make NPCs invisible (targetable but not rendered).
// Keep default model geosets for online creatures until this path is made
// data-accurate per display model.
static constexpr bool kEnableNpcHumanoidOverrides = true;
static constexpr bool kEnableNpcHumanoidOverrides = false;
// Set geosets for humanoid NPCs based on CreatureDisplayInfoExtra
if (kEnableNpcHumanoidOverrides &&
@ -5198,44 +5225,42 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
// Equipment slots: 0=helm, 1=shoulder, 2=shirt, 3=chest, 4=belt, 5=legs, 6=feet, 7=wrist, 8=hands, 9=tabard, 10=cape
const uint32_t fGG1 = idiL ? (*idiL)["GeosetGroup1"] : 7;
// Chest (slot 3) → group 5 (torso) + group 8 (sleeves/wristbands)
if (extra.equipDisplayId[3] != 0) {
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[3]);
if (idx >= 0) {
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
if (gg > 0) geosetTorso = pickGeoset(static_cast<uint16_t>(501 + gg), 5);
if (gg > 0) geosetSleeves = pickGeoset(static_cast<uint16_t>(801 + gg), 8);
// Do not derive robe/kilt from chest by default here.
// Some NPC datasets set chest geosets that cause persistent
// apron/robe overlays; prefer explicit legs slot for trousers.
auto readGeosetGroup = [&](int slot, const char* slotName) -> uint32_t {
uint32_t did = extra.equipDisplayId[slot];
if (did == 0) return 0;
int32_t idx = itemDisplayDbc->findRecordById(did);
if (idx < 0) {
LOG_INFO("NPC equip slot ", slotName, " displayId=", did, " NOT FOUND in ItemDisplayInfo.dbc");
return 0;
}
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
LOG_INFO("NPC equip slot ", slotName, " displayId=", did, " GeosetGroup1=", gg, " (field=", fGG1, ")");
return gg;
};
// Chest (slot 3) → group 5 (torso) + group 8 (sleeves/wristbands)
{
uint32_t gg = readGeosetGroup(3, "chest");
if (gg > 0) geosetTorso = pickGeoset(static_cast<uint16_t>(501 + gg), 5);
if (gg > 0) geosetSleeves = pickGeoset(static_cast<uint16_t>(801 + gg), 8);
}
// Legs (slot 5) → group 13 (trousers)
if (extra.equipDisplayId[5] != 0) {
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[5]);
if (idx >= 0) {
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
if (gg > 0) geosetPants = pickGeoset(static_cast<uint16_t>(1301 + gg), 13);
}
{
uint32_t gg = readGeosetGroup(5, "legs");
if (gg > 0) geosetPants = pickGeoset(static_cast<uint16_t>(1301 + gg), 13);
}
// Feet (slot 6) → group 4 (boots/shins)
if (extra.equipDisplayId[6] != 0) {
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[6]);
if (idx >= 0) {
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
if (gg > 0) geosetBoots = pickGeoset(static_cast<uint16_t>(401 + gg), 4);
}
{
uint32_t gg = readGeosetGroup(6, "feet");
if (gg > 0) geosetBoots = pickGeoset(static_cast<uint16_t>(401 + gg), 4);
}
// Hands (slot 8) → group 3 (gloves/forearms)
if (extra.equipDisplayId[8] != 0) {
int32_t idx = itemDisplayDbc->findRecordById(extra.equipDisplayId[8]);
if (idx >= 0) {
uint32_t gg = itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1);
if (gg > 0) geosetGloves = pickGeoset(static_cast<uint16_t>(301 + gg), 3);
}
{
uint32_t gg = readGeosetGroup(8, "hands");
if (gg > 0) geosetGloves = pickGeoset(static_cast<uint16_t>(301 + gg), 3);
}
// Tabard (slot 9) intentionally disabled for now (see geosetTabard TODO above).
@ -5495,6 +5520,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
uint16_t selectedFacial300 = 300;
uint16_t selectedFacial300Alt = 300;
bool wantsFacialHair = false;
uint32_t equipChestGG = 0, equipLegsGG = 0, equipFeetGG = 0;
std::unordered_set<uint16_t> hairScalpGeosetsForRaceSex;
if (itDisplayData != displayDataMap_.end() &&
itDisplayData->second.extraDisplayId != 0) {
@ -5597,6 +5623,20 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
}
}
}
// Read GeosetGroup1 from equipment to drive clothed mesh selection
if (itemDisplayDbc) {
const uint32_t fGG1 = idiL ? (*idiL)["GeosetGroup1"] : 7;
auto readGG = [&](uint32_t did) -> uint32_t {
if (did == 0) return 0;
int32_t idx = itemDisplayDbc->findRecordById(did);
return (idx >= 0) ? itemDisplayDbc->getUInt32(static_cast<uint32_t>(idx), fGG1) : 0;
};
equipChestGG = readGG(itExtra->second.equipDisplayId[3]);
if (equipChestGG == 0) equipChestGG = readGG(itExtra->second.equipDisplayId[2]); // shirt fallback
equipLegsGG = readGG(itExtra->second.equipDisplayId[5]);
equipFeetGG = readGG(itExtra->second.equipDisplayId[6]);
}
}
}
@ -5650,11 +5690,17 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
// Even "bare" variants can produce unwanted looped arm geometry on NPCs.
if (hasGroup4) {
uint16_t forearmSid = pickFromGroup(401, 4);
if (forearmSid != 0) normalizedGeosets.insert(forearmSid);
uint16_t wantBoots = (equipFeetGG > 0) ? static_cast<uint16_t>(400 + equipFeetGG) : 401;
uint16_t bootsSid = pickFromGroup(wantBoots, 4);
if (bootsSid != 0) normalizedGeosets.insert(bootsSid);
}
// Intentionally do not add group 8 (sleeve/wrist accessory meshes).
// Add sleeve/wrist meshes when chest armor calls for them.
if (hasGroup8 && equipChestGG > 0) {
uint16_t wantSleeves = static_cast<uint16_t>(800 + equipChestGG);
uint16_t sleeveSid = pickFromGroup(wantSleeves, 8);
if (sleeveSid != 0) normalizedGeosets.insert(sleeveSid);
}
// Show tabard mesh only when CreatureDisplayInfoExtra equips one.
if (hasGroup12 && hasEquippedTabard) {
@ -5675,9 +5721,10 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
}
}
// Prefer trousers geoset, not robe/kilt overlays.
// Prefer trousers geoset; use covered variant when legs armor exists.
if (hasGroup13) {
uint16_t pantsSid = pickFromGroup(1301, 13);
uint16_t wantPants = (equipLegsGG > 0) ? static_cast<uint16_t>(1300 + equipLegsGG) : 1301;
uint16_t pantsSid = pickFromGroup(wantPants, 13);
if (pantsSid != 0) normalizedGeosets.insert(pantsSid);
}

View file

@ -8809,13 +8809,21 @@ void GameHandler::checkAreaTriggers() {
areaTriggerSuppressFirst_ = false;
}
// Deeprun Tram entrance triggers need extended range because WMO
// collision walls block the player from reaching the trigger center.
static const std::unordered_set<uint32_t> extendedRangeTriggers = {
712, 713, // Stormwind/Ironforge → Deeprun Tram
2166, 2171, // Tram interior exit triggers
};
for (const auto& at : areaTriggers_) {
if (at.mapId != currentMapId_) continue;
const bool extended = extendedRangeTriggers.count(at.id) > 0;
bool inside = false;
if (at.radius > 0.0f) {
// Sphere trigger — small minimum so player must be near the portal
float effectiveRadius = std::max(at.radius, 12.0f);
float effectiveRadius = std::max(at.radius, extended ? 45.0f : 12.0f);
float dx = px - at.x;
float dy = py - at.y;
float dz = pz - at.z;
@ -8823,9 +8831,10 @@ void GameHandler::checkAreaTriggers() {
inside = (distSq <= effectiveRadius * effectiveRadius);
} else if (at.boxLength > 0.0f || at.boxWidth > 0.0f || at.boxHeight > 0.0f) {
// Box trigger — small minimum so player must walk into the portal area
float effLength = std::max(at.boxLength, 16.0f);
float effWidth = std::max(at.boxWidth, 16.0f);
float effHeight = std::max(at.boxHeight, 16.0f);
float boxMin = extended ? 60.0f : 16.0f;
float effLength = std::max(at.boxLength, boxMin);
float effWidth = std::max(at.boxWidth, boxMin);
float effHeight = std::max(at.boxHeight, boxMin);
float dx = px - at.x;
float dy = py - at.y;

View file

@ -753,6 +753,44 @@ static void blitOverlayScaled2x(std::vector<uint8_t>& composite, int compW, int
blitOverlayScaledN(composite, compW, compH, overlay, dstX, dstY, 2);
}
// Nearest-neighbor downscale blit: sample every Nth pixel from overlay
static void blitOverlayDownscaleN(std::vector<uint8_t>& composite, int compW, int compH,
const pipeline::BLPImage& overlay, int dstX, int dstY, int scale) {
if (scale < 2) { blitOverlay(composite, compW, compH, overlay, dstX, dstY); return; }
int outW = overlay.width / scale;
int outH = overlay.height / scale;
for (int oy = 0; oy < outH; oy++) {
int dy = dstY + oy;
if (dy < 0 || dy >= compH) continue;
for (int ox = 0; ox < outW; ox++) {
int dx = dstX + ox;
if (dx < 0 || dx >= compW) continue;
int sx = ox * scale;
int sy = oy * scale;
size_t srcIdx = (static_cast<size_t>(sy) * overlay.width + sx) * 4;
size_t dstIdx = (static_cast<size_t>(dy) * compW + dx) * 4;
uint8_t srcA = overlay.data[srcIdx + 3];
if (srcA == 0) continue;
if (srcA == 255) {
composite[dstIdx + 0] = overlay.data[srcIdx + 0];
composite[dstIdx + 1] = overlay.data[srcIdx + 1];
composite[dstIdx + 2] = overlay.data[srcIdx + 2];
composite[dstIdx + 3] = 255;
} else {
float alpha = srcA / 255.0f;
float invAlpha = 1.0f - alpha;
composite[dstIdx + 0] = static_cast<uint8_t>(overlay.data[srcIdx + 0] * alpha + composite[dstIdx + 0] * invAlpha);
composite[dstIdx + 1] = static_cast<uint8_t>(overlay.data[srcIdx + 1] * alpha + composite[dstIdx + 1] * invAlpha);
composite[dstIdx + 2] = static_cast<uint8_t>(overlay.data[srcIdx + 2] * alpha + composite[dstIdx + 2] * invAlpha);
composite[dstIdx + 3] = std::max(composite[dstIdx + 3], srcA);
}
}
}
}
VkTexture* CharacterRenderer::compositeTextures(const std::vector<std::string>& layerPaths) {
if (layerPaths.empty() || !assetManager || !assetManager->isInitialized()) {
return whiteTexture_.get();
@ -1116,14 +1154,31 @@ VkTexture* CharacterRenderer::compositeWithRegions(const std::string& basePath,
// Expected full-resolution size for this region at current atlas scale
int expectedW = regionSizes256[regionIdx][0] * scaleX;
int expectedH = regionSizes256[regionIdx][1] * scaleY;
if (overlay.width * 2 == expectedW && overlay.height * 2 == expectedH) {
if (overlay.width == expectedW && overlay.height == expectedH) {
// Exact match — blit 1:1
blitOverlay(composite, width, height, overlay, dstX, dstY);
} else if (overlay.width * 2 == expectedW && overlay.height * 2 == expectedH) {
// Overlay is half size — upscale 2x
blitOverlayScaled2x(composite, width, height, overlay, dstX, dstY);
} else if (overlay.width > expectedW && overlay.height > expectedH &&
expectedW > 0 && expectedH > 0) {
// Overlay is larger than region (e.g. HD textures for 1024 atlas on 512 canvas)
// Downscale to fit
int dsX = overlay.width / expectedW;
int dsY = overlay.height / expectedH;
int ds = std::min(dsX, dsY);
if (ds >= 2) {
blitOverlayDownscaleN(composite, width, height, overlay, dstX, dstY, ds);
} else {
blitOverlay(composite, width, height, overlay, dstX, dstY);
}
} else {
blitOverlay(composite, width, height, overlay, dstX, dstY);
}
core::Logger::getInstance().debug("compositeWithRegions: region ", regionIdx,
" at (", dstX, ",", dstY, ") ", overlay.width, "x", overlay.height, " from ", rl.second);
core::Logger::getInstance().warning("compositeWithRegions: region ", regionIdx,
" at (", dstX, ",", dstY, ") overlay=", overlay.width, "x", overlay.height,
" expected=", expectedW, "x", expectedH, " from ", rl.second);
}
// Upload to GPU via VkTexture

View file

@ -883,20 +883,40 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu
surface.origin.z = adjustedZ;
surface.position.z = adjustedZ;
if (surface.origin.z > 300.0f || surface.origin.z < -100.0f) return;
// Build tile mask from MLIQ flags — tiles with (flag & 0x0F) == 0x0F have no liquid
// Build tile mask from MLIQ flags and per-vertex heights
size_t tileCount = static_cast<size_t>(surface.width) * static_cast<size_t>(surface.height);
size_t maskBytes = (tileCount + 7) / 8;
surface.mask.assign(maskBytes, 0x00);
const float baseZ = liquid.basePosition.z;
const bool hasHeights = !liquid.heights.empty() &&
liquid.heights.size() >= static_cast<size_t>(vertexCount);
for (size_t t = 0; t < tileCount; t++) {
bool hasLiquid = true;
int tx = static_cast<int>(t) % surface.width;
int ty = static_cast<int>(t) / surface.width;
// Standard WoW check: low nibble 0x0F = "don't render"
if (t < liquid.flags.size()) {
if ((liquid.flags[t] & 0x0F) == 0x0F) {
hasLiquid = false;
}
}
// Suppress water tiles that extend into enclosed WMO areas
// (e.g. Stormwind barracks stairway where canal water pokes through)
// Render coords: x=wowY(west), y=wowX(north)
if (hasLiquid) {
glm::vec3 tileWorld = surface.origin +
surface.stepX * (static_cast<float>(tx) + 0.5f) +
surface.stepY * (static_cast<float>(ty) + 0.5f);
// Stormwind Barracks / Stockade stairway:
// Stockade entrance at approximately render (-8768, 848)
if (tileWorld.x > -8790.0f && tileWorld.x < -8735.0f &&
tileWorld.y > 828.0f && tileWorld.y < 878.0f) {
hasLiquid = false;
}
}
if (hasLiquid) {
size_t byteIdx = t / 8;
size_t bitIdx = t % 8;
@ -905,6 +925,32 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu
}
createWaterMesh(surface);
// Count how many tiles passed the flag check and compute bounds
size_t activeTiles = 0;
float minWX = 1e9f, maxWX = -1e9f, minWY = 1e9f, maxWY = -1e9f;
for (size_t t = 0; t < tileCount; t++) {
size_t byteIdx = t / 8;
size_t bitIdx = t % 8;
if (surface.mask[byteIdx] & (1 << bitIdx)) {
activeTiles++;
int atx = static_cast<int>(t) % surface.width;
int aty = static_cast<int>(t) / surface.width;
glm::vec3 tw = surface.origin +
surface.stepX * (static_cast<float>(atx) + 0.5f) +
surface.stepY * (static_cast<float>(aty) + 0.5f);
if (tw.x < minWX) minWX = tw.x;
if (tw.x > maxWX) maxWX = tw.x;
if (tw.y < minWY) minWY = tw.y;
if (tw.y > maxWY) maxWY = tw.y;
}
}
LOG_DEBUG("WMO water: origin=(", surface.origin.x, ",", surface.origin.y, ",", surface.origin.z,
") tiles=", (int)surface.width, "x", (int)surface.height,
" active=", activeTiles, "/", tileCount,
" wmoId=", wmoId, " indexCount=", surface.indexCount,
" bounds x=[", minWX, "..", maxWX, "] y=[", minWY, "..", maxWY, "]");
if (surface.indexCount > 0) {
if (vkCtx) updateMaterialUBO(surface);
surfaces.push_back(std::move(surface));