mirror of
https://github.com/open-goal/jak-project
synced 2026-08-28 00:41:58 -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`).
222 lines
8.8 KiB
C++
222 lines
8.8 KiB
C++
#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"
|
|
|
|
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<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
|
|
auto dts = decompiler::DecompilerTypeSystem(GameVersion::Jak2);
|
|
dts.parse_enum_defs({"decompiler", "config", "jak2", "all-types.gc"});
|
|
std::vector<EntityActor> 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);
|
|
// cameras
|
|
// nodes
|
|
// regions
|
|
// 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);
|
|
}
|
|
|
|
// 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<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/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);
|
|
}
|
|
|
|
// 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<std::string> 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<std::vector<std::string>>(), 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 textures
|
|
if (level_json.contains("textures") && !level_json.at("textures").empty()) {
|
|
std::vector<std::string> processed_textures;
|
|
std::vector<std::string> wanted_texs =
|
|
level_json.at("textures").get<std::vector<std::string>>();
|
|
// 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, tex_db.tpage_names.at(tex.page), true));
|
|
processed_textures.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::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
|