From 7f2dc7902c1175916bcec9d602bf43bed92c6a6d Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Tue, 9 Jun 2026 23:50:47 -0700 Subject: [PATCH] add los table loading --- files.cmake | 2 + include/d/d_stage.h | 5 ++ src/d/actor/d_a_bg.cpp | 64 +++++++++++++- src/d/actor/d_a_kytag10.cpp | 7 ++ src/d/actor/d_a_obj_sWallShutter.cpp | 7 ++ src/d/actor/d_a_swc00.cpp | 16 +++- src/d/d_particle.cpp | 11 +++ src/d/d_stage.cpp | 103 ++++++++++++++++++++++- src/dusk/tphd/HdAssetLayer.cpp | 2 + src/dusk/tphd/LosTable.cpp | 119 +++++++++++++++++++++++++++ src/dusk/tphd/LosTable.hpp | 29 +++++++ 11 files changed, 361 insertions(+), 4 deletions(-) create mode 100644 src/dusk/tphd/LosTable.cpp create mode 100644 src/dusk/tphd/LosTable.hpp diff --git a/files.cmake b/files.cmake index 23bb153041..9704e13c5b 100644 --- a/files.cmake +++ b/files.cmake @@ -1538,6 +1538,8 @@ set(DUSK_FILES src/dusk/tphd/AddrLib.cpp src/dusk/tphd/HdAssetLayer.hpp src/dusk/tphd/HdAssetLayer.cpp + src/dusk/tphd/LosTable.hpp + src/dusk/tphd/LosTable.cpp ) set(DUSK_HTTP_BACKEND_FILES diff --git a/include/d/d_stage.h b/include/d/d_stage.h index 04a6115fd0..cc86dfa3eb 100644 --- a/include/d/d_stage.h +++ b/include/d/d_stage.h @@ -1416,6 +1416,11 @@ dStage_KeepDoorInfo* dStage_GetKeepDoorInfo(); dStage_KeepDoorInfo* dStage_GetRoomKeepDoorInfo(); void dStage_dt_c_fieldMapLoader(void* i_data, dStage_dt_c* i_stage); +#if TARGET_PC +// TP HD Cave of Shadows (D_SB11): reveal the los next-floor when a descent gate opens. +void dStage_showLOSNextFloor(int fromRoom); +#endif + #if DEBUG void dStage_DebugDisp(); #endif diff --git a/src/d/actor/d_a_bg.cpp b/src/d/actor/d_a_bg.cpp index 110e2aef15..35e5c82fa8 100644 --- a/src/d/actor/d_a_bg.cpp +++ b/src/d/actor/d_a_bg.cpp @@ -13,6 +13,7 @@ #include "d/d_bg_parts.h" #include "m_Do/m_Do_lib.h" #include "d/d_demo.h" +#include "dusk/tphd/LosTable.hpp" #include "JSystem/JKernel/JKRExpHeap.h" #include "JSystem/JKernel/JKRSolidHeap.h" #include "JSystem/J3DGraphAnimator/J3DMaterialAnm.h" @@ -297,6 +298,25 @@ int daBg_c::draw() { dComIfGd_setListBG(); mDoLib_clipper::changeFar(1000000.0f); +#if TARGET_PC + bool losClip = false; + Mtx losBgMtx; + if (dusk::tphd_active()) { + // TPHD Cave of Shadows rooms have a base matrix far from identity; it gets the room translation from 'los.bin' + // HD daBg::draw clips the shape bbox in world space, so recompute the room base matrix to transform it. + if (strcmp(dComIfGp_getStartStageName(), "D_SB11") == 0) { + f32 hx, hy, hz; + s16 ha; + if (dusk::tphd::los_get_room_trans(roomNo, &hx, &hy, &hz, &ha)) { + mDoMtx_stack_c::transS(hx, hy, hz); + mDoMtx_stack_c::YrotM(ha); + mDoMtx_copy(mDoMtx_stack_c::get(), losBgMtx); + losClip = true; + } + } + } +#endif + J3DModelData* modelData; for (int i = 0; i < 6; i++) { sp8 = 0; @@ -325,8 +345,22 @@ int daBg_c::draw() { for (u16 j = 0; j < modelData->getShapeNum(); j++) { J3DShape* shape = modelData->getShapeNodePointer(j); - if (mDoLib_clipper::clip(j3dSys.getViewMtx(), (Vec*)shape->getMin(), - (Vec*)shape->getMax())) { + Vec* clipMin = (Vec*)shape->getMin(); + Vec* clipMax = (Vec*)shape->getMax(); +#if TARGET_PC + if (dusk::tphd_active()) { + Vec losClipMin, losClipMax; + if (losClip) { + // HD transforms the bbox min/max by the room base matrix; clip rebuilds + // the 8 corners (exact for the room angles, which are multiples of 90). + mDoMtx_multVec(losBgMtx, clipMin, &losClipMin); + mDoMtx_multVec(losBgMtx, clipMax, &losClipMax); + clipMin = &losClipMin; + clipMax = &losClipMax; + } + } +#endif + if (mDoLib_clipper::clip(j3dSys.getViewMtx(), clipMin, clipMax)) { shape->hide(); } else { shape->show(); @@ -565,17 +599,43 @@ int daBg_c::create() { } J3DModelData* modelData; +#if TARGET_PC + f32 transX = 0.0f; + f32 transY = 0.0f; + f32 transVert = 0.0f; + s16 angle = 0; + bool foundMapTrans = false; + + if (dusk::tphd_active()) { + // TPHD positions Cave of Shadows rooms via los.bin (per-room world X/Y/Z + + // Y-rotation, incl. the vertical Y that GC's MULT lacks). Retail gates + // this on g_dComIfG_gameInfo.field_0x1e448; los.bin only carries + // D_SB11's room data, so restrict it to that stage and fall back to MULT. + if (strcmp(dComIfGp_getStartStageName(), "D_SB11") == 0) { + foundMapTrans = dusk::tphd::los_get_room_trans(roomNo, &transX, &transVert, &transY, &angle); + } + } + + if (!foundMapTrans) + foundMapTrans = dComIfGp_getMapTrans(roomNo, &transX, &transY, &angle); + if (foundMapTrans) { +#else f32 transX; f32 transY; s16 angle; if (dComIfGp_getMapTrans(roomNo, &transX, &transY, &angle)) { +#endif daBg_Part* bgPart = mBgParts; J3DModel* model; for (int i = 0; i < 6; i++) { model = bgPart->model; if (model != NULL) { + #if TARGET_PC + mDoMtx_stack_c::transS(transX, transVert, transY); + #else mDoMtx_stack_c::transS(transX, 0.0f, transY); + #endif mDoMtx_stack_c::YrotM(angle); model->setBaseTRMtx(mDoMtx_stack_c::get()); diff --git a/src/d/actor/d_a_kytag10.cpp b/src/d/actor/d_a_kytag10.cpp index 27c02c2df0..53df1cdd67 100644 --- a/src/d/actor/d_a_kytag10.cpp +++ b/src/d/actor/d_a_kytag10.cpp @@ -64,6 +64,13 @@ static dPath* get_Extent_pos_end_get(kytag10_class* i_this, dPath* i_path, cXyz* } static void sparks_move(kytag10_class* i_this) { +#if TARGET_PC + // Emitters NULL when no scene particle bank loaded (e.g. TP HD D_SB11). + if (i_this->mpEmitter1 == NULL || i_this->mpEmitter2 == NULL) { + return; + } +#endif + camera_process_class* camera_p = dComIfGp_getCamera(0); cXyz ratio_pos_1; diff --git a/src/d/actor/d_a_obj_sWallShutter.cpp b/src/d/actor/d_a_obj_sWallShutter.cpp index 65b07efd30..341a1ef029 100644 --- a/src/d/actor/d_a_obj_sWallShutter.cpp +++ b/src/d/actor/d_a_obj_sWallShutter.cpp @@ -208,6 +208,13 @@ void daSwShutter_c::init_modeMoveDownInit() { mMaxAtten = l_HIO.mMaxAtten; mMinAtten = l_HIO.mMinAtten; +#if TARGET_PC + if (dusk::tphd_active()) { + // TPHD: opening a Cave-of-Shadows shutter gate reveals the floor below + dStage_showLOSNextFloor(fopAcM_GetRoomNo(this)); + } +#endif + if (mModelType == TYPE_SUBDAN_e) { dComIfGp_particle_set(0x8C73, ¤t.pos, &shape_angle, NULL); dComIfGp_particle_set(0x8C74, ¤t.pos, &shape_angle, NULL); diff --git a/src/d/actor/d_a_swc00.cpp b/src/d/actor/d_a_swc00.cpp index cb7d48d05b..3acdf9e6a9 100644 --- a/src/d/actor/d_a_swc00.cpp +++ b/src/d/actor/d_a_swc00.cpp @@ -126,6 +126,10 @@ int daSwc00_c::execute() { case 1: case 2: case 7: +#if TARGET_PC + case 10: // HD Cave of Shadows region switch + case 11: +#endif case 15: if (sw2 != 0xff && !fopAcM_isSwitch(this, sw2)) { return 1; @@ -179,7 +183,17 @@ int daSwc00_c::execute() { field_0x584 = 1; } break; - +#if TARGET_PC + // HD Cave of Shadows region switch (CoS-controller bookkeeping omitted; not in this port) + case 10: + case 11: + if (hitCheck(this)) { + dComIfGs_onSwitch(sw1, fopAcM_GetRoomNo(this)); + field_0x583 = 1; + field_0x584 = 1; + } + break; +#endif case 7: case 8: if (hitCheck(this)) { diff --git a/src/d/d_particle.cpp b/src/d/d_particle.cpp index c3f4e9067f..17707bd310 100644 --- a/src/d/d_particle.cpp +++ b/src/d/d_particle.cpp @@ -1736,6 +1736,17 @@ u32 dPa_control_c::set(u32 param_0, u8 param_1, u16 param_2, cXyz const* pos, level_c::emitter_c* this_00 = field_0x210.get(param_0); u8 uVar7 = getRM_ID(param_2); JPAResourceManager* this_01 = mEmitterMng->getResourceManager(uVar7); + +#if TARGET_PC + // A scene particle (id & 0x8000) was requested but this stage has no scene resource + // manager: its STAG mParticleNo is 0xFF, so readScene/createRoomScene never ran (the scene + // bank is bank 1). D_SB11 is such a stage. Returning 0 (do not emit) is the correct response + // to a missing bank — emitting would deref a NULL JPAResourceManager. + if (this_01 == NULL) { + return 0; + } +#endif + u32 uVar3 = this_01->getResUserWork(param_2); if (this_00 != NULL) { if (param_2 == this_00->getNameId()) { diff --git a/src/d/d_stage.cpp b/src/d/d_stage.cpp index 3b5f6863ec..fdd47cf38c 100644 --- a/src/d/d_stage.cpp +++ b/src/d/d_stage.cpp @@ -26,6 +26,8 @@ #include "dusk/string.hpp" #if TARGET_PC #include +#include "dusk/tphd/LosTable.hpp" +#include "os_report.h" #endif void dStage_nextStage_c::set(const char* i_stage, s8 i_roomId, s16 i_point, s8 i_layer, s8 i_wipe, @@ -311,7 +313,7 @@ int dStage_roomControl_c::loadRoom(int roomCount, u8* rooms, bool param_2) { return 0; } } - + BOOL r26 = TRUE; for (int roomNo = 0; roomNo < ARRAY_SIZE(mStatus); roomNo++) { if (dStage_roomControl_c::checkStatusFlag(roomNo, 0x01)) { @@ -2093,6 +2095,92 @@ static int dStage_doorInfoInit(dStage_dt_c* i_stage, void* i_data, int entryNum, return 1; } +#if TARGET_PC +// D_SB11 (Cave of Shadows, HD): true when the los table is loaded and we are in D_SB11. +// Mirrors HD's `g_dComIfG_gameInfo.field4_0x1e448 != 0` gate (set once in phase_1 for D_SB11). +static bool isLOSStage() { + return dusk::tphd::los_loaded() && + std::strcmp(dComIfGp_getStartStageName(), "D_SB11") == 0; +} + +// Mirrors HD FUN_02ab7e94 (los override): the file RTBL is a linear placeholder chain +// (r19 -> [19,18,20]); rewrite each room's m_rooms in place from los.bin floor links. +// next gets NO 0x80 (floor not yet unlocked) so it is not bg-loaded at stage-create; +// RoomCheck streams it in after the player exists. m_rooms arrays are 3 bytes each +// (verified: contiguous, gap=3), so writing up to 3 bytes in place is safe. +static void dStage_LOSRoomReadOverride(roomRead_class* p_node) { + OFFSET_PTR(roomRead_data_class)* rtbl = p_node->m_entries; + + for (int roomNo = 0; roomNo < p_node->num; roomNo++) { + int nextFloorNo = dusk::tphd::los_next_floor(roomNo); + int prevFloorNo = dusk::tphd::los_prev_floor(roomNo); + u8* roomData = rtbl[roomNo]->m_rooms; + int n = 1; + + roomData[0] = (u8)((roomNo & 0x3f) | (roomData[0] & 0xc0)); + if (nextFloorNo > -1) { + // nextFloorNo is bg-loaded only once its floor-unlock switch (roomNo + 0x80) is set + u8 nextBg = dComIfGs_isSwitch(roomNo + 0x80, roomNo) ? 0x80 : 0; + roomData[1] = (u8)((nextFloorNo & 0x3f) | nextBg); + n = 2; + } + + if (prevFloorNo > -1) { + roomData[n] = (u8)((prevFloorNo & 0x3f) | 0x80); + n++; + } + + if (nextFloorNo < 0 && prevFloorNo > -1) { + int prevPrevFloorNo = dusk::tphd::los_prev_floor(prevFloorNo); + if (prevPrevFloorNo > -1) { + roomData[n] = (u8)((prevPrevFloorNo & 0x3f) | 0x80); + n++; + } + } + + rtbl[roomNo]->num = (u8)n; + OSReport("[SB11] override r%-2d nextFloorNo=%d prevFloorNo=%d -> num=%d [%02x %02x %02x]\n", + roomNo, nextFloorNo, prevFloorNo, n, roomData[0], n > 1 ? roomData[1] : 0, n > 2 ? roomData[2] : 0); + } +} + +#endif + +#if TARGET_PC +// Mirrors HD FUN_028290d0's reveal (called by the Cave-of-Shadows gate sWallShutter when it +// opens): mark `fromRoom`'s los next-floor as bg (0x80) in fromRoom's m_rooms and clear that +// floor's 0x08 status, so its daBg mesh gets created. Without 0x80 the floor streams in but the +// mesh stays hidden (objectSetCheck skips fopAcM_create(BG) while status flag 0x08 is set). +// No-op outside los stages. This replaces the earlier per-frame RoomCheck reveal (which churned +// room loads). HD reveals the next floor at the moment you open the descent gate, not per-frame. +void dStage_showLOSNextFloor(int fromRoom) { + if (!isLOSStage()) { + return; + } + + roomRead_class* room = dComIfGp_getStageRoom(); + if (room == NULL || fromRoom < 0 || fromRoom >= room->num) { + return; + } + + int nextFloorNo = dusk::tphd::los_next_floor(fromRoom); + if (nextFloorNo < 0) { + return; + } + + roomRead_data_class* e = room->m_entries[fromRoom]; + u8* roomData = e->m_rooms; + for (int j = 0; j < e->num; j++) { + if ((roomData[j] & 0x3f) == nextFloorNo) { + roomData[j] = (u8)(roomData[j] | 0x80); + } + } + + dComIfGp_roomControl_offStatusFlag(nextFloorNo, 0x08); + OSReport("[SB11] gate reveal: from r%d -> next floor r%d\n", fromRoom, nextFloorNo); +} +#endif + static int dStage_roomReadInit(dStage_dt_c* i_stage, void* i_data, int param_2, void* param_3) { UNUSED(param_2); roomRead_class* p_node = (roomRead_class*)((int*)i_data + 1); @@ -2112,6 +2200,12 @@ static int dStage_roomReadInit(dStage_dt_c* i_stage, void* i_data, int param_2, #endif } +#if TARGET_PC + if (dusk::tphd_active() && isLOSStage()) { + dStage_LOSRoomReadOverride(p_node); + } +#endif + return 1; } @@ -2300,6 +2394,13 @@ static int dStage_mecoInfoInit(dStage_dt_c* i_stage, void* i_data, int param_2, dStage_MemoryConfig_data* entry_p = pd->field_0x4; for (int i = 0; i < pd->m_num; i++) { +#if TARGET_PC + // los stages (D_SB11): the file MEC0 (roomNo%3) collides for los-adjacent floors; + // HD re-derives the block from the los floor index instead (FUN_02ab8910). + if (dusk::tphd_active() && isLOSStage()) { + entry_p->m_blockID = (u8)(dusk::tphd::los_floor_index(entry_p->m_roomNo) % 3); + } +#endif dStage_roomControl_c::setMemoryBlockID(entry_p->m_roomNo, entry_p->m_blockID); entry_p++; } diff --git a/src/dusk/tphd/HdAssetLayer.cpp b/src/dusk/tphd/HdAssetLayer.cpp index d0247ff897..afadf121b7 100644 --- a/src/dusk/tphd/HdAssetLayer.cpp +++ b/src/dusk/tphd/HdAssetLayer.cpp @@ -28,6 +28,7 @@ #include "dusk/logging.h" #include "AddrLib.hpp" #include "GtxParser.hpp" +#include "LosTable.hpp" #include "TphdPack.hpp" #include "tracy/Tracy.hpp" @@ -910,6 +911,7 @@ void set_hd_content_path(std::filesystem::path contentPath) { g_entryNumToOverlay().clear(); g_arcRanges().clear(); rebuild_hd_overlay_locked(); + load_los_table(g_contentPath); HdLog.info("HD content path set to: {}", g_contentPath.empty() ? "(disabled)" : g_contentPath.string()); } diff --git a/src/dusk/tphd/LosTable.cpp b/src/dusk/tphd/LosTable.cpp new file mode 100644 index 0000000000..2ed6855a31 --- /dev/null +++ b/src/dusk/tphd/LosTable.cpp @@ -0,0 +1,119 @@ +#include "LosTable.hpp" + +#include +#include + +#include "aurora/lib/logging.hpp" +#include "dusk/endian.h" + +static aurora::Module LosLog("dusk::tphd::los"); + +namespace dusk::tphd { + +namespace { + +struct LosHeader { + /* 0x00 */ BE(u32) count; + /* 0x04 */ BE(u32) unk04; + /* 0x08 */ BE(u32) unk08; +}; + +struct LosEntry { + /* 0x00 */ BE(u32) roomNo; + /* 0x04 */ BE(u32) id1; + /* 0x08 */ BE(u32) id2; + /* 0x0C */ BE(u32) id3; + /* 0x10 */ BE(f32) x; + /* 0x14 */ BE(f32) y; + /* 0x18 */ BE(f32) z; + /* 0x1C */ BE(s16) unk1C; + /* 0x1E */ BE(s16) angleY; +}; + +static_assert(sizeof(LosHeader) == 0x0C); +static_assert(sizeof(LosEntry) == 0x20); + +std::vector g_data; +u32 g_count = 0; + +const LosEntry* entries() { + return reinterpret_cast(g_data.data() + sizeof(LosHeader)); +} + +} // namespace + +void load_los_table(const std::filesystem::path& contentPath) { + g_data.clear(); + g_count = 0; + if (contentPath.empty()) { + return; + } + + const std::filesystem::path losPath = contentPath / "los.bin"; + std::ifstream in(losPath, std::ios::binary); + if (!in) { + LosLog.info("no los.bin at {}", losPath.string()); + return; + } + + std::vector data((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + if (data.size() < sizeof(LosHeader)) { + LosLog.warn("los.bin too small: {} bytes", data.size()); + return; + } + + u32 count = reinterpret_cast(data.data())->count; + if (sizeof(LosHeader) + size_t(count) * sizeof(LosEntry) > data.size()) { + LosLog.warn("los.bin truncated: count={} size={}", count, data.size()); + return; + } + + g_data = std::move(data); + g_count = count; + LosLog.info("loaded los.bin: {} room transforms from {}", count, losPath.string()); +} + +// Mirrors HD FUN_02ababbc: +bool los_get_room_trans(int roomNo, f32* o_x, f32* o_y, f32* o_z, s16* o_angle) { + if (g_count == 0 || roomNo < 0 || u32(roomNo) >= g_count) { + return false; + } + const LosEntry& src = entries()[roomNo]; + *o_x = src.x; + *o_y = src.y; + *o_z = src.z; + *o_angle = src.angleY; + return true; +} + +bool los_loaded() { + return g_count != 0; +} + +int los_room_count() { + return int(g_count); +} + +int los_next_floor(int roomNo) { + if (g_count == 0 || roomNo < 0 || u32(roomNo) >= g_count) { + return -1; + } + return s32(u32(entries()[roomNo].id1)); +} + +int los_prev_floor(int roomNo) { + if (g_count == 0 || roomNo < 0 || u32(roomNo) >= g_count) { + return -1; + } + return s32(u32(entries()[roomNo].id2)); +} + +int los_floor_index(int roomNo) { + if (g_count == 0 || roomNo < 0 || u32(roomNo) >= g_count) { + return 0; + } + return s32(u32(entries()[roomNo].id3)); +} + +} // namespace dusk::tphd diff --git a/src/dusk/tphd/LosTable.hpp b/src/dusk/tphd/LosTable.hpp new file mode 100644 index 0000000000..b269f5e761 --- /dev/null +++ b/src/dusk/tphd/LosTable.hpp @@ -0,0 +1,29 @@ +#ifndef DUSK_TPHD_LOS_TABLE_HPP +#define DUSK_TPHD_LOS_TABLE_HPP + +#include + +#include + +namespace dusk::tphd { + +// Loads `/los.bin` — the TP HD per-room transform table +void load_los_table(const std::filesystem::path& contentPath); + +// HD room map transform (mirrors HD `getMapTrans`/FUN_02905328 ) which +// fills world X/Y/Z translation and Y-rotation for `roomNo` from los.bin. +bool los_get_room_trans(int roomNo, f32* o_x, f32* o_y, f32* o_z, s16* o_angle); + +bool los_loaded(); + +// Number of room entries in the los table +int los_room_count(); + +int los_next_floor(int roomNo); +int los_prev_floor(int roomNo); + +int los_floor_index(int roomNo); + +} + +#endif