#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" #include "fmt/ranges.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()}); 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(); 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(); ASSERT_MSG(file.name.size() <= 10, fmt::format("long_name over 10 characters ({} characters): '{}'", file.name.size(), file.name)); // nick file.nickname = level_json.at("nickname").get(); // vis infos // actors std::vector aid_list; std::vector 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::ranges::sort(actors, [](auto& a, auto& b) { return a.aid < b.aid; }); for (auto& actor : actors) { aid_list.push_back(actor.aid); } auto duplicate_actors = std::ranges::adjacent_find(actors, [](auto& a, auto& b) { return a.aid == b.aid; }); ASSERT_MSG(duplicate_actors == actors.end(), fmt::format("Actor IDs must be unique. Found at least two actors with ID {}", duplicate_actors->aid)); file.actors = std::move(actors); // ambients std::vector ambients; 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 if (level_json.contains("cameras")) { std::vector cameras; add_cameras_from_json( level_json.at("cameras"), cameras, std::max(level_json.value("base_id", 2345), (int)*std::ranges::max_element(aid_list)), dts); std::ranges::sort(cameras, [](auto& a, auto& b) { return a.aid < b.aid; }); for (auto& cam : cameras) { aid_list.push_back(cam.aid); } std::ranges::sort(aid_list, [](auto& a, auto& b) { return a < b; }); auto duplicate_cams = std::ranges::adjacent_find(cameras, [](auto& a, auto& b) { return a.aid == b.aid; }); ASSERT_MSG( duplicate_cams == cameras.end(), fmt::format("Actor IDs must be unique. Found at least two camera entities with ID {}", duplicate_cams->aid)); auto duplicate_aids = std::ranges::adjacent_find(aid_list, [](auto& a, auto& b) { return a == b; }); ASSERT_MSG(duplicate_aids == aid_list.end(), fmt::format("Actor IDs must be unique. Found one actor and camera entity with ID {}", *duplicate_aids)); file.cameras = std::move(cameras); } // nodes // boxes // 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({})); // 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 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 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); ASSERT_MSG( db.obj_files_by_dgo.contains(dgo_name), fmt::format("{} DGO expected to be part of the ObjectDB but it is not!", dgo_name)); 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{}), files); std::vector 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 (!tpages.empty()) { file.texture_ids.resize(tpages.size()); for (size_t i = 0; i < tpages.size(); ++i) { file.texture_ids[i] = tpages[i] << 20; } } } 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 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> processed_textures; auto tex_json = level_json.value("textures", std::vector>{}); std::map> 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& 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 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_db, 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>& 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, 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>(); 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