jak3: custom level support (#3522)

pull/3524/head
Hat Kid 2024-05-16 21:15:54 +02:00 committed by GitHub
parent c12a5d777c
commit a61f24a168
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 2465 additions and 7 deletions

View File

@ -0,0 +1,90 @@
{
// The "in-game" name of the level. Should be lower case, with dashes (GOAL symbol name)
// the name of this file, and the folder this file is in must have the same name.
"long_name": "test-zone",
// The file name, should be upper case and 8 characters or less.
"iso_name": "TESTZONE",
// The nickname, should be exactly 3 characters
"nickname": "tsz", // 3 char name, all lowercase
// Background mesh file.
// Must have vertex colors. Use the blender cycles renderer, bake, diffuse, uncheck color,
// and bake to vertex colors. For now, only the first vertex color group is used, so make sure you
// only have 1.
"gltf_file": "custom_levels/jak3/test-zone/test-zone2.glb",
// automatically set wall vs. ground based on angle. Useful if you don't want to assign this yourself
"automatic_wall_detection": true,
"automatic_wall_angle": 45.0,
// if your mesh has triangles with incorrect orientation, set this to make all collision mesh triangles double sided
// this makes collision 2x slower and bigger, so only use if really needed
"double_sided_collide": false,
// available res-lump tag types:
// integer types: int32, uint32, enum-int32, enum-uint32
// float types: float, meters (1 meter = 4096.0 units), degrees (65536.0 = 360°)
// vector types: vector (normal floats), vector4m (meters), vector3m (meters with w set to 1.0), vector-vol (normal floats with w in meters), movie-pos (meters with w in degrees)
// special types: symbol, type, string, eco-info, cell-info, buzzer-info, water-height
//
// examples:
//
// adds a float tag 'spring-height' with value of 200 meters (1 meter = 4096.0 units):
// "spring-height": ["meters", 200.0]
//
// adds a degrees tag 'rotoffset':
// "rotoffset": ["degrees", -45.0]
//
// adds a movie-pos tag:
// "movie-pos": ["movie-pos", [100.22, -25.3, 99.5, 180.0]]
//
// adds an enum tag 'options':
// "options": ["enum-int32", "(fact-options large)"]
//
// adds a water-height tag:
// "water-height": ["water-height", 25.0, 0.5, 2.0, "(water-flags can-swim can-wade)"]
//
// adds an eco-info tag:
// "eco-info": ["eco-info", "(pickup-type health)", 2]
//
// adds a 'type' tag (using the "symbol" and "string" lump types works the same way):
// "spawn-types": ["type", "spyder", "juicer"]
// The base actor id for your custom level. If you have multiple levels, this should be unique!
"base_id": 100,
// All art groups you want to use in your custom level. Will add their models and corresponding textures to the FR3 file.
// "art_groups": [],
// Any textures you want to include in your custom level.
// This is mainly useful for textures which are not in the common level files and have no art group associated with them.
// To get a list of all the textures, you can extract all of the game's textures
// by setting "save_texture_pngs" to true in the decompiler config.
// "textures": [],
"actors" : [
{
"trans": [-15.2818, 15.2461, 17.1360], // translation
"etype": "crate", // actor type
"game_task": "(game-task none)", // associated game task (for powercells, etc)
"kill_mask": 0,
"quat": [0, 0, 0, 1], // quaternion
"bsphere": [-15.2818, 15.2461, 17.1360, 10], // bounding sphere
"lump": {
"name": "test-crate",
"eco-info": ["eco-info", "(pickup-type gem)", 1]
}
},
{
"trans": [-5.4630, 17.4553, 1.6169], // translation
"etype": "eco-yellow", // actor type
"game_task": "(game-task none)", // associated game task (for powercells, etc)
"kill_mask": 0,
"quat": [0, 0, 0, 1], // quaternion
"bsphere": [-5.4630, 17.4553, 1.6169, 10], // bounding sphere
"lump": {
"name": "test-eco"
}
}
]
}

Binary file not shown.

View File

@ -0,0 +1,8 @@
;; DGO definition file for Awful Village level
;; We use the convention of having a longer DGO name for levels without precomputed visibility.
;; the actual file name still needs to be 8.3
("TSZ.DGO"
("test-zone.go"
)
)

View File

@ -2306,10 +2306,32 @@
(dotimes (s1-1 s2-3)
(set! sv-48 (-> s3-4 data s1-1))
(cond
((and (is-object-visible? s4-1 (-> sv-48 vis-id))
;; og:preserve-this entity blacklist
((and (#if PC_PORT (or (with-pc (and (not (-> *pc-settings* ps2-actor-vis?))
;; ban specific entities
; (not (let ((name (res-lump-struct (-> sv-48 entity) 'name string)))
; (nmember name '("fort-entry-gate-11"
; "fort-elec-gate-11"
; "fort-elec-gate-12"
; "com-airlock-outer-13"
; "com-airlock-inner-41"
; "under-lift-4"
; "under-locking-1"
; "under-locking-2"))))
))
(is-object-visible? s4-1 (-> sv-48 vis-id)))
(is-object-visible? s4-1 (-> sv-48 vis-id)))
(not (logtest? (-> sv-48 perm status) (entity-perm-status bit-9 bit-10)))
(not (logtest? (-> sv-48 kill-mask) sv-32))
(or (-> s4-1 vis-info 0) (< (vector-vector-distance (-> sv-48 trans) sv-16) (-> sv-48 vis-dist)))
;; og:preserve-this PC port note: added this extra check to fix level types being used after deload because of bad entity placement
(#if PC_PORT
(or (not (type-type? (-> sv-48 entity type) entity-actor)) (and (valid? (-> (the-as entity-actor (-> sv-48 entity)) etype) type "entity-type-check etype" #f *stdcon*)
(valid? (-> (the-as entity-actor (-> sv-48 entity)) etype symbol) symbol "entity-type-check symbol" #f *stdcon*)
(valid? (-> (the-as entity-actor (-> sv-48 entity)) etype symbol value) type "entity-type-check value" #f *stdcon*)
(= (-> (the-as entity-actor (-> sv-48 entity)) etype) (-> (the-as entity-actor (-> sv-48 entity)) etype symbol value))
))
#f)
)
(when (not (or (-> sv-48 process) (logtest? (-> sv-48 perm status) (entity-perm-status bit-0 dead)) s0-0))
(birth! (-> sv-48 entity))

View File

@ -20658,3 +20658,60 @@
4amy
)
)
;; og:preserve-this added test-zone level
(define test-zone
(new 'static 'level-load-info
:name 'test-zone
:visname 'test-zone-vis
:nickname 'tsz
:dbname 'test-zone
:taskname 'default
:index #x128
:master-level #f
:level-flags (level-flags lf8 lf9 lf11 lf12)
:packages '()
:run-packages '("common")
:memory-mode (level-memory-mode large)
:music-bank #f
:extra-sound-bank #f
:mood-func 'update-mood-default
:special-mood #f
:ocean #f
:ocean-alpha 1.0
:priority 100
:draw-priority 10.0
:base-task-mask (task-mask task0)
:bigmap-id (bigmap-id no-map)
:continues '((new 'static 'continue-point
:name "test-zone-start"
:level 'test-zone
:trans (static-vectorm 0 10 10)
:camera-trans (static-vectorm 0 1 0)
:quat (new 'static 'vector4h :data (new 'static 'array int16 4 0 #x7f9f 0 -2460))
:camera-rot (new 'static 'array int16 9 -32717 0 -1749 #xd1 #x7f0c -3954 #x6c8 -3958 -32478)
:on-goto #f
:vis-nick 'tsz
:vehicle-type #x1b
:want-count 3
:want (new 'static 'inline-array level-buffer-state-small 3
(new 'static 'level-buffer-state-small :name 'wasall :display? 'display)
(new 'static 'level-buffer-state-small :name 'waswide :display? 'display)
(new 'static 'level-buffer-state-small :name 'test-zone :display? 'display)
)
:want-sound (new 'static 'array symbol 3 #f #f #f)
)
)
:callback-list '()
:borrow #f
:bottom-height (meters -150)
:fog-height (meters 80)
:max-rain 1.0
:fog-mult 1.0
:mood-range (new 'static 'mood-range :max-cloud 1.0 :min-fog 0.5 :max-fog 1.0)
)
)
(#when PC_PORT
(cons! *level-load-list* 'test-zone)
)

View File

@ -760,7 +760,8 @@
)
)
(let ((s5-5 (level-get *level* (-> arg0 level))))
(when s5-5
;; og:preserve-this don't wait for vis if level doesn't have it
(when (and s5-5 (-> s5-5 vis-info 0))
(while (and (-> *level* vis?) (-> s5-5 vis-info 0) (= (-> s5-5 all-visible?) 'loading))
(suspend)
)

View File

@ -394,6 +394,17 @@
(cgo-file "lwlandm.gd" common-dep)
(cgo-file "lwstdpck.gd" common-dep)
;;;;;;;;;;;;;;;;;;;;;;;;;
;; Example Custom Level
;;;;;;;;;;;;;;;;;;;;;;;;;
;; Set up the build system to build the level geometry
;; this path is relative to the custom_levels/jak3 folder
;; it should point to the .jsonc file that specifies the level.
(build-custom-level "test-zone")
;; the DGO file
(custom-level-cgo "TSZ.DGO" "test-zone/testzone.gd")
;;;;;;;;;;;;;;;;;;;;;
;; ANIMATIONS
;;;;;;;;;;;;;;;;;;;;;

View File

@ -235,3 +235,20 @@
(copy-objs ,@objs)))
)
))
(defun custom-level-cgo (output-name desc-file-name)
"Add a CGO with the given output name (in $OUT/iso) and input name (in custom_levels/jak3/)"
(let ((out-name (string-append "$OUT/iso/" output-name)))
(defstep :in (string-append "custom_levels/jak3/" desc-file-name)
:tool 'dgo
:out `(,out-name)
)
(set! *all-cgos* (cons out-name *all-cgos*))
)
)
(defmacro build-custom-level (name)
(let* ((path (string-append "custom_levels/jak3/" name "/" name ".jsonc")))
`(defstep :in ,path
:tool 'build-level3
:out '(,(string-append "$OUT/obj/" name ".go")))))

View File

@ -8,20 +8,25 @@ add_library(compiler
build_level/common/build_level.cpp
build_level/jak1/build_level.cpp
build_level/jak2/build_level.cpp
build_level/jak3/build_level.cpp
build_level/collide/jak1/collide_bvh.cpp
build_level/collide/jak1/collide_drawable.cpp
build_level/collide/jak1/collide_pack.cpp
build_level/collide/jak2/collide.cpp
build_level/collide/jak3/collide.cpp
build_level/common/color_quantization.cpp
build_level/common/Entity.cpp
build_level/jak1/Entity.cpp
build_level/jak2/Entity.cpp
build_level/jak3/Entity.cpp
build_level/common/FileInfo.cpp
build_level/jak1/FileInfo.cpp
build_level/jak2/FileInfo.cpp
build_level/jak3/FileInfo.cpp
build_level/common/gltf_mesh_extract.cpp
build_level/jak1/LevelFile.cpp
build_level/jak2/LevelFile.cpp
build_level/jak3/LevelFile.cpp
build_level/common/ResLump.cpp
build_level/common/Tfrag.cpp
build_level/jak1/ambient.cpp

View File

@ -312,3 +312,222 @@ struct CollideFace {
PatSurface pat;
};
} // namespace jak2
namespace jak3 {
struct PatSurface {
enum class Mode { GROUND = 0, WALL = 1, OBSTACLE = 2, HALFPIPE = 3, MAX_MODE = 4 };
enum class Material {
NONE = 0,
ICE = 1,
QUICKSAND = 2,
WATERBOTTOM = 3,
TAR = 4,
SAND = 5,
WOOD = 6,
GRASS = 7,
PCMETAL = 8,
SNOW = 9,
DEEPSNOW = 10,
HOTCOALS = 11,
LAVA = 12,
CRWOOD = 13,
GRAVEL = 14,
DIRT = 15,
METAL = 16,
STRAW = 17,
TUBE = 18,
SWAMP = 19,
STOPPROJ = 20,
ROTATE = 21,
NEUTRAL = 22,
STONE = 23,
CRMETAL = 24,
CARPET = 25,
GRMETAL = 26,
SHMETAL = 27,
HDWOOD = 28,
SQUISH = 29,
MHSHROOM = 30,
FOREST = 31,
MHSWAMP = 32,
DMAKER = 33,
MAX_MATERIAL = 34
};
enum class Event {
NONE = 0,
DEADLY = 1,
ENDLESSFALL = 2,
BURN = 3,
DEADLYUP = 4,
BURNUP = 5,
MELT = 6,
SLIDE = 7,
LIP = 8,
LIPRAMP = 9,
SHOCK = 10,
SHOCKUP = 11,
HIDE = 12,
RAIL = 13,
SLIPPERY = 14,
DRAG = 15,
WATERFLOOR = 16,
HANG = 17,
FRY = 18,
SLIME = 19,
MAX_EVENT = 20
};
void set_noentity(bool x) {
if (x) {
val |= (1 << 0);
} else {
val &= ~(1 << 0);
}
}
bool get_noentity() const { return val & (1 << 0); }
void set_nocamera(bool x) {
if (x) {
val |= (1 << 1);
} else {
val &= ~(1 << 1);
}
}
bool get_nocamera() const { return val & (1 << 1); }
void set_noedge(bool x) {
if (x) {
val |= (1 << 2);
} else {
val &= ~(1 << 2);
}
}
bool get_noedge() const { return val & (1 << 2); }
void set_nogrind(bool x) {
if (x) {
val |= (1 << 3);
} else {
val &= ~(1 << 3);
}
}
bool get_nogrind() const { return val & (1 << 3); }
void set_nojak(bool x) {
if (x) {
val |= (1 << 4);
} else {
val &= ~(1 << 4);
}
}
bool get_nojak() const { return val & (1 << 4); }
void set_noboard(bool x) {
if (x) {
val |= (1 << 5);
} else {
val &= ~(1 << 5);
}
}
bool get_noboard() const { return val & (1 << 5); }
void set_nopilot(bool x) {
if (x) {
val |= (1 << 6);
} else {
val &= ~(1 << 6);
}
}
bool get_nopilot() const { return val & (1 << 6); }
void set_mode(Mode mode) {
val &= ~(0b111 << 7);
val |= ((u32)mode << 7);
}
Mode get_mode() const { return (Mode)(0b111 & (val >> 7)); }
void set_material(Material mat) {
val &= ~(0b111111 << 10);
val |= ((u32)mat << 10);
}
Material get_material() const { return (Material)(0b111111 & (val >> 10)); }
void set_nolineofsight(bool x) {
if (x) {
val |= (1 << 16);
} else {
val &= ~(1 << 16);
}
}
bool get_nolineofsight() const { return val & (1 << 16); }
void set_event(Event ev) {
val &= ~(0b111111 << 18);
val |= ((u32)ev << 18);
}
Event get_event() const { return (Event)(0b111111 & (val >> 18)); }
void set_probe(bool x) {
if (x) {
val |= (1 << 24);
} else {
val &= ~(1 << 24);
}
}
bool get_probe() const { return val & (1 << 24); }
void set_nomech(bool x) {
if (x) {
val |= (1 << 25);
} else {
val &= ~(1 << 25);
}
}
bool get_nomech() const { return val & (1 << 25); }
void set_noproj(bool x) {
if (x) {
val |= (1 << 26);
} else {
val &= ~(1 << 26);
}
}
bool get_noproj() const { return val & (1 << 26); }
void set_noendlessfall(bool x) {
if (x) {
val |= (1 << 27);
} else {
val &= ~(1 << 27);
}
}
bool get_noendlessfall() const { return val & (1 << 27); }
void set_noprobe(bool x) {
if (x) {
val |= (1 << 28);
} else {
val &= ~(1 << 28);
}
}
bool get_noprobe() const { return val & (1 << 28); }
void set_board(bool x) {
if (x) {
val |= (1 << 4);
} else {
val &= ~(1 << 4);
}
}
bool get_board() const { return val & (1 << 4); }
bool operator==(const PatSurface& other) const { return val == other.val; }
u32 val = 0;
};
struct CollideFace {
math::Vector3f v[3];
PatSurface pat;
};
} // namespace jak3

View File

@ -10,6 +10,7 @@
#include "goalc/data_compiler/DataObjectGenerator.h"
namespace jak2 {
/*!
* An axis-aligned bounding box
*/
@ -1156,3 +1157,4 @@ size_t add_to_object_file(const CollideHash& hash, DataObjectGenerator& gen) {
return result;
}
} // namespace jak2

View File

@ -7,6 +7,8 @@
#include "goalc/build_level/collide/common/collide_common.h"
class DataObjectGenerator;
namespace jak2 {
// High-level collision system idea:
// Each level has a single collide-hash object storing all collision data.
// The mesh is divided into "fragments". Each fragment is made up of triangles.
@ -44,7 +46,7 @@ struct CollideBucket {
};
struct CollideFragment {
std::vector<jak2::PatSurface> pat_array;
std::vector<PatSurface> pat_array;
// per-cell references to the index list
std::vector<CollideBucket> buckets;
@ -115,8 +117,7 @@ struct CollideHash {
};
CollideHash construct_collide_hash(const std::vector<jak1::CollideFace>& tris);
CollideHash construct_collide_hash(const std::vector<jak2::CollideFace>& tris);
class DataObjectGenerator;
CollideHash construct_collide_hash(const std::vector<CollideFace>& tris);
size_t add_to_object_file(const CollideHash& hash, DataObjectGenerator& gen);
} // namespace jak2

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,123 @@
#pragma once
#include <vector>
#include "common/common_types.h"
#include "common/math/Vector.h"
#include "goalc/build_level/collide/common/collide_common.h"
class DataObjectGenerator;
namespace jak3 {
// High-level collision system idea:
// Each level has a single collide-hash object storing all collision data.
// The mesh is divided into "fragments". Each fragment is made up of triangles.
// There's a two-level lookup: if you want to find all triangles in a box, you must first find all
// the fragments that intersect the box, then find all the triangles in those fragments that
// intersect the box.
// Each fragment has a bounding box. All triangles inside that fragment fit inside the bounding box.
/*!
* Vertex in the collide mesh. This is stored as an offset from the bottom corner of the bounding
* box. This is scaled by 16. (a "1" stored here means a distance of 16.f, or 16/4096 of in-game
* meter.)
*/
struct CollideFragmentVertex {
u16 position[3];
};
/*!
* Polygon in the collide mesh. This is a reference to three vertices in the vertex array, and a
* "pat" (polygon attributes?) in the pat array.
*/
struct CollideFragmentPoly {
u8 vertex_index[3];
u8 pat_index;
};
/*!
* The Collide Fragment is divided into a 3D grid. Each cell in the grid has a "bucket" which
* collects a list of all polygons that intersect the cell. The bucket stores a reference to values
* in the index list, which are polygon indices.
*/
struct CollideBucket {
s16 index;
s16 count;
};
struct CollideFragment {
std::vector<jak3::PatSurface> pat_array;
// per-cell references to the index list
std::vector<CollideBucket> buckets;
// references to polygons
std::vector<u8> index_array;
// references to vertices/pats
std::vector<CollideFragmentPoly> poly_array;
std::vector<CollideFragmentVertex> vert_array;
// others
// the x/y/z sizes of a grid cell
math::Vector3f grid_step;
// inverse of grid step
math::Vector3f axis_scale;
// the corners of our bounding box
math::Vector3f bbox_min_corner;
math::Vector3f bbox_max_corner;
math::Vector4f bsphere;
math::Vector<s32, 3> bbox_min_corner_i;
math::Vector<s32, 3> bbox_max_corner_i;
// the number of cells in the grid along the x/y/z axis
u32 dimension_array[3] = {0, 0, 0};
};
/*
((num-ids uint16 :offset 4)
(id-count uint16 :offset 6)
(num-buckets uint32 :offset 8)
(qwc-id-bits uint32 :offset 12)
(grid-step vector :inline :offset 16)
(bbox bounding-box :inline :offset-assert 32)
(bbox4w bounding-box4w :inline :offset-assert 64)
(axis-scale vector :inline :offset 48)
(avg-extents vector :inline :offset 64)
(bucket-array uint32 :offset 44)
(item-array (inline-array collide-hash-item) :offset 60 :score 1)
(dimension-array uint32 3 :offset 76) ;; ?
(num-items uint32 :offset 92)
*/
struct CollideHash {
// if you have a bit for each ID in the item list, how many quadwords (128-byte word) is it?
u32 qwc_id_bits = 0;
// this is similar to the use in CollideHashFragment, but this points to entries in the .
std::vector<CollideBucket> buckets;
// buckets point to this array, which points to the fragments below
std::vector<u32> index_array;
// the actual fragments
std::vector<CollideFragment> fragments;
// all these have the same meaning as in CollideFragment and define the grid.
math::Vector3f grid_step;
math::Vector3f axis_scale;
math::Vector3f bbox_min_corner;
math::Vector<s32, 3> bbox_min_corner_i;
math::Vector<s32, 3> bbox_max_corner_i;
u32 dimension_array[3] = {0, 0, 0};
};
CollideHash construct_collide_hash(const std::vector<jak1::CollideFace>& tris);
CollideHash construct_collide_hash(const std::vector<jak3::CollideFace>& tris);
size_t add_to_object_file(const CollideHash& hash, DataObjectGenerator& gen);
} // namespace jak3

View File

@ -0,0 +1,118 @@
#include "Entity.h"
namespace jak3 {
size_t EntityActor::generate(DataObjectGenerator& gen) const {
size_t result = res_lump.generate_header(gen, "entity-actor");
for (int i = 0; i < 4; i++) {
gen.add_word_float(trans[i]);
}
gen.add_word(aid);
gen.add_word(kill_mask);
gen.add_type_tag(etype);
ASSERT(game_task < UINT16_MAX);
ASSERT(vis_id < UINT16_MAX);
u32 packed = (game_task) | (vis_id << 16);
gen.add_word(packed);
for (int i = 0; i < 4; i++) {
gen.add_word_float(quat[i]);
}
res_lump.generate_tag_list_and_data(gen, result);
return result;
}
size_t generate_drawable_actor(DataObjectGenerator& gen,
const EntityActor& actor,
size_t actor_loc) {
gen.align_to_basic();
gen.add_type_tag("drawable-actor"); // 0
size_t result = gen.current_offset_bytes();
gen.add_word(actor.vis_id); // 4
gen.link_word_to_byte(gen.add_word(0), actor_loc); // 8
gen.add_word(0); // 12
for (int i = 0; i < 4; i++) {
gen.add_word_float(actor.bsphere[i]); // 16, 20, 24, 28
}
return result;
}
size_t generate_inline_array_actors(DataObjectGenerator& gen,
const std::vector<EntityActor>& actors) {
std::vector<size_t> actor_locs;
for (auto& actor : actors) {
actor_locs.push_back(actor.generate(gen));
}
gen.align_to_basic();
gen.add_type_tag("drawable-inline-array-actor"); // 0
size_t result = gen.current_offset_bytes();
ASSERT(actors.size() < UINT16_MAX);
gen.add_word(actors.size() << 16); // 4
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
ASSERT((gen.current_offset_bytes() % 16) == 0);
for (size_t i = 0; i < actors.size(); i++) {
generate_drawable_actor(gen, actors[i], actor_locs[i]);
}
return result;
}
void add_actors_from_json(const nlohmann::json& json,
std::vector<EntityActor>& actor_list,
u32 base_aid,
decompiler::DecompilerTypeSystem& dts) {
for (const auto& actor_json : json) {
auto& actor = actor_list.emplace_back();
actor.aid = actor_json.value("aid", base_aid + actor_list.size());
actor.trans = vectorm3_from_json(actor_json.at("trans"));
actor.etype = actor_json.at("etype").get<std::string>();
if (actor_json.contains("kill_mask") && actor_json.at("kill_mask").is_string()) {
actor.kill_mask = get_enum_val(actor_json.at("kill_mask").get<std::string>(), dts);
} else {
actor.kill_mask = actor_json.value("kill_mask", 0);
}
if (actor_json.contains("game_task") && actor_json.at("game_task").is_string()) {
actor.game_task = get_enum_val(actor_json.at("game_task").get<std::string>(), dts);
} else {
actor.game_task = actor_json.value("game_task", 0);
}
actor.vis_id = actor_json.value("vis_id", 0);
actor.quat = math::Vector4f(0, 0, 0, 1);
if (actor_json.find("quat") != actor_json.end()) {
actor.quat = vector_from_json(actor_json.at("quat"));
}
actor.bsphere = vectorm4_from_json(actor_json.at("bsphere"));
if (actor_json.find("lump") != actor_json.end()) {
for (auto [key, value] : actor_json.at("lump").items()) {
if (value.is_string()) {
std::string value_string = value.get<std::string>();
if (value_string.size() > 0 && value_string[0] == '\'') {
actor.res_lump.add_res(
std::make_unique<ResSymbol>(key, value_string.substr(1), -1000000000.0000));
} else {
actor.res_lump.add_res(
std::make_unique<ResString>(key, value_string, -1000000000.0000));
}
continue;
}
if (value.is_array()) {
actor.res_lump.add_res(res_from_json_array(key, value, dts));
}
}
}
actor.res_lump.sort_res();
}
}
} // namespace jak3

View File

@ -0,0 +1,37 @@
#pragma once
#include "goalc/build_level/common/Entity.h"
namespace jak3 {
/*
* (trans vector :inline :offset-assert 32)
* (aid uint32 :offset-assert 48)
* (kill-mask task-mask :offset-assert 52)
* (etype type :offset-assert 56) ;; probably type
* (task game-task :offset-assert 60)
* (vis-id uint16 :offset-assert 62)
* (quat quaternion :inline :offset-assert 64)
*/
struct EntityActor {
ResLump res_lump;
math::Vector4f trans; // w = 1 here
u32 aid = 0;
u32 kill_mask = 0;
std::string etype;
u32 game_task = 0;
u32 vis_id = 0;
math::Vector4f quat;
math::Vector4f bsphere;
size_t generate(DataObjectGenerator& gen) const;
};
size_t generate_inline_array_actors(DataObjectGenerator& gen,
const std::vector<EntityActor>& actors);
void add_actors_from_json(const nlohmann::json& json,
std::vector<EntityActor>& actor_list,
u32 base_aid,
decompiler::DecompilerTypeSystem& dts);
} // namespace jak3

View File

@ -0,0 +1,19 @@
#include "FileInfo.h"
#include "common/versions/versions.h"
#include "goalc/data_compiler/DataObjectGenerator.h"
namespace jak3 {
FileInfo make_file_info_for_level(const std::string& file_name) {
FileInfo info;
info.file_type = "bsp-header";
info.file_name = file_name;
info.major_version = versions::jak3::LEVEL_FILE_VERSION;
info.minor_version = 0;
info.maya_file_name = "Unknown";
info.tool_debug = "Created by OpenGOAL buildlevel";
info.mdb_file_name = "Unknown";
return info;
}
} // namespace jak3

View File

@ -0,0 +1,7 @@
#pragma once
#include "goalc/build_level/common/FileInfo.h"
namespace jak3 {
FileInfo make_file_info_for_level(const std::string& file_name);
} // namespace jak3

View File

@ -0,0 +1,141 @@
#include "LevelFile.h"
#include "goalc/data_compiler/DataObjectGenerator.h"
namespace jak3 {
size_t DrawableTreeArray::add_to_object_file(DataObjectGenerator& gen) const {
/*
(deftype drawable-tree-array (drawable-group)
((trees drawable-tree 1 :offset 32 :score 100))
:flag-assert #x1200000024
)
(deftype drawable-group (drawable)
((length int16 :offset 6)
(data drawable 1 :offset-assert 32)
)
(:methods
(new (symbol type int) _type_)
)
:flag-assert #x1200000024
)
*/
gen.align_to_basic();
gen.add_type_tag("drawable-tree-array");
size_t result = gen.current_offset_bytes();
int num_trees = 0;
num_trees += tfrags.size();
gen.add_word(num_trees << 16);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
// todo add trees...
if (num_trees == 0) {
gen.add_word(0); // the one at the end.
} else {
int tree_word = (int)gen.current_offset_bytes() / 4;
for (int i = 0; i < num_trees; i++) {
gen.add_word(0);
}
for (auto& tfrag : tfrags) {
// gen.set_word(tree_word++, tfrag.add_to_object_file(gen));
gen.link_word_to_byte(tree_word++, tfrag.add_to_object_file(gen));
}
}
return result;
}
size_t generate_u32_array(const std::vector<u32>& array, DataObjectGenerator& gen) {
gen.align(4);
size_t result = gen.current_offset_bytes();
for (auto& entry : array) {
gen.add_word(entry);
}
return result;
}
std::vector<u8> LevelFile::save_object_file() const {
DataObjectGenerator gen;
gen.add_type_tag("bsp-header");
// add blank space for the bsp-header
while (gen.words() < 100) {
gen.add_word(0);
}
//(info file-info :offset 4)
auto file_info_slot = info.add_to_object_file(gen);
gen.link_word_to_byte(1, file_info_slot);
//(bsphere vector :inline :offset-assert 16)
//(all-visible-list (pointer uint8) :offset-assert 32)
//(visible-list-length int32 :offset-assert 36)
//(drawable-trees drawable-tree-array :offset-assert 40)
gen.link_word_to_byte(40 / 4, drawable_trees.add_to_object_file(gen));
//(pat pointer :offset-assert 44)
//(pat-length int32 :offset-assert 48)
//(texture-remap-table (pointer uint64) :offset-assert 52)
//(texture-remap-table-len int32 :offset-assert 56)
//(texture-ids (pointer texture-id) :offset-assert 60)
//(texture-page-count int32 :offset-assert 64)
//(unknown-basic basic :offset-assert 68)
//(name symbol :offset-assert 72)
gen.link_word_to_symbol(name, 72 / 4);
//(nickname symbol :offset-assert 76)
gen.link_word_to_symbol(nickname, 76 / 4);
//(vis-info level-vis-info 8 :offset-assert 80)
//(actors drawable-inline-array-actor :offset-assert 112)
gen.link_word_to_byte(112 / 4, generate_inline_array_actors(gen, actors));
//(cameras (array entity-camera) :offset-assert 116)
//(nodes (inline-array bsp-node) :offset-assert 120)
//(level level :offset-assert 124)
//(current-leaf-idx uint16 :offset-assert 128)
//(cam-outside-bsp uint8 :offset 152)
//(cam-using-back uint8 :offset-assert 153)
//(cam-box-idx uint16 :offset-assert 154)
//(ambients symbol :offset-assert 156)
//(subdivide-close float :offset-assert 160)
//(subdivide-far float :offset-assert 160)
//(race-meshes (array entity-race-mesh) :offset-assert 168)
//(actor-birth-order (pointer uint32) :offset-assert 172)
gen.link_word_to_byte(172 / 4, generate_u32_array(actor_birth_order, gen));
//(light-hash light-hash :offset-assert 176)
//(nav-meshes (array entity-nav-mesh) :offset-assert 180)
//(actor-groups (array actor-group) :offset-assert 184)
//(region-trees (array drawable-tree-region-prim) :offset-assert 188)
//(region-array region-array :offset-assert 192)
//(collide-hash collide-hash :offset-assert 196)
gen.link_word_to_byte(196 / 4, add_to_object_file(collide_hash, gen));
//(wind-array uint32 :offset 200)
//(wind-array-length int32 :offset 204)
//(city-level-info city-level-info :offset 208)
//(vis-spheres vector-array :offset 216)
//(vis-spheres-length uint32 :offset 248)
//(region-tree drawable-tree-region-prim :offset 252)
//(tfrag-masks texture-masks-array :offset-assert 256)
//(tfrag-closest (pointer float) :offset-assert 260)
//(tfrag-mask-count uint32 :offset 260)
//(shrub-masks texture-masks-array :offset-assert 264)
//(shrub-closest (pointer float) :offset-assert 268)
//(shrub-mask-count uint32 :offset 268)
//(alpha-masks texture-masks-array :offset-assert 272)
//(alpha-closest (pointer float) :offset-assert 276)
//(alpha-mask-count uint32 :offset 276)
//(water-masks texture-masks-array :offset-assert 280)
//(water-closest (pointer float) :offset-assert 284)
//(water-mask-count uint32 :offset 284)
//(bsp-scale vector :inline :offset-assert 288)
//(bsp-offset vector :inline :offset-assert 304)
//(hfrag-drawable drawable :offset 320)
//(end uint8 :offset 399)
return gen.generate_v2();
}
} // namespace jak3

View File

@ -0,0 +1,182 @@
#pragma once
#include <array>
#include <string>
#include <vector>
#include "common/common_types.h"
#include "goalc/build_level/collide/common/collide_common.h"
#include "goalc/build_level/collide/jak3/collide.h"
#include "goalc/build_level/common/Tfrag.h"
#include "goalc/build_level/jak3/Entity.h"
#include "goalc/build_level/jak3/FileInfo.h"
namespace jak3 {
struct VisibilityString {
std::vector<u8> bytes;
};
struct DrawableTreeInstanceTie {};
struct DrawableTreeActor {};
struct DrawableTreeInstanceShrub {};
struct DrawableTreeRegionPrim {};
struct DrawableTreeArray {
std::vector<DrawableTreeTfrag> tfrags;
std::vector<DrawableTreeInstanceTie> ties;
std::vector<DrawableTreeActor> actors; // unused?
std::vector<DrawableTreeRegionPrim> regions;
std::vector<DrawableTreeInstanceShrub> shrubs;
size_t add_to_object_file(DataObjectGenerator& gen) const;
};
struct TextureRemap {};
struct TextureId {};
struct VisInfo {};
struct EntityCamera {};
struct BspNode {};
struct RaceMesh {};
struct LightHash {};
struct EntityNavMesh {};
struct ActorGroup {};
struct RegionTree {};
struct RegionArray {};
struct CityLevelInfo {};
struct TextureMasksArray {};
// This is a place to collect all the data that should go into the bsp-header file.
struct LevelFile {
// (info file-info :offset 4)
FileInfo info;
// (all-visible-list (pointer uint16) :offset-assert 32)
// (visible-list-length int16 :offset-assert 36)
// (extra-vis-list-length int16 :offset-assert 38)
VisibilityString all_visibile_list;
// (drawable-trees drawable-tree-array :offset-assert 40)
DrawableTreeArray drawable_trees;
// (pat pointer :offset-assert 44)
// (pat-length int32 :offset-assert 48)
std::vector<PatSurface> pat;
// (texture-remap-table (pointer uint64) :offset-assert 52)
// (texture-remap-table-len int32 :offset-assert 56)
std::vector<TextureRemap> texture_remap_table;
// (texture-ids (pointer texture-id) :offset-assert 60)
// (texture-page-count int32 :offset-assert 64)
std::vector<TextureId> texture_ids;
// (unknown-basic basic :offset-assert 68)
// "misc", seems like it can be zero and is unused.
// (name symbol :offset-assert 72)
std::string name; // full name
// (nickname symbol :offset-assert 76)
std::string nickname; // 3 char name
// (vis-info level-vis-info 8 :offset-assert 80) ;; note: 0 when
std::array<VisInfo, 8> vis_infos;
// (actors drawable-inline-array-actor :offset-assert 112)
std::vector<EntityActor> actors;
// (cameras (array entity-camera) :offset-assert 116)
std::vector<EntityCamera> cameras;
// (nodes (inline-array bsp-node) :offset-assert 120)
std::vector<BspNode> nodes;
// (level level :offset-assert 124)
// zero
// (current-leaf-idx uint16 :offset-assert 128)
// zero
// (texture-flags texture-page-flag 10 :offset-assert 130)
std::vector<u16> texture_flags;
// (cam-outside-bsp uint8 :offset 152)
// (cam-using-back uint8 :offset-assert 153)
// (cam-box-idx uint16 :offset-assert 154)
// zero
// (ambients symbol :offset-assert 156)
// #t
// (subdivide-close float :offset-assert 160)
float close_subdiv = 0;
// (subdivide-far float :offset-assert 164)
float far_subdiv = 0;
// (race-meshes (array entity-race-mesh) :offset-assert 168)
std::vector<RaceMesh> race_meshes;
// (actor-birth-order (pointer uint32) :offset-assert 172)
std::vector<u32> actor_birth_order;
// (light-hash light-hash :offset-assert 176)
LightHash light_hash;
// (nav-meshes (array entity-nav-mesh) :offset-assert 180)
std::vector<EntityNavMesh> entity_nav_meshes;
// (actor-groups (array actor-group) :offset-assert 184)
std::vector<ActorGroup> actor_groups;
// (region-trees (array drawable-tree-region-prim) :offset-assert 188)
std::vector<RegionTree> region_trees;
// (region-array region-array :offset-assert 192)
RegionArray region_array;
// (collide-hash collide-hash :offset-assert 196)
CollideHash collide_hash;
// (wind-array uint32 :offset 200)
std::vector<u32> wind_array;
// (wind-array-length int32 :offset 204)
s32 wind_array_length = 0;
// (city-level-info city-level-info :offset 208)
CityLevelInfo city_level_info;
// (vis-spheres vector-array :offset 216)
// (vis-spheres-length uint32 :offset 248)
// (region-tree drawable-tree-region-prim :offset 252)
RegionTree region_tree;
// (tfrag-masks texture-masks-array :offset-assert 256)
// (tfrag-closest (pointer float) :offset-assert 260)
// (tfrag-mask-count uint32 :offset 260)
TextureMasksArray tfrag_masks;
// (shrub-masks texture-masks-array :offset-assert 264)
// (shrub-closest (pointer float) :offset-assert 268)
// (shrub-mask-count uint32 :offset 268)
TextureMasksArray shrub_masks;
// (alpha-masks texture-masks-array :offset-assert 272)
// (alpha-closest (pointer float) :offset-assert 276)
// (alpha-mask-count uint32 :offset 276)
TextureMasksArray alpha_masks;
// (water-masks texture-masks-array :offset-assert 280)
// (water-closest (pointer float) :offset-assert 284)
// (water-mask-count uint32 :offset 284)
TextureMasksArray water_masks;
// (bsp-scale vector :inline :offset-assert 288)
// (bsp-offset vector :inline :offset-assert 304)
std::vector<u8> save_object_file() const;
};
} // namespace jak3

View File

@ -0,0 +1,197 @@
#include "build_level.h"
#include "decompiler/extractor/extractor_util.h"
#include "decompiler/level_extractor/extract_merc.h"
#include "goalc/build_level/collide/jak3/collide.h"
#include "goalc/build_level/common/Tfrag.h"
#include "goalc/build_level/jak3/Entity.h"
#include "goalc/build_level/jak3/FileInfo.h"
#include "goalc/build_level/jak3/LevelFile.h"
namespace jak3 {
bool run_build_level(const std::string& input_file,
const std::string& bsp_output_file,
const std::string& output_prefix) {
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
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::Jak3);
dts.parse_enum_defs({"decompiler", "config", "jak3", "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
auto& tfrag_drawable_tree = file.drawable_trees.tfrags.emplace_back();
tfrag_from_gltf(mesh_extract_out.tfrag, tfrag_drawable_tree,
pc_level.tfrag_trees[0].emplace_back());
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 ((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::Jak3);
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/jak3/jak3_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/jak3/textures";
file_util::create_dir_if_needed(textures_out);
db.process_tpages(tex_db, textures_out, config, "");
// find all art groups used by the custom level in other dgos
if (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::extract_merc(ag_file, tex_db, db.dts, tex_remap, pc_level, false,
db.version());
}
}
}
}
// 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);
}
}
}
}
}
// Save the PC level
save_pc_data(file.name, pc_level,
file_util::get_jak_project_dir() / "out" / output_prefix / "fr3");
return true;
}
} // namespace jak3

View File

@ -0,0 +1,9 @@
#pragma once
#include "goalc/build_level/common/build_level.h"
namespace jak3 {
bool run_build_level(const std::string& input_file,
const std::string& bsp_output_file,
const std::string& output_prefix);
} // namespace jak3

View File

@ -5,6 +5,7 @@
#include "goalc/build_level/jak1/build_level.h"
#include "goalc/build_level/jak2/build_level.h"
#include "goalc/build_level/jak3/build_level.h"
#include "third-party/CLI11.hpp"
@ -33,7 +34,7 @@ int main(int argc, char** argv) {
app.add_option("output-file", output_file,
"Output .go file, (for example out/jak2/obj/test-zone.go)")
->required();
app.add_option("-g,--game", game, "Game version (jak1 or jak2)")->required();
app.add_option("-g,--game", game, "Game version (jak1, jak2 or jak3)")->required();
app.add_option("--proj-path", project_path_override,
"Specify the location of the 'data/' folder");
app.validate_positionals();
@ -61,6 +62,9 @@ int main(int argc, char** argv) {
case GameVersion::Jak2:
jak2::run_build_level(input_json, output_file, "jak2/");
break;
case GameVersion::Jak3:
jak3::run_build_level(input_json, output_file, "jak3/");
break;
default:
ASSERT_NOT_REACHED_MSG("unsupported game version");
}

View File

@ -104,6 +104,7 @@ MakeSystem::MakeSystem(const std::optional<REPL::Config> repl_config, const std:
add_tool<SubtitleV2Tool>();
add_tool<BuildLevelTool>();
add_tool<BuildLevel2Tool>();
add_tool<BuildLevel3Tool>();
}
/*!

View File

@ -6,6 +6,7 @@
#include "goalc/build_level/jak1/build_level.h"
#include "goalc/build_level/jak2/build_level.h"
#include "goalc/build_level/jak3/build_level.h"
#include "goalc/compiler/Compiler.h"
#include "goalc/data_compiler/dir_tpages.h"
#include "goalc/data_compiler/game_count.h"
@ -278,3 +279,20 @@ bool BuildLevel2Tool::run(const ToolInput& task, const PathMap& path_map) {
}
return jak2::run_build_level(task.input.at(0), task.output.at(0), path_map.output_prefix);
}
BuildLevel3Tool::BuildLevel3Tool() : Tool("build-level3") {}
bool BuildLevel3Tool::needs_run(const ToolInput& task, const PathMap& path_map) {
if (task.input.size() != 1) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
auto deps = get_build_level_deps(task.input.at(0));
return Tool::needs_run({task.input, deps, task.output, task.arg}, path_map);
}
bool BuildLevel3Tool::run(const ToolInput& task, const PathMap& path_map) {
if (task.input.size() != 1) {
throw std::runtime_error(fmt::format("Invalid amount of inputs to {} tool", name()));
}
return jak3::run_build_level(task.input.at(0), task.output.at(0), path_map.output_prefix);
}

View File

@ -85,3 +85,10 @@ class BuildLevel2Tool : public Tool {
bool run(const ToolInput& task, const PathMap& path_map) override;
bool needs_run(const ToolInput& task, const PathMap& path_map) override;
};
class BuildLevel3Tool : public Tool {
public:
BuildLevel3Tool();
bool run(const ToolInput& task, const PathMap& path_map) override;
bool needs_run(const ToolInput& task, const PathMap& path_map) override;
};