#include "build_level.h" #include "common/util/gltf_util.h" #include "decompiler/extractor/extractor_util.h" #include "decompiler/level_extractor/extract_merc.h" #include "goalc/build_level/collide/jak2/collide.h" #include "goalc/build_level/common/Tfrag.h" #include "goalc/build_level/jak2/Entity.h" #include "goalc/build_level/jak2/FileInfo.h" #include "goalc/build_level/jak2/LevelFile.h" #include namespace jak2 { 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 auto dts = decompiler::DecompilerTypeSystem(GameVersion::Jak2); dts.parse_enum_defs({"decompiler", "config", "jak2", "all-types.gc"}); std::vector actors; 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); // actor groups if (level_json.contains("actor_groups") && !level_json.at("actor_groups").empty()) { add_actor_groups_from_json(level_json.at("actor_groups"), file.actors, file.actor_groups, 0); } // cameras // nodes // regions if (level_json.contains("region_trees") && !level_json.at("region_trees").empty()) { file.region_array.entities = &file.actors; file.region_array.actor_groups = &file.actor_groups; fill_region_trees(file.region_trees, file.regions, file.region_array, level_json.at("region_trees"), level_json.value("base_region_id", 0)); } // subdivs // actor birth for (size_t i = 0; i < file.actors.size(); i++) { file.actor_birth_order.push_back(i); } // 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"); 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]); } 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 { file.collide_hash = construct_collide_hash(mesh_extract_out.collide.faces); // 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); } } } // 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()); // 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::Jak2); 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/jak2/jak2_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/jak2/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); } // add textures if (level_json.contains("textures") && !level_json.at("textures").empty()) { std::vector processed_textures; std::vector wanted_texs = level_json.at("textures").get>(); // first check the texture is not already in the level for (auto& level_tex : pc_level.textures) { if (std::find(wanted_texs.begin(), wanted_texs.end(), level_tex.debug_name) != wanted_texs.end()) { processed_textures.push_back(level_tex.debug_name); } } // then add for (auto& [id, tex] : tex_db.textures) { for (auto& tex0 : wanted_texs) { if (std::find(processed_textures.begin(), processed_textures.end(), tex.name) != processed_textures.end()) { continue; } if (tex.name == tex0) { lg::info("custom level: adding texture {} from tpage {} ({})", tex.name, tex.page, tex_db.tpage_names.at(tex.page)); pc_level.textures.push_back(make_texture(id, tex_db, true)); processed_textures.push_back(tex.name); } } } } // find all art groups used by the custom level in other dgos if (gen_fr3 && level_json.contains("art_groups") && !level_json.at("art_groups").empty()) { for (auto& dgo : config.dgo_names) { std::vector processed_art_groups; // 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.at("art_groups").get>(), files); auto tex_remap = decompiler::extract_tex_remap(db, dgo_name); 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::print("custom level: extracting art group {}\n", 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 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::Jak2, name, pc_level); } } // 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 jak2