mirror of
https://github.com/open-goal/jak-project
synced 2026-08-07 02:06:59 -04:00
386 lines
16 KiB
C++
386 lines
16 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"
|
|
|
|
#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<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>();
|
|
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<std::string>();
|
|
// vis infos
|
|
// actors
|
|
std::vector<u32> aid_list;
|
|
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::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<EntityAmbient> 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<EntityCamera> 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<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);
|
|
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<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 (!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<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_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<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, 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
|