mirror of
https://github.com/open-goal/jak-project
synced 2026-09-06 11:20:54 -04:00
710f3ac117
Custom levels for Jak 2/3 now support envmapped TIE geometry. The TIE extract was also changed to ignore materials that have the specular flag set, but are missing a roughness texture. Jak 2/3 now also support the `build-actor` tool. The `build-custom-level` and `build-actor` macros now have a few new options: - Both now have a `force-run` option (`#f` by default) that, when set to `#t`, will always run level/art group generation even if the output files are up to date. - `build-custom-level` has a `gen-fr3` option (`#t` by default) that, when set to `#f`, will skip generating the FR3 file for the custom level and only generate the GOAL level file to skip the potentially slow process of finding and adding art groups and textures. Useful for when you want to temporarily edit only the GOAL side of the level (such as entity placement, etc.). - `build-actor` has a `texture-bucket` option (default 0) which will determine what DMA sink group the model will be placed in, which is useful to determine the draw order of the model. Previously, this was omitted, resulting in shadows not drawing over custom actors because the actors were put in a bucket that is drawn after shadows (this behavior can be restored with `:texture-bucket #f`).
346 lines
15 KiB
C++
346 lines
15 KiB
C++
#include "build_level.h"
|
|
|
|
#include "common/util/gltf_util.h"
|
|
|
|
#include "decompiler/extractor/extractor_util.h"
|
|
#include "decompiler/level_extractor/BspHeader.h"
|
|
#include "decompiler/level_extractor/extract_collide_frags.h"
|
|
#include "decompiler/level_extractor/extract_level.h"
|
|
#include "decompiler/level_extractor/extract_merc.h"
|
|
#include "goalc/build_level/collide/jak1/collide_bvh.h"
|
|
#include "goalc/build_level/collide/jak1/collide_pack.h"
|
|
#include "goalc/build_level/common/Tfrag.h"
|
|
#include "goalc/build_level/jak1/Entity.h"
|
|
#include "goalc/build_level/jak1/FileInfo.h"
|
|
#include "goalc/build_level/jak1/LevelFile.h"
|
|
|
|
namespace jak1 {
|
|
bool run_build_level(const std::string& input_file,
|
|
const std::string& bsp_output_file,
|
|
const std::string& output_prefix,
|
|
bool gen_fr3) {
|
|
auto level_json = parse_commented_json(
|
|
file_util::read_text_file(file_util::get_file_path({input_file})), input_file);
|
|
LevelFile file{}; // GOAL level file
|
|
tfrag3::Level pc_level; // PC level file
|
|
gltf_util::TexturePool tex_pool; // pc level texture pool
|
|
|
|
// process input mesh from blender
|
|
gltf_mesh_extract::Input mesh_extract_in;
|
|
mesh_extract_in.filename =
|
|
file_util::get_file_path({level_json.at("gltf_file").get<std::string>()});
|
|
mesh_extract_in.auto_wall_enable = level_json.value("automatic_wall_detection", true);
|
|
mesh_extract_in.double_sided_collide = level_json.at("double_sided_collide").get<bool>();
|
|
mesh_extract_in.auto_wall_angle = level_json.value("automatic_wall_angle", 30.0);
|
|
mesh_extract_in.tex_pool = &tex_pool;
|
|
gltf_mesh_extract::Output mesh_extract_out;
|
|
gltf_mesh_extract::extract(mesh_extract_in, mesh_extract_out);
|
|
|
|
// add stuff to the GOAL level structure
|
|
file.info = make_file_info_for_level(fs::path(input_file).filename().string());
|
|
// all vis
|
|
// drawable trees
|
|
// pat
|
|
// texture remap
|
|
// texture ids
|
|
// unk zero
|
|
// name
|
|
file.name = level_json.at("long_name").get<std::string>();
|
|
// nick
|
|
file.nickname = level_json.at("nickname").get<std::string>();
|
|
// vis infos
|
|
// actors
|
|
std::vector<EntityActor> actors;
|
|
auto dts = decompiler::DecompilerTypeSystem(GameVersion::Jak1);
|
|
dts.parse_type_defs({"decompiler", "config", "jak1", "all-types.gc"});
|
|
add_actors_from_json(level_json.at("actors"), actors, level_json.value("base_id", 1234), dts);
|
|
std::sort(actors.begin(), actors.end(), [](auto& a, auto& b) { return a.aid < b.aid; });
|
|
auto duplicates = std::adjacent_find(actors.begin(), actors.end(),
|
|
[](auto& a, auto& b) { return a.aid == b.aid; });
|
|
ASSERT_MSG(duplicates == actors.end(),
|
|
fmt::format("Actor IDs must be unique. Found at least two actors with ID {}",
|
|
duplicates->aid));
|
|
file.actors = std::move(actors);
|
|
// ambients
|
|
std::vector<EntityAmbient> ambients;
|
|
jak1::add_ambients_from_json(level_json.at("ambients"), ambients,
|
|
level_json.value("base_id", 12345), dts);
|
|
file.ambients = std::move(ambients);
|
|
auto& ambient_drawable_tree = file.drawable_trees.ambients.emplace_back();
|
|
(void)ambient_drawable_tree;
|
|
// cameras
|
|
// nodes
|
|
// boxes
|
|
// ambients
|
|
// subdivs
|
|
// adgifs
|
|
// actor birth
|
|
for (size_t i = 0; i < file.actors.size(); i++) {
|
|
file.actor_birth_order.push_back(i);
|
|
}
|
|
// split box
|
|
|
|
// add stuff to the PC level structure
|
|
pc_level.level_name = file.name;
|
|
|
|
// TFRAG
|
|
file.drawable_trees.tfrags.emplace_back("drawable-tree-tfrag", "drawable-inline-array-tfrag");
|
|
file.drawable_trees.tfrags.emplace_back("drawable-tree-trans-tfrag",
|
|
"drawable-inline-array-trans-tfrag");
|
|
|
|
tfrag_from_gltf(mesh_extract_out.tfrag, pc_level.tfrag_trees[0]);
|
|
|
|
// TIE
|
|
if (!mesh_extract_out.tie.base_draws.empty()) {
|
|
file.drawable_trees.ties.emplace_back();
|
|
tie_from_gltf(mesh_extract_out.tie, pc_level.tie_trees[0]);
|
|
}
|
|
|
|
// TEXTURE
|
|
pc_level.textures = std::move(tex_pool.textures_by_idx);
|
|
|
|
// COLLIDE
|
|
if (mesh_extract_out.collide.faces.empty()) {
|
|
lg::error("No collision geometry was found");
|
|
} else {
|
|
auto& collide_drawable_tree = file.drawable_trees.collides.emplace_back();
|
|
collide_drawable_tree.bvh = collide::construct_collide_bvh(mesh_extract_out.collide.faces);
|
|
collide_drawable_tree.packed_frags = pack_collide_frags(collide_drawable_tree.bvh.frags.frags);
|
|
// for collision renderer
|
|
for (auto& face : mesh_extract_out.collide.faces) {
|
|
math::Vector4f verts[3];
|
|
for (int i = 0; i < 3; i++) {
|
|
verts[i].x() = face.v[i].x();
|
|
verts[i].y() = face.v[i].y();
|
|
verts[i].z() = face.v[i].z();
|
|
verts[i].w() = 1.f;
|
|
}
|
|
tfrag3::CollisionMesh::Vertex out_verts[3];
|
|
decompiler::set_vertices_for_tri(out_verts, verts);
|
|
for (auto& out : out_verts) {
|
|
out.pat = face.pat.val;
|
|
pc_level.collision.vertices.push_back(out);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto sky_name = level_json.value("sky", "none");
|
|
auto texture_remap = level_json.value("tex_remap", "none");
|
|
auto tpages = level_json.value("tpages", std::vector<u32>({}));
|
|
|
|
// Add textures and models
|
|
// TODO remove hardcoded config settings
|
|
if (gen_fr3 && ((level_json.contains("art_groups") && !level_json.at("art_groups").empty()) ||
|
|
(level_json.contains("textures") && !level_json.at("textures").empty()))) {
|
|
lg::info("Looking for ISO path...");
|
|
const auto iso_folder = file_util::get_iso_dir_for_game(GameVersion::Jak1);
|
|
lg::info("Found ISO path: {}", iso_folder.string());
|
|
|
|
if (iso_folder.empty() || !fs::exists(iso_folder)) {
|
|
lg::warn("Could not locate ISO path!");
|
|
return false;
|
|
}
|
|
|
|
// Look for iso build info if it's available, otherwise default to ntsc_v1
|
|
const auto version_info = get_version_info_or_default(iso_folder);
|
|
|
|
decompiler::Config config;
|
|
try {
|
|
config = decompiler::read_config_file(
|
|
file_util::get_jak_project_dir() / "decompiler/config/jak1/jak1_config.jsonc",
|
|
version_info.decomp_config_version,
|
|
R"({"decompile_code": false, "find_functions": false, "levels_extract": true, "allowed_objects": [], "save_texture_pngs": false})");
|
|
} catch (const std::exception& e) {
|
|
lg::error("Failed to parse config: {}", e.what());
|
|
return false;
|
|
}
|
|
|
|
std::vector<fs::path> dgos, objs;
|
|
for (const auto& dgo_name : config.dgo_names) {
|
|
dgos.push_back(iso_folder / dgo_name);
|
|
}
|
|
|
|
for (const auto& obj_name : config.object_file_names) {
|
|
objs.push_back(iso_folder / obj_name);
|
|
}
|
|
|
|
decompiler::ObjectFileDB db(dgos, fs::path(config.obj_file_name_map_file), objs, {}, {}, {},
|
|
config);
|
|
|
|
// need to process link data for tpages
|
|
db.process_link_data(config);
|
|
|
|
decompiler::TextureDB tex_db;
|
|
auto textures_out = file_util::get_jak_project_dir() / "decompiler_out/jak1/textures";
|
|
file_util::create_dir_if_needed(textures_out);
|
|
db.process_tpages(tex_db, textures_out, config, "");
|
|
auto replacements_path = file_util::get_jak_project_dir() / "custom_assets" /
|
|
game_version_names[config.game_version] / "texture_replacements";
|
|
if (fs::exists(replacements_path)) {
|
|
tex_db.replace_textures(replacements_path);
|
|
}
|
|
|
|
std::vector<std::string> processed_art_groups;
|
|
|
|
// find all art groups used by the custom level in other dgos and extract sky and texture remap
|
|
// if desired
|
|
auto should_process_art_groups =
|
|
(level_json.contains("art_groups") && !level_json.at("art_groups").empty()) ||
|
|
(sky_name != "none" || texture_remap != "none");
|
|
if (should_process_art_groups) {
|
|
for (auto& dgo : config.dgo_names) {
|
|
// remove "DGO/" prefix
|
|
const auto& dgo_name = dgo.substr(4);
|
|
const auto& files = db.obj_files_by_dgo.at(dgo_name);
|
|
auto art_groups =
|
|
find_art_groups(processed_art_groups,
|
|
level_json.value("art_groups", std::vector<std::string>{}), files);
|
|
std::vector<level_tools::TextureRemap> tex_remap{};
|
|
if (auto bsp = get_bsp_file(files, dgo_name)) {
|
|
const auto& link_data = db.lookup_record(bsp.value()).linked_data;
|
|
if (is_valid_bsp(link_data)) {
|
|
level_tools::BspHeader level_file;
|
|
level_file.read_from_file(link_data, dts, GameVersion::Jak1, true);
|
|
auto bsp_name = bsp.value().name.substr(0, bsp.value().name.size() - 4);
|
|
tex_remap = level_file.texture_remap_table;
|
|
auto is_sky_bsp = bsp_name == sky_name;
|
|
auto is_tex_remap_bsp = bsp_name == texture_remap;
|
|
auto sky_and_tex_remap_same = sky_name == texture_remap;
|
|
if (is_tex_remap_bsp) {
|
|
lg::info("custom level: copying texture remap data from {}", texture_remap);
|
|
// copy texture remap data from bsp
|
|
file.texture_remap_table.resize(tex_remap.size());
|
|
memcpy(file.texture_remap_table.data(), level_file.texture_remap_table.data(),
|
|
tex_remap.size() * sizeof(level_tools::TextureRemap));
|
|
}
|
|
if (is_sky_bsp) {
|
|
// copy adgif data from bsp
|
|
lg::info("custom level: copying adgifs from {}", sky_name);
|
|
auto& adgifs = file.adgifs.adgifs;
|
|
adgifs.resize(level_file.adgifs.adgifs.size());
|
|
memcpy(adgifs.data(), level_file.adgifs.adgifs.data(),
|
|
level_file.adgifs.adgifs.size() * sizeof(AdGifData));
|
|
}
|
|
if (sky_and_tex_remap_same && is_sky_bsp && is_tex_remap_bsp && tpages.empty()) {
|
|
// if tpages json is empty and sky and tex remap are the same level, auto fill
|
|
file.texture_ids.resize(level_file.texture_page_count);
|
|
memcpy(file.texture_ids.data(), level_file.texture_ids.data(),
|
|
sizeof(u32) * level_file.texture_page_count);
|
|
std::vector<u32> tex_ids;
|
|
tex_ids.reserve(level_file.texture_page_count);
|
|
for (auto& id : level_file.texture_ids) {
|
|
tex_ids.push_back(id >> 20);
|
|
}
|
|
lg::info("custom level: login tpages automatically set to [{}]",
|
|
fmt::join(tex_ids, ", "));
|
|
}
|
|
}
|
|
}
|
|
for (const auto& ag : art_groups) {
|
|
if (ag.name.length() > 3 && !ag.name.compare(ag.name.length() - 3, 3, "-ag")) {
|
|
const auto& ag_file = db.lookup_record(ag);
|
|
lg::info("custom level: extracting art group {}", ag_file.name_in_dgo);
|
|
decompiler::MercSwapInfo info;
|
|
decompiler::extract_merc(ag_file, tex_db, db.dts, tex_remap, pc_level, false,
|
|
db.version(), info);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// add textures
|
|
if (level_json.contains("textures") && !level_json.at("textures").empty()) {
|
|
std::map<std::string, std::vector<std::string>> processed_textures;
|
|
auto tex_json = level_json.value("textures", std::vector<std::vector<std::string>>{});
|
|
std::map<std::string, std::vector<std::string>> wanted_texs;
|
|
for (auto& arr : tex_json) {
|
|
auto tpage_name = arr[0];
|
|
// we only want a select few textures
|
|
if (arr.size() > 1) {
|
|
for (size_t i = 1; i < arr.size(); i++) {
|
|
wanted_texs.insert({tpage_name, {arr.begin() + 1, arr.end()}});
|
|
}
|
|
} else {
|
|
// we want all textures from this tpage
|
|
auto it = std::find_if(tex_db.tpage_names.begin(), tex_db.tpage_names.end(),
|
|
[tpage_name](const std::pair<u32, std::string>& t) {
|
|
return t.second == tpage_name;
|
|
});
|
|
if (it != tex_db.tpage_names.end()) {
|
|
lg::info("custom level: adding all textures from tpage {}:", tpage_name);
|
|
std::vector<std::string> tex_names;
|
|
for (auto& [id, tex] : tex_db.textures) {
|
|
if (tex_db.tpage_names.at(tex.page) == tpage_name) {
|
|
lg::info("custom level: adding texture {} (tpage {})", tex.name, tex.page);
|
|
tex_names.push_back(tex.name);
|
|
pc_level.textures.push_back(make_texture(id, tex, tpage_name, true));
|
|
processed_textures[tpage_name].push_back(tex.name);
|
|
}
|
|
}
|
|
wanted_texs.insert({tpage_name, tex_names});
|
|
}
|
|
}
|
|
}
|
|
|
|
// first check the texture is not already in the level
|
|
for (auto& level_tex : pc_level.textures) {
|
|
auto tpage = level_tex.debug_tpage_name;
|
|
auto name = level_tex.debug_name;
|
|
auto it = std::find_if(
|
|
wanted_texs.begin(), wanted_texs.end(),
|
|
[tpage, name](const std::pair<std::string, std::vector<std::string>>& elt) {
|
|
return elt.first == tpage &&
|
|
std::find(elt.second.begin(), elt.second.end(), name) != elt.second.end();
|
|
});
|
|
if (it != wanted_texs.end()) {
|
|
processed_textures[level_tex.debug_tpage_name].push_back(level_tex.debug_name);
|
|
}
|
|
}
|
|
|
|
// then add
|
|
for (auto& [id, tex] : tex_db.textures) {
|
|
auto db_tpage_name = tex_db.tpage_names.at(tex.page);
|
|
for (auto& [wanted_tpage_name, wanted_tex_list] : wanted_texs) {
|
|
auto processed = processed_textures[db_tpage_name];
|
|
if (std::find(processed.begin(), processed.end(), tex.name) != processed.end()) {
|
|
// lg::info("custom level: ignoring duplicate texture {} from {}", tex.name, tex.page);
|
|
continue;
|
|
}
|
|
for (auto& wanted_tex : wanted_tex_list)
|
|
if (db_tpage_name == wanted_tpage_name && tex.name == wanted_tex) {
|
|
lg::info("custom level: adding texture {} from tpage {} ({})", tex.name, tex.page,
|
|
db_tpage_name);
|
|
pc_level.textures.push_back(make_texture(id, tex, db_tpage_name, true));
|
|
processed_textures[db_tpage_name].push_back(tex.name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// add custom models to fr3
|
|
if (gen_fr3 && level_json.contains("custom_models") && !level_json.at("custom_models").empty()) {
|
|
auto models = level_json.at("custom_models").get<std::vector<std::string>>();
|
|
for (auto& name : models) {
|
|
add_model_to_level(GameVersion::Jak1, name, pc_level);
|
|
}
|
|
}
|
|
|
|
// Save the GOAL level
|
|
auto result = file.save_object_file();
|
|
lg::print("Level bsp file size {} bytes\n", result.size());
|
|
auto save_path = file_util::get_jak_project_dir() / bsp_output_file;
|
|
file_util::create_dir_if_needed_for_file(save_path);
|
|
lg::print("Saving to {}\n", save_path.string());
|
|
file_util::write_binary_file(save_path, result.data(), result.size());
|
|
|
|
// Save the PC level
|
|
if (gen_fr3) {
|
|
save_pc_data(file.name, pc_level,
|
|
file_util::get_jak_project_dir() / "out" / output_prefix / "fr3");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
} // namespace jak1
|