Files
jak-project/goalc/build_level/common/ResLump.cpp
Hat Kid c64eea6337 [buildactor] support generating collide-meshes for custom models (#3540)
This adds support for generating collide meshes when importing custom
models. A couple of things to keep in mind:

- A single `collide-mesh` may only have up to 255 vertices.
- When exporting a GLTF file in Blender, a `collide-mesh` will be
generated for every mesh object that has collision properties applied
(ideally, you would set all visual meshes to `ignore` and your collision
meshes to `invisible` in the OpenGOAL plugin's custom properties).
- Ensure that your actor using the model properly allocates enough
`collide-shape-prim-mesh`es for each `collide-mesh` ([example from the
original game that uses multiple
meshes](https://github.com/open-goal/jak-project/blob/f6688659f2ef85f5ceaacea6271580c9f4d91ed1/goal_src/jak1/levels/finalboss/robotboss.gc#L2628-L2806)).

~One annoying problem that I haven't fully figured out yet (unrelated to
the actual functionality):
`collide-mesh`es are stored in art groups as an `(array collide-mesh)`
in the `art-joint-geo`'s `extra`, so I had to add a new `Res` type to
support this. The way that `array`s are stored in `res-lump`s is a bit
of a hack right now. The lump only stores a pointer to the array, so the
size of that is 4 bytes, but because we have to generate all the actual
array data too, the current `ResLump` code in C++ doesn't handle this
case well and would assert, so I decided to omit the asserts if an
`array` tag is present and "fake" the size so the object file is
generated more closely to how the game expects it until we figure out
something better.~
This was fixed by generating the array data beforehand and creating a
`ResRef` class that takes the pointer to the array data and adds it to
the lump.
2024-05-29 06:09:20 +02:00

385 lines
10 KiB
C++

#include "ResLump.h"
#include <algorithm>
#include "common/util/BitUtils.h"
#include "goalc/data_compiler/DataObjectGenerator.h"
#include "fmt/core.h"
/*
* name: crate-3141
* .symbol name
.word 0xce6e6b28
.type string
.word 0x10000
scale = 1., 1., 1., 1.,
.symbol scale
.word 0xce6e6b28
.type vector
.word 0x80010010
.symbol visvol
.word 0xce6e6b28
.type vector
.word 0x80020020
.symbol shadow-mask
.word 0xce6e6b28
.type uint8
.word 0x80010040
.symbol eco-info
.word 0xce6e6b28
.type int32
.word 0x80020044
.symbol movie-pos
.word 0xce6e6b28
.type vector
.word 0x80010050
.symbol vis-dist
.word 0xce6e6b28
.type float
.word 0x80010060
*/
Res::Res(const std::string& name, float key_frame) : m_name(name), m_key_frame(key_frame) {}
ResFloat::ResFloat(const std::string& name, const std::vector<float>& values, float key_frame)
: Res(name, key_frame), m_values(values) {}
TagInfo ResFloat::get_tag_info() const {
TagInfo result;
result.elt_type = "float";
result.elt_count = m_values.size();
result.inlined = true;
result.data_size = m_values.size() * sizeof(float);
return result;
}
void ResFloat::write_data(DataObjectGenerator& gen) const {
for (auto& val : m_values) {
gen.add_word_float(val);
}
}
int ResFloat::get_alignment() const {
return 16;
}
ResInt32::ResInt32(const std::string& name, const std::vector<s32>& values, float key_frame)
: Res(name, key_frame), m_values(values) {}
TagInfo ResInt32::get_tag_info() const {
TagInfo result;
result.elt_type = "int32";
result.elt_count = m_values.size();
result.inlined = true;
result.data_size = m_values.size() * sizeof(s32);
return result;
}
void ResInt32::write_data(DataObjectGenerator& gen) const {
for (auto& val : m_values) {
gen.add_word(val);
}
}
int ResInt32::get_alignment() const {
return 16;
}
ResUint8::ResUint8(const std::string& name, const std::vector<u8>& values, float key_frame)
: Res(name, key_frame), m_values(values) {}
TagInfo ResUint8::get_tag_info() const {
TagInfo result;
result.elt_type = "uint8";
result.elt_count = m_values.size();
result.inlined = true;
result.data_size = align4(m_values.size()) * sizeof(u8);
return result;
}
void ResUint8::write_data(DataObjectGenerator& gen) const {
u32 size_words = align4(m_values.size()) / 4;
auto offset = gen.current_offset_bytes();
for (u32 i = 0; i < size_words; i++) {
gen.add_word(0);
}
memcpy(gen.data() + offset, m_values.data(), m_values.size());
}
int ResUint8::get_alignment() const {
return 16;
}
ResUint32::ResUint32(const std::string& name, const std::vector<u32>& values, float key_frame)
: Res(name, key_frame), m_values(values) {}
TagInfo ResUint32::get_tag_info() const {
TagInfo result;
result.elt_type = "uint32";
result.elt_count = m_values.size();
result.inlined = true;
result.data_size = m_values.size() * sizeof(u32);
return result;
}
void ResUint32::write_data(DataObjectGenerator& gen) const {
for (auto& val : m_values) {
gen.add_word(val);
}
}
int ResUint32::get_alignment() const {
return 16;
}
ResVector::ResVector(const std::string& name,
const std::vector<math::Vector4f>& values,
float key_frame)
: Res(name, key_frame), m_values(values) {}
TagInfo ResVector::get_tag_info() const {
TagInfo result;
result.elt_type = "vector";
result.elt_count = m_values.size();
result.inlined = true;
result.data_size = m_values.size() * sizeof(math::Vector4f);
return result;
}
void ResVector::write_data(DataObjectGenerator& gen) const {
for (auto& val : m_values) {
for (int i = 0; i < 4; i++) {
gen.add_word_float(val[i]);
}
}
}
int ResVector::get_alignment() const {
return 16;
}
ResString::ResString(const std::string& name, const std::vector<std::string>& str, float key_frame)
: Res(name, key_frame), m_str(str) {}
ResString::ResString(const std::string& name, const std::string& str, float key_frame)
: Res(name, key_frame), m_str({str}) {}
TagInfo ResString::get_tag_info() const {
TagInfo result;
result.elt_type = "string";
result.elt_count = m_str.size();
result.inlined = false;
result.data_size = 4 * m_str.size();
return result;
}
void ResString::write_data(DataObjectGenerator& gen) const {
for (auto& str : m_str) {
gen.add_ref_to_string_in_pool(str);
}
}
int ResString::get_alignment() const {
return 4;
}
ResSymbol::ResSymbol(const std::string& name, const std::vector<std::string>& str, float key_frame)
: Res(name, key_frame), m_str(str) {}
ResSymbol::ResSymbol(const std::string& name, const std::string& str, float key_frame)
: Res(name, key_frame), m_str({str}) {}
TagInfo ResSymbol::get_tag_info() const {
TagInfo result;
result.elt_type = "symbol";
result.elt_count = m_str.size();
result.inlined = false;
result.data_size = 4 * m_str.size();
return result;
}
void ResSymbol::write_data(DataObjectGenerator& gen) const {
for (auto& str : m_str) {
gen.add_symbol_link(str);
}
}
int ResSymbol::get_alignment() const {
return 4;
}
ResType::ResType(const std::string& name, const std::vector<std::string>& str, float key_frame)
: Res(name, key_frame), m_str(str) {}
ResType::ResType(const std::string& name, const std::string& str, float key_frame)
: Res(name, key_frame), m_str({str}) {}
TagInfo ResType::get_tag_info() const {
TagInfo result;
result.elt_type = "type";
result.elt_count = m_str.size();
result.inlined = false;
result.data_size = 4 * m_str.size();
return result;
}
void ResType::write_data(DataObjectGenerator& gen) const {
for (auto& str : m_str) {
gen.add_type_tag(str);
}
}
int ResType::get_alignment() const {
return 4;
}
ResRef::ResRef(const std::string& name, const std::string& type, size_t ref, float key_frame)
: Res(name, key_frame), m_ref(ref), m_type(type) {}
TagInfo ResRef::get_tag_info() const {
TagInfo result;
result.elt_type = m_type;
result.elt_count = 1;
result.inlined = false;
result.data_size = 4;
return result;
}
void ResRef::write_data(DataObjectGenerator& gen) const {
gen.link_word_to_byte(gen.add_word(0), m_ref);
}
int ResRef::get_alignment() const {
return 4;
}
void ResLump::add_res(std::unique_ptr<Res> res) {
m_sorted = false;
m_res.emplace_back(std::move(res));
}
constexpr int kExtraTagSlots = 10;
void ResLump::sort_res() {
std::stable_sort(m_res.begin(), m_res.end(),
[](const std::unique_ptr<Res>& a, const std::unique_ptr<Res>& b) {
u64 a_chars = 0;
u64 b_chars = 0;
auto& a_name = a->name();
auto& b_name = b->name();
memcpy(&a_chars, a_name.data(), std::min(sizeof(u64), a_name.size()));
memcpy(&b_chars, b_name.data(), std::min(sizeof(u64), b_name.size()));
return a_chars < b_chars;
});
m_sorted = true;
}
size_t ResLump::generate_header(DataObjectGenerator& gen,
const std::string& most_specific_type) const {
gen.align_to_basic();
gen.add_type_tag(most_specific_type);
auto result = gen.current_offset_bytes();
gen.add_word(m_res.size()); // length;
gen.add_word(m_res.size() + kExtraTagSlots); // allocated-length
gen.add_word(0); // data base
gen.add_word(0); // data top
gen.add_word(0); // data size;
gen.add_word(0); // extra
gen.add_word(0); // tag.
return result;
}
void ResLump::generate_tag_list_and_data(DataObjectGenerator& gen, size_t header_to_update) const {
ASSERT(m_sorted);
gen.align_to_basic();
// first is the tag array.
const size_t tag_array_start = gen.current_offset_bytes();
const size_t tag_array_size = 16 * (m_res.size() + kExtraTagSlots);
const size_t tag_array_end = tag_array_start + tag_array_size;
// next is data
const size_t data_start = align16(tag_array_end);
size_t current_data_ptr = data_start;
// on the first pass through, we'll also build these:
struct ResRec {
size_t align;
size_t data;
size_t reported_size;
};
std::vector<ResRec> recs;
// first pass to write tags and figure out data layout
for (auto& res : m_res) {
auto& rec = recs.emplace_back();
auto alignment = res->get_alignment();
while (current_data_ptr % alignment) {
current_data_ptr++;
}
auto tag = res->get_tag_info();
// name:
gen.add_symbol_link(res->name());
// key frame
gen.add_word_float(res->key_frame());
// elt type
gen.add_type_tag(tag.elt_type);
// packed
u32 packed = 0;
ASSERT(current_data_ptr - data_start < UINT16_MAX);
packed |= (u16)(current_data_ptr - data_start);
ASSERT(tag.elt_count < (UINT16_MAX >> 1));
packed |= (((u16)tag.elt_count) << 16);
if (tag.inlined) {
packed |= (1 << 31);
}
gen.add_word(packed);
rec.data = current_data_ptr;
rec.reported_size = tag.data_size;
rec.align = alignment;
current_data_ptr += tag.data_size;
}
for (int i = 0; i < kExtraTagSlots * 4; i++) {
gen.add_word(0);
}
const size_t data_end = current_data_ptr; // todo, does this get rounded up at all?
gen.align_to_basic();
ASSERT(gen.current_offset_bytes() == data_start);
current_data_ptr = data_start;
// second pass to write data
for (size_t res_idx = 0; res_idx < m_res.size(); res_idx++) {
const auto& res = m_res[res_idx];
const auto& rec = recs[res_idx];
// pad!
while (current_data_ptr % rec.align) {
current_data_ptr += 4;
gen.add_word(0);
}
res->write_data(gen);
ASSERT_MSG(gen.current_offset_bytes() - current_data_ptr == rec.reported_size,
fmt::format("reported size of {} does not match actual size of {}",
rec.reported_size, gen.current_offset_bytes() - current_data_ptr));
current_data_ptr = gen.current_offset_bytes();
}
ASSERT_MSG(gen.current_offset_bytes() == data_end,
fmt::format("mismatch between current offset ({}) and data end ({})",
gen.current_offset_bytes(), data_end));
ASSERT(data_end == current_data_ptr);
// update header
gen.link_word_to_byte((header_to_update + 2 * 4) / 4, data_start);
gen.link_word_to_byte((header_to_update + 3 * 4) / 4, data_end);
gen.set_word((header_to_update + 4 * 4) / 4, data_end - data_start);
gen.link_word_to_byte((header_to_update + 6 * 4) / 4, tag_array_start);
}