jak-project/decompiler/data/StrFileReader.cpp

281 lines
8.4 KiB
C++
Raw Normal View History

/*!
* @file StrFileReader.cpp
* Utility class to read a .STR file and extract the full file name.
*/
#include "StrFileReader.h"
#include <cstring>
#include "common/util/Assert.h"
#include "common/util/FileUtil.h"
#include "game/common/overlord_common.h"
#include "game/common/str_rpc_types.h"
#include "fmt/format.h"
namespace decompiler {
StrFileReader::StrFileReader(const fs::path& file_path, GameVersion version) : m_version(version) {
switch (version) {
case GameVersion::Jak1:
init_jak1(file_path);
break;
case GameVersion::Jak2:
decomp3: more engine stuff, detect non-virtual state inheritance (#3377) - `speech` - `ambient` - `water-h` - `vol-h` - `generic-obs` - `carry-h` - `pilot-h` - `board-h` - `gun-h` - `flut-h` - `indax-h` - `lightjak-h` - `darkjak-h` - `target-util` - `history` - `collide-reaction-target` - `logic-target` - `sidekick` - `projectile` - `voicebox` - `ragdoll-edit` - most of `ragdoll` (not added to gsrc yet) - `curves` - `find-nearest` - `lightjak-wings` - `target-handler` - `target-anim` - `target` - `target2` - `target-swim` - `target-lightjak` - `target-invisible` - `target-death` - `target-gun` - `gun-util` - `board-util` - `target-board` - `board-states` - `mech-h` - `vol` - `vent` - `viewer` - `gem-pool` - `collectables` - `crates` - `secrets-menu` Additionally: - Detection of non-virtual state inheritance - Added a config file that allows overriding the process stack size set by `stack-size-set!` calls - Fix for integer multiplication with `r0` - Fixed detection for the following macros: - `static-attack-info` - `defpart` and `defpartgroup` (probably still needs adjustments, uses Jak 2 implementation at the moment) - `sound-play` (Jak 3 seems to always call `sound-play-by-name` with a `sound-group` of 0, so the macro has been temporarily defaulted to use that) One somewhat significant change made here that should be noted is that the return type of `process::init-from-entity!` was changed to `object`. I've been thinking about this for a while, since it looks a bit nicer without the `(none)` at the end and I have recently encountered init methods that early return `0`.
2024-03-03 15:15:27 -05:00
case GameVersion::Jak3:
init_jak2(file_path);
break;
default:
throw std::runtime_error("[StrFileReader] NYI game version");
}
}
void StrFileReader::init_jak1(const fs::path& file_path) {
auto data = file_util::read_binary_file(file_path);
ASSERT(data.size() >= SECTOR_SIZE); // must have at least the header sector
ASSERT(data.size() % SECTOR_SIZE == 0); // should be multiple of the sector size.
int end_sector = int(data.size()) / SECTOR_SIZE;
auto* header = (StrFileHeaderSector*)data.data();
bool got_zero = false;
for (int i = 0; i < SECTOR_TABLE_SIZE; i++) {
// the chunk is from sector to next_sector
int sector = header->sectors[i];
// assume this chunk continues to the end...
int next_sector = end_sector;
// unless there's another chunk.
if (i + 1 < SECTOR_TABLE_SIZE && header->sectors[i + 1]) {
next_sector = header->sectors[i + 1];
}
if (sector) {
ASSERT(!got_zero); // shouldn't have a non-zero after a zero!
ASSERT(next_sector > sector); // should have a positive size.
ASSERT(next_sector * SECTOR_SIZE <= int(data.size())); // check for overflowing the file
// get chunk data.
std::vector<u8> chunk;
chunk.insert(chunk.end(), data.begin() + sector * SECTOR_SIZE,
data.begin() + next_sector * SECTOR_SIZE);
m_chunks.emplace_back(std::move(chunk));
} else {
got_zero = true;
}
}
// check our sizes are accurate. Will make sure that we include all data, as our m_chunks
// are sized assuming they are packed in order and dense (sectors);
for (int i = 0; i < SECTOR_TABLE_SIZE; i++) {
if (header->sectors[i]) {
ASSERT(header->sizes[i] == m_chunks.at(i).size());
} else {
ASSERT(header->sizes[i] == 0);
}
}
// check nothing stored in the padding.
for (auto x : header->pad) {
ASSERT(x == 0);
}
}
void StrFileReader::init_jak2(const fs::path& file_path) {
auto data = file_util::read_binary_file(file_path);
ASSERT(data.size() >= SECTOR_SIZE); // must have at least the header sector
ASSERT(data.size() % SECTOR_SIZE == 0); // should be multiple of the sector size.
int end_sector = int(data.size()) / SECTOR_SIZE;
auto* header = (StrFileHeaderJ2*)data.data();
bool got_zero = false;
for (int i = 0; i < SECTOR_TABLE_SIZE_J2; i++) {
// the chunk is from sector to next_sector
int sector = header->sectors[i];
// assume this chunk continues to the end...
int next_sector = end_sector;
// unless there's another chunk.
if (i + 1 < SECTOR_TABLE_SIZE_J2 && header->sectors[i + 1]) {
next_sector = header->sectors[i + 1];
}
if (sector) {
ASSERT(!got_zero); // shouldn't have a non-zero after a zero!
ASSERT(next_sector > sector); // should have a positive size.
ASSERT(next_sector * SECTOR_SIZE <= int(data.size())); // check for overflowing the file
// get chunk data.
std::vector<u8> chunk;
chunk.insert(chunk.end(), data.begin() + sector * SECTOR_SIZE,
data.begin() + next_sector * SECTOR_SIZE);
m_chunks.emplace_back(std::move(chunk));
} else {
got_zero = true;
}
}
// check our sizes are accurate. Will make sure that we include all data, as our m_chunks
// are sized assuming they are packed in order and dense (sectors);
for (int i = 0; i < SECTOR_TABLE_SIZE_J2; i++) {
if (header->sectors[i]) {
ASSERT(header->sizes[i] == m_chunks.at(i).size());
} else {
ASSERT(header->sizes[i] == 0);
}
}
}
int StrFileReader::chunk_count() const {
return m_chunks.size();
}
const std::vector<u8>& StrFileReader::get_chunk(int idx) const {
return m_chunks.at(idx);
}
namespace {
bool find_string_in_data(const u8* data, int data_size, const std::string& str, int* result) {
for (int i = 0; i < data_size - int(str.length()); i++) {
if (std::memcmp(data + i, str.c_str(), str.length()) == 0) {
*result = i;
return true;
}
}
return false;
}
std::string get_string_of_max_length(const char* data, int max_length) {
std::string result;
for (int i = 0; i < max_length; i++) {
if (data[i]) {
result.push_back(data[i]);
} else {
return result;
}
}
ASSERT(false);
return "";
}
struct FullName {
std::string name;
int chunk_idx = -1;
};
FullName extract_name(const std::string& file_info_name) {
FullName name;
name.name = file_info_name;
ASSERT(name.name.length() > 10);
ASSERT(name.name.substr(name.name.length() - 6, 6) == "-ag.go");
name.name = name.name.substr(0, name.name.length() - 6);
int chunk_id = 0;
int place = 0;
for (int i = 2; i-- > 0;) {
char c = name.name.back();
if (c >= '0' && c <= '9') {
int val = (c - '0');
for (int j = 0; j < place; j++) {
val *= 10;
}
chunk_id += val;
name.name.pop_back();
place++;
} else {
break;
}
}
ASSERT(name.name.back() == '+');
name.name.pop_back();
name.chunk_idx = chunk_id;
return name;
}
} // namespace
std::string StrFileReader::get_chunk_art_name(int idx) const {
const auto& file_info_string = get_art_group_file_info_string();
const auto& chunk = m_chunks.at(idx);
int offset;
if (find_string_in_data(chunk.data(), int(chunk.size()), file_info_string, &offset)) {
offset += file_info_string.length();
} else {
ASSERT_MSG(false, fmt::format("did not find string '{}'", file_info_string));
}
// extract the name info as a "name" + "chunk id" + "-ag.go" format.
return extract_name(get_string_of_max_length((const char*)(chunk.data() + offset), 128)).name +
"-ag";
}
/*!
* Look inside the chunks to determine the source file name.
* Does a lot of checking, might not work in future versions without some updating.
*/
std::string StrFileReader::get_full_name(const std::string& short_name) const {
std::string result;
bool done_first = false;
// this string is part of the file info struct and the stuff after it is the file name.
const auto& file_info_string = get_art_group_file_info_string();
// it should occur in each chunk.
int chunk_id = 0;
for (const auto& chunk : m_chunks) {
std::string chunk_long_name;
// find the file info string in the chunk.
int offset;
if (find_string_in_data(chunk.data(), int(chunk.size()), file_info_string, &offset)) {
offset += file_info_string.length();
} else {
ASSERT_MSG(false, fmt::format("did not find string '{}'", file_info_string));
}
// extract the name info as a "name" + "chunk id" + "-ag.go" format.
auto full_name =
extract_name(get_string_of_max_length((const char*)(chunk.data() + offset), 128));
// make sure it matches previous chunks for the name
if (!done_first) {
result = full_name.name;
} else {
ASSERT(result == full_name.name);
}
// make sure the index is right.
ASSERT(full_name.chunk_idx == chunk_id);
done_first = true;
chunk_id++;
}
// convert to ISO names in two ways.
char iso_name_2[256];
char iso_name_1[256];
// first, using the file name to ISO name
file_util::MakeISOName(iso_name_1, short_name.c_str());
// second, using the full name.
file_util::ISONameFromAnimationName(iso_name_2, result.c_str());
ASSERT(strcmp(iso_name_1, iso_name_2) == 0);
return result;
}
std::string StrFileReader::get_chunk_texture_name(int idx) const {
const auto& chunk = m_chunks[idx];
auto find_string = get_texture_page_file_info_string();
int offset;
if (find_string_in_data(chunk.data(), int(chunk.size()), find_string, &offset)) {
offset += find_string.length();
} else {
ASSERT_MSG(false, fmt::format("did not find string '{}'", find_string));
}
for (int i = 0; i < 128; i++) {
if (chunk[offset + i] == '.') {
std::string result;
result.assign((const char*)&chunk[offset], i);
return result;
}
}
ASSERT_NOT_REACHED();
}
} // namespace decompiler