add los table loading

This commit is contained in:
TakaRikka
2026-06-09 23:50:47 -07:00
parent e345323f0a
commit 7f2dc7902c
11 changed files with 361 additions and 4 deletions
+2
View File
@@ -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
+5
View File
@@ -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
+62 -2
View File
@@ -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());
+7
View File
@@ -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;
+7
View File
@@ -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, &current.pos, &shape_angle, NULL);
dComIfGp_particle_set(0x8C74, &current.pos, &shape_angle, NULL);
+15 -1
View File
@@ -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)) {
+11
View File
@@ -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()) {
+102 -1
View File
@@ -26,6 +26,8 @@
#include "dusk/string.hpp"
#if TARGET_PC
#include <fmt/ranges.h>
#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++;
}
+2
View File
@@ -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());
}
+119
View File
@@ -0,0 +1,119 @@
#include "LosTable.hpp"
#include <fstream>
#include <vector>
#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<u8> g_data;
u32 g_count = 0;
const LosEntry* entries() {
return reinterpret_cast<const LosEntry*>(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<u8> data((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
if (data.size() < sizeof(LosHeader)) {
LosLog.warn("los.bin too small: {} bytes", data.size());
return;
}
u32 count = reinterpret_cast<const LosHeader*>(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
+29
View File
@@ -0,0 +1,29 @@
#ifndef DUSK_TPHD_LOS_TABLE_HPP
#define DUSK_TPHD_LOS_TABLE_HPP
#include <filesystem>
#include <dolphin/types.h>
namespace dusk::tphd {
// Loads `<contentPath>/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