custom levels: support custom regions in jak2/3 (#4300)

This adds support for generating pairs with `DataObjectGenerator` and
defining your own regions and `actor-group`s in custom levels.
This commit is contained in:
Hat Kid
2026-06-03 18:39:21 +02:00
committed by GitHub
parent e00bc479f4
commit 1254d93fbe
22 changed files with 2111 additions and 46 deletions
@@ -53,9 +53,12 @@
// The base actor id for your custom level. If you have multiple levels, this should be unique!
"base_id": 100,
// Base id for regions.
"base_region_id": 0,
// All art groups you want to use in your custom level. Will add their models and corresponding textures to the FR3 file.
// Commented out so that the release builds don't have to double-decompile the game
// "art_groups": ["yakow-ag"],
// "art_groups": ["yakow-ag", "water-anim-fortress-ag"],
// If you have any custom models in the "custom_assets/jak2/models/custom_levels" folder that you want to use in your level, add them to this list.
// Note: Like with art groups, these should also be added to your level's .gd file.
@@ -69,6 +72,177 @@
// If you want all textures from a tpage, you can just do ["tpage-name"].
"textures": ["yak-medfur-end"], // for yakow texture fix
// Any regions you want to include in your custom level.
// Regions run scripts that do things like loading specific levels,
// playing ambient sounds, checking if a task is completed in order to e.g. open airlocks, play cutscenes, etc.
//
// They can have three scripts that run on different conditions:
// - A single time when entering the region's bounds (on-enter)
// - Once every frame while you are inside a region (on-inside)
// - A single time when exiting a region's bounds (on-exit)
// Scripts are Lisp pairs, e.g. (want-load 'ctysluma 'ctyindb 'ctywide).
// The list of commands that can be used is too exhaustive to show here and there is no proper documentation for this,
// but you can refer to https://github.com/open-goal/jak-project/blob/master/goal_src/jak2/engine/util/script.gc
// for more information or inspect other regions via the debug menu in-game for examples.
//
// There are two special cases for script forms: "entity-actor" and "actor-group".
// Region scripts can refer to entities or actor groups that are stored inside the level data,
// so forms that start with either of these will be changed during the level building process to insert a reference
// to the given object.
//
// Regions can either be spheres, planes or volumes.
// Spheres do not need anything special, just the trans and bsphere, but if you want a face or a volume,
// you need to additionally specify a "face" or a "volume" key.
// A "face" key contains the face normal and a list of points.
// A "volume" key contains a list of faces.
//
// There are different types of regions like ones that get triggered based on camera position, Jak's position, etc.
// Each type of region is stored in its own region tree, each of which has its own bsphere that all its regions must be encompassed within.
// The full list is as follows:
// target (Jak based), camera (camera based), data (?), water (water volumes), city_vis (?),
// sample (?), light (presumably for foreground lights), entity (?)
"region_trees": {
// camera regions are activated when the camera enters their bounds
"camera": {
// every region tree has its own bsphere. all of a tree's regions must be encompassed within this sphere.
"bsphere": [0, 0, 0, 50],
"regions": [
{
"id": 1,
"shape": "sphere", // can be "sphere", "face" or "volume"
"trans": [-17, 4, 30],
"bsphere": [-17, 4, 30, 5],
// "(actor-group 2)" will be replaced with an actual reference
// to the actor-group in the level data with id 2.
"on-enter": "(begin (print 'enter1) (send-event (actor-group 2) 'none))",
"on-inside": "(begin (sound-play-loop \"punch\") (sound-play-loop \"spin\"))",
"on-exit": "(print 'exit1)"
}
]
},
// target regions are activated when Jak enters their bounds
"target": {
"bsphere": [0, 0, 0, 50],
"regions": [
{
"id": 2,
"shape": "face",
"on-enter": "(begin (print 'enter14) (send-event \"test-actor\" 'rot))",
"bsphere": [3.37, 6.2, 16.59, 7.0711],
"face": {
"normal": [-0.615662, 0.0, -0.788011, -15.147877],
"points": [
[-0.570054, 0, 19.668308],
[-0.570053, 11, 19.668308],
[7.310053, 0, 13.511692],
[7.310053, 11, 13.511692]
]
}
}
]
},
// water regions are used to define water volumes
"water": {
"bsphere": [0, 0, 0, 200],
"regions": [
{
"id": 3,
"shape": "sphere",
// "(entity-actor water-anim-test-zone-1)" will be replaced with an actual reference of the entity with that name.
"on-inside": "(water water-anim (entity-actor water-anim-test-zone-1) (swim wade))",
"trans": [5.0, 6.0, -66.0],
"bsphere": [5.0, 6.0, -66.0, 50.0]
},
{
"id": 4,
"shape": "volume",
"on-inside": "(water height 0.0 (swim wade))",
"bsphere": [5.1664, 9.4453, 51.2316, 25.2828],
"volume": {
"faces": [
{
"normal": [-1.0, 0.0, 0.0, 11.785238],
"points": [
[-11.785238, -2.468076, 65.720734],
[-11.785238, 21.358631, 65.720734],
[-11.785238, -2.468076, 36.742477],
[-11.785238, 21.358631, 36.742477]
]
},
{
"normal": [0.0, 0.0, -1.0, -36.742475],
"points": [
[-11.785238, -2.468076, 36.742477],
[-11.785238, 21.358631, 36.742477],
[22.118128, -2.468076, 36.742477],
[22.118128, 21.358631, 36.742477]
]
},
{
"normal": [1.0, 0.0, 0.0, 22.118128],
"points": [
[22.118128, -2.468076, 36.742477],
[22.118128, 21.358631, 36.742477],
[22.118128, -2.468076, 65.720734],
[22.118128, 21.358631, 65.720734]
]
},
{
"normal": [0.0, 0.0, 1.0, 65.72073],
"points": [
[22.118128, -2.468076, 65.720734],
[22.118128, 21.358631, 65.720734],
[-11.785238, -2.468076, 65.720734],
[-11.785238, 21.358631, 65.720734]
]
},
{
"normal": [0.0, -1.0, 0.0, 2.468076],
"points": [
[-11.785238, -2.468076, 36.742477],
[22.118128, -2.468076, 36.742477],
[-11.785238, -2.468076, 65.720734],
[22.118128, -2.468076, 65.720734]
]
},
{
"normal": [0.0, 1.0, 0.0, 21.358631],
"points": [
[22.118128, 21.358631, 36.742477],
[-11.785238, 21.358631, 36.742477],
[22.118128, 21.358631, 65.720734],
[-11.785238, 21.358631, 65.720734]
]
}
]
}
}
]
},
"data": {},
"city_vis": {},
"sample": {},
"light": {},
"entity": {}
},
// Actor groups you want to include in your level.
//
// Actor groups are arrays of entities, used for example in regions to send events to a list of actors
// or in "battles" as a list of enemies that need to be defeated before being able to progress.
// You can give them an id, this has no effect in-game, but it is used for region scripts so the level builder
// knows which actor group to insert.
"actor_groups": [
{
"id": 0,
"entities": ["test-crate", "test-eco", "test-yakow"]
},
{
"id": 2,
"entities": ["test-crate", "test-actor"]
}
],
"actors": [
{
"trans": [-15.2818, 15.2461, 17.136], // translation
@@ -115,6 +289,24 @@
"lump": {
"name": "test-actor"
}
},
{
"trans": [5, 6.778769493103027, -66],
"etype": "water-anim-test-zone",
"game_task": 0,
"quat": [0.0, 0.0, 0.0, 1.0],
"bsphere": [5, 6.778769493103027, -66, 50],
"lump": {
"look": ["int32", 21],
"name": "water-anim-test-zone-1",
"water-height": [
"water-height",
6.77869987487793,
0.5,
2.0,
"(water-flags can-swim can-wade)"
]
}
}
]
}
@@ -53,9 +53,12 @@
// The base actor id for your custom level. If you have multiple levels, this should be unique!
"base_id": 100,
// Base id for regions.
"base_region_id": 0,
// All art groups you want to use in your custom level. Will add their models and corresponding textures to the FR3 file.
// Commented out so that the release builds don't have to double-decompile the game
// "art_groups": ["yakow-ag"],
// "art_groups": ["yakow-ag", "water-anim-waspala-ag"],
// If you have any custom models in the "custom_assets/jak3/models/custom_levels" folder that you want to use in your level, add them to this list.
// Note: Like with art groups, these should also be added to your level's .gd file.
@@ -69,6 +72,177 @@
// If you want all textures from a tpage, you can just do ["tpage-name"].
"textures": ["yak-medfur-end"], // for yakow texture fix
// Any regions you want to include in your custom level.
// Regions run scripts that do things like loading specific levels,
// playing ambient sounds, checking if a task is completed in order to e.g. open airlocks, play cutscenes, etc.
//
// They can have three scripts that run on different conditions:
// - A single time when entering the region's bounds (on-enter)
// - Once every frame while you are inside a region (on-inside)
// - A single time when exiting a region's bounds (on-exit)
// Scripts are Lisp pairs, e.g. (want-load 'ctysluma 'ctyindb 'ctywide).
// The list of commands that can be used is too exhaustive to show here and there is no proper documentation for this,
// but you can refer to https://github.com/open-goal/jak-project/blob/master/goal_src/jak3/engine/util/script.gc
// for more information or inspect other regions via the debug menu in-game for examples.
//
// There are two special cases for script forms: "entity-actor" and "actor-group".
// Region scripts can refer to entities or actor groups that are stored inside the level data,
// so forms that start with either of these will be changed during the level building process to insert a reference
// to the given object.
//
// Regions can either be spheres, planes or volumes.
// Spheres do not need anything special, just the trans and bsphere, but if you want a face or a volume,
// you need to additionally specify a "face" or a "volume" key.
// A "face" key contains the face normal and a list of points.
// A "volume" key contains a list of faces.
//
// There are different types of regions like ones that get triggered based on camera position, Jak's position, etc.
// Each type of region is stored in its own region tree, each of which has its own bsphere that all its regions must be encompassed within.
// The full list is as follows:
// target (Jak based), camera (camera based), data (?), water (water volumes), city_vis (?),
// sample (?), light (presumably for foreground lights), entity (?)
"region_trees": {
// camera regions are activated when the camera enters their bounds
"camera": {
// every region tree has its own bsphere. all of a tree's regions must be encompassed within this sphere.
"bsphere": [0, 0, 0, 50],
"regions": [
{
"id": 1,
"shape": "sphere", // can be "sphere", "face" or "volume"
"trans": [-17, 4, 30],
"bsphere": [-17, 4, 30, 5],
// "(actor-group 2)" will be replaced with an actual reference
// to the actor-group in the level data with id 2.
"on-enter": "(begin (print 'enter1) (send-event (actor-group 2) 'none))",
"on-inside": "(begin (sound-play-loop \"punch\") (sound-play-loop \"spin\"))",
"on-exit": "(print 'exit1)"
}
]
},
// target regions are activated when Jak enters their bounds
"target": {
"bsphere": [0, 0, 0, 50],
"regions": [
{
"id": 2,
"shape": "face",
"on-enter": "(begin (print 'enter14) (send-event \"test-actor\" 'rot))",
"bsphere": [3.37, 6.2, 16.59, 7.0711],
"face": {
"normal": [-0.615662, 0.0, -0.788011, -15.147877],
"points": [
[-0.570054, 0, 19.668308],
[-0.570053, 11, 19.668308],
[7.310053, 0, 13.511692],
[7.310053, 11, 13.511692]
]
}
}
]
},
// water regions are used to define water volumes
"water": {
"bsphere": [0, 0, 0, 200],
"regions": [
{
"id": 3,
"shape": "sphere",
// "(entity-actor water-anim-test-zone-1)" will be replaced with an actual reference of the entity with that name.
"on-inside": "(water water-anim (entity-actor water-anim-test-zone-1) (swim wade))",
"trans": [5.0, 6.0, -66.0],
"bsphere": [5.0, 6.0, -66.0, 50.0]
},
{
"id": 4,
"shape": "volume",
"on-inside": "(water height 0.0 (swim wade))",
"bsphere": [5.1664, 9.4453, 51.2316, 25.2828],
"volume": {
"faces": [
{
"normal": [-1.0, 0.0, 0.0, 11.785238],
"points": [
[-11.785238, -2.468076, 65.720734],
[-11.785238, 21.358631, 65.720734],
[-11.785238, -2.468076, 36.742477],
[-11.785238, 21.358631, 36.742477]
]
},
{
"normal": [0.0, 0.0, -1.0, -36.742475],
"points": [
[-11.785238, -2.468076, 36.742477],
[-11.785238, 21.358631, 36.742477],
[22.118128, -2.468076, 36.742477],
[22.118128, 21.358631, 36.742477]
]
},
{
"normal": [1.0, 0.0, 0.0, 22.118128],
"points": [
[22.118128, -2.468076, 36.742477],
[22.118128, 21.358631, 36.742477],
[22.118128, -2.468076, 65.720734],
[22.118128, 21.358631, 65.720734]
]
},
{
"normal": [0.0, 0.0, 1.0, 65.72073],
"points": [
[22.118128, -2.468076, 65.720734],
[22.118128, 21.358631, 65.720734],
[-11.785238, -2.468076, 65.720734],
[-11.785238, 21.358631, 65.720734]
]
},
{
"normal": [0.0, -1.0, 0.0, 2.468076],
"points": [
[-11.785238, -2.468076, 36.742477],
[22.118128, -2.468076, 36.742477],
[-11.785238, -2.468076, 65.720734],
[22.118128, -2.468076, 65.720734]
]
},
{
"normal": [0.0, 1.0, 0.0, 21.358631],
"points": [
[22.118128, 21.358631, 36.742477],
[-11.785238, 21.358631, 36.742477],
[22.118128, 21.358631, 65.720734],
[-11.785238, 21.358631, 65.720734]
]
}
]
}
}
]
},
"data": {},
"city_vis": {},
"sample": {},
"light": {},
"entity": {}
},
// Actor groups you want to include in your level.
//
// Actor groups are arrays of entities, used for example in regions to send events to a list of actors
// or in "battles" as a list of enemies that need to be defeated before being able to progress.
// You can give them an id, this has no effect in-game, but it is used for region scripts so the level builder
// knows which actor group to insert.
"actor_groups": [
{
"id": 0,
"entities": ["test-crate", "test-eco", "test-yakow"]
},
{
"id": 2,
"entities": ["test-crate", "test-actor"]
}
],
"actors": [
{
"trans": [-15.2818, 15.2461, 17.136], // translation
@@ -116,6 +290,24 @@
"lump": {
"name": "test-actor"
}
},
{
"trans": [5, 6.778769493103027, -66],
"etype": "water-anim-test-zone",
"game_task": 0,
"quat": [0.0, 0.0, 0.0, 1.0],
"bsphere": [5, 6.778769493103027, -66, 50],
"lump": {
"look": ["int32", 7],
"name": "water-anim-test-zone-1",
"water-height": [
"water-height",
6.77869987487793,
0.5,
2.0,
"(water-flag can-swim can-wade)"
]
}
}
]
}
@@ -6,7 +6,8 @@
(base vector :inline)
(old-base vector :inline)
(bob-offset int64)
(bob-amount float))
(bob-amount float)
(last-fast-rot time-frame))
(:methods
(init-collision! (_type_) object))
(:state-methods
@@ -74,6 +75,7 @@
:event
(behavior ((proc process) (argc int) (message symbol) (block event-message-block))
(case message
(('rot) (set-time! (-> self last-fast-rot)))
(('attack 'touch)
; (if (= (-> proc type) target)
; (send-event proc 'attack #f (static-attack-info ((shove-up (meters 2.5)) (shove-back (meters 7.5)))))
@@ -82,7 +84,7 @@
:code
(behavior ()
(loop
(quaternion-rotate-y! (-> self root quat) (-> self root quat) (* (degrees 45) (seconds-per-frame)))
(quaternion-rotate-y! (-> self root quat) (-> self root quat) (* (degrees (if (not (time-elapsed? (-> self last-fast-rot) (seconds 2))) 240 45)) (seconds-per-frame)))
(let ((bob (-> self bob-amount)))
(when (< 0.0 bob)
(set! (-> self root trans y)
@@ -99,3 +101,31 @@
; )
(suspend)))
:post transform-post)
(deftype water-anim-test-zone (water-anim)
()
)
(define ripple-for-water-anim-test-zone (new 'static 'ripple-wave-set
:count 3
:converted #f
:normal-scale 1.0
:wave (new 'static 'inline-array ripple-wave 4
(new 'static 'ripple-wave :scale 40.0 :xdiv 1 :speed 1.5)
(new 'static 'ripple-wave :scale 40.0 :xdiv -1 :zdiv 1 :speed 1.5)
(new 'static 'ripple-wave :scale 20.0 :xdiv 5 :zdiv 3 :speed 0.75)
(new 'static 'ripple-wave)
)
)
)
(defmethod init-water! ((this water-anim-test-zone))
(call-parent-method this)
(set! (-> this draw ripple) (new 'process 'ripple-control))
(set-vector! (-> this draw color-mult) 0.01 0.45 0.5 0.75)
(set! (-> this draw ripple global-scale) (meters 0.75))
(set! (-> this draw ripple close-fade-dist) (meters 40))
(set! (-> this draw ripple far-fade-dist) (meters 60))
(set! (-> this draw ripple waveform) ripple-for-water-anim-test-zone)
(none)
)
@@ -6,7 +6,8 @@
(base vector :inline)
(old-base vector :inline)
(bob-offset int64)
(bob-amount float))
(bob-amount float)
(last-fast-rot time-frame))
(:methods
(init-collision! (_type_) object))
(:state-methods
@@ -73,6 +74,7 @@
:event
(behavior ((proc process) (argc int) (message symbol) (block event-message-block))
(case message
(('rot) (set-time! (-> self last-fast-rot)))
(('attack 'touch)
; (if (= (-> proc type) target)
; (send-event proc 'attack #f (static-attack-info ((shove-up (meters 2.5)) (shove-back (meters 7.5)))))
@@ -81,7 +83,7 @@
:code
(behavior ()
(loop
(quaternion-rotate-y! (-> self root quat) (-> self root quat) (* (degrees 45) (seconds-per-frame)))
(quaternion-rotate-y! (-> self root quat) (-> self root quat) (* (degrees (if (not (time-elapsed? (-> self last-fast-rot) (seconds 2))) 240 45)) (seconds-per-frame)))
(let ((bob (-> self bob-amount)))
(when (< 0.0 bob)
(set! (-> self root trans y)
@@ -98,3 +100,27 @@
; )
(suspend)))
:post transform-post)
(deftype water-anim-test-zone (water-anim) ())
(define ripple-for-water-anim-test-zone (new 'static 'ripple-wave-set
:count 3
:converted #f
:normal-scale 1.0
:wave (new 'static 'inline-array ripple-wave 4
(new 'static 'ripple-wave :scale 10.0 :xdiv 1 :speed 1.5)
(new 'static 'ripple-wave :scale 10.0 :xdiv -1 :zdiv 1 :speed 1.5)
(new 'static 'ripple-wave :scale 5.0 :xdiv 5 :zdiv 3 :speed 0.75)
(new 'static 'ripple-wave)
)
)
)
(defmethod init-water! ((this water-anim-test-zone))
(call-parent-method this)
(set! (-> this draw ripple) (new 'process 'ripple-control))
(set! (-> this draw ripple global-scale) (meters 0.75))
(set! (-> this draw ripple close-fade-dist) (meters 40))
(set! (-> this draw ripple far-fade-dist) (meters 60))
(set! (-> this draw ripple waveform) ripple-for-water-anim-test-zone)
)
+2
View File
@@ -36,6 +36,8 @@ add_library(compiler
build_level/common/Tfrag.cpp
build_level/common/Tie.cpp
build_level/jak1/ambient.cpp
build_level/jak2/Region.cpp
build_level/jak3/Region.cpp
compiler/Compiler.cpp
compiler/Env.cpp
compiler/Val.cpp
+17 -2
View File
@@ -1,8 +1,23 @@
#include "FileInfo.h"
#include <chrono>
#include "common/versions/versions.h"
#include "goalc/data_compiler/DataObjectGenerator.h"
#include <fmt/chrono.h>
std::string get_current_time_and_date() {
auto const now = std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now());
std::time_t const t = std::chrono::system_clock::to_time_t(now);
std::tm tm{};
#if defined(_WIN32)
localtime_s(&tm, &t);
#else
localtime_r(&t, &tm);
#endif
return fmt::format("{:%a %b %d %H:%M:%S %Y}", tm);
}
size_t FileInfo::add_to_object_file(DataObjectGenerator& gen) const {
gen.align_to_basic();
@@ -13,8 +28,8 @@ size_t FileInfo::add_to_object_file(DataObjectGenerator& gen) const {
gen.add_word(major_version);
gen.add_word(minor_version);
gen.add_ref_to_string_in_pool(maya_file_name);
gen.add_ref_to_string_in_pool(tool_debug);
gen.add_ref_to_string_in_pool(tool_debug);
gen.add_ref_to_string_in_pool(tool_debug + " " + get_current_time_and_date() + "\n");
gen.add_ref_to_string_in_pool(mdb_file_name);
return offset;
}
+67 -4
View File
@@ -1,5 +1,7 @@
#include "Entity.h"
#include "common/util/Assert.h"
namespace jak2 {
size_t EntityActor::generate(DataObjectGenerator& gen) const {
size_t result = res_lump.generate_header(gen, "entity-actor");
@@ -39,8 +41,7 @@ size_t generate_drawable_actor(DataObjectGenerator& gen,
return result;
}
size_t generate_inline_array_actors(DataObjectGenerator& gen,
const std::vector<EntityActor>& actors) {
size_t generate_inline_array_actors(DataObjectGenerator& gen, std::vector<EntityActor>& actors) {
std::vector<size_t> actor_locs;
for (auto& actor : actors) {
actor_locs.push_back(actor.generate(gen));
@@ -62,6 +63,7 @@ size_t generate_inline_array_actors(DataObjectGenerator& gen,
ASSERT((gen.current_offset_bytes() % 16) == 0);
for (size_t i = 0; i < actors.size(); i++) {
actors[i].slot = actor_locs[i];
generate_drawable_actor(gen, actors[i], actor_locs[i]);
}
return result;
@@ -94,10 +96,13 @@ void add_actors_from_json(const nlohmann::json& json,
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()) {
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] == '\'') {
if (key == "name") {
actor.name = value_string;
}
if (!value_string.empty() && value_string[0] == '\'') {
actor.res_lump.add_res(
std::make_unique<ResSymbol>(key, value_string.substr(1), -1000000000.0000));
} else {
@@ -115,4 +120,62 @@ void add_actors_from_json(const nlohmann::json& json,
actor.res_lump.sort_res();
}
}
size_t generate_actor_group(DataObjectGenerator& gen, const ActorGroup& group) {
gen.align_to_basic();
gen.add_type_tag("actor-group");
size_t result = gen.current_offset_bytes();
auto length = group.actors.size();
gen.add_word(length);
gen.add_word(length);
gen.add_type_tag("entity-actor");
for (auto& actor : group.actors) {
gen.link_word_to_byte(gen.add_word(0), actor->slot);
gen.add_word(actor->aid);
}
gen.align(4);
return result;
}
size_t generate_actor_group_array(DataObjectGenerator& gen, std::vector<ActorGroup>& actor_groups) {
gen.align_to_basic();
gen.add_type_tag("array");
size_t result = gen.current_offset_bytes();
auto length = actor_groups.size();
gen.add_word(length);
gen.add_word(length);
gen.add_type_tag("actor-group");
std::vector<size_t> group_slots;
for (auto& group : actor_groups) {
group_slots.push_back(gen.add_word(0));
}
gen.align(4);
for (int i = 0; i < group_slots.size(); i++) {
actor_groups.at(i).slot = generate_actor_group(gen, actor_groups.at(i));
gen.link_word_to_byte(group_slots.at(i), actor_groups.at(i).slot);
}
return result;
}
void add_actor_groups_from_json(const nlohmann::json& json,
const std::vector<EntityActor>& actors,
std::vector<ActorGroup>& actor_groups,
u32 base_id) {
for (const auto& actor_group : json) {
auto& group = actor_groups.emplace_back();
group.id = actor_group.value("id", base_id + actor_groups.size());
auto entity_list = actor_group.value<std::vector<std::string>>("entities", {});
for (auto& ent_name : entity_list) {
auto ent = std::ranges::find_if(
actors, [ent_name](const EntityActor& actor) { return actor.name == ent_name; });
if (ent != actors.end()) {
group.actors.push_back(&*ent);
} else {
ASSERT_MSG(false, fmt::format("Failed to find actor \"{}\", declared in actor group id {}.",
ent_name, group.id));
}
}
}
}
} // namespace jak2
+15 -2
View File
@@ -13,6 +13,7 @@ namespace jak2 {
* (quat quaternion :inline :offset-assert 64)
*/
struct EntityActor {
std::string name;
ResLump res_lump;
math::Vector4f trans; // w = 1 here
u32 aid = 0;
@@ -24,14 +25,26 @@ struct EntityActor {
math::Vector4f bsphere;
size_t slot;
size_t generate(DataObjectGenerator& gen) const;
};
size_t generate_inline_array_actors(DataObjectGenerator& gen,
const std::vector<EntityActor>& actors);
struct ActorGroup {
u64 id;
std::vector<const EntityActor*> actors;
size_t slot;
};
size_t generate_inline_array_actors(DataObjectGenerator& gen, std::vector<EntityActor>& actors);
size_t generate_actor_group_array(DataObjectGenerator& gen, std::vector<ActorGroup>& actor_groups);
void add_actors_from_json(const nlohmann::json& json,
std::vector<EntityActor>& actor_list,
u32 base_aid,
decompiler::DecompilerTypeSystem& dts);
void add_actor_groups_from_json(const nlohmann::json& json,
const std::vector<EntityActor>& actors,
std::vector<ActorGroup>& actor_groups,
u32 base_id);
} // namespace jak2
+5 -2
View File
@@ -66,7 +66,7 @@ size_t generate_u32_array(const std::vector<u32>& array, DataObjectGenerator& ge
return result;
}
std::vector<u8> LevelFile::save_object_file() const {
std::vector<u8> LevelFile::save_object_file() {
DataObjectGenerator gen;
gen.add_type_tag("bsp-header");
@@ -78,7 +78,6 @@ std::vector<u8> LevelFile::save_object_file() const {
//(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 uint16) :offset-assert 32)
//(visible-list-length int32 :offset-assert 36)
@@ -114,8 +113,12 @@ std::vector<u8> LevelFile::save_object_file() const {
//(light-hash light-hash :offset-assert 176)
//(nav-meshes (array entity-nav-mesh) :offset-assert 180)
//(actor-groups (array actor-group) :offset-assert 184)
gen.link_word_to_byte(184 / 4, generate_actor_group_array(gen, actor_groups));
//(region-trees (array drawable-tree-region-prim) :offset-assert 188)
gen.link_word_to_byte(188 / 4,
generate_drawable_tree_region_prim_array(gen, region_array, region_trees));
//(region-array region-array :offset-assert 192)
gen.link_word_to_byte(192 / 4, region_array.slot);
//(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)
+5 -12
View File
@@ -12,6 +12,7 @@
#include "goalc/build_level/common/Tie.h"
#include "goalc/build_level/jak2/Entity.h"
#include "goalc/build_level/jak2/FileInfo.h"
#include "goalc/build_level/jak2/Region.h"
namespace jak2 {
struct VisibilityString {
@@ -22,13 +23,10 @@ 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;
};
@@ -49,12 +47,6 @@ struct LightHash {};
struct EntityNavMesh {};
struct ActorGroup {};
struct RegionTree {};
struct RegionArray {};
struct CityLevelInfo {};
struct TextureMasksArray {};
@@ -141,7 +133,8 @@ struct LevelFile {
// (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;
std::map<int, Region> regions;
std::vector<DrawableTreeRegionPrim> region_trees;
// (region-array region-array :offset-assert 192)
RegionArray region_array;
// (collide-hash collide-hash :offset-assert 196)
@@ -156,7 +149,7 @@ struct LevelFile {
// (vis-spheres-length uint32 :offset 248)
// (region-tree drawable-tree-region-prim :offset 252)
RegionTree region_tree;
DrawableTreeRegionPrim region_tree;
// (tfrag-masks texture-masks-array :offset-assert 256)
// (tfrag-closest (pointer float) :offset-assert 260)
// (tfrag-mask-count uint32 :offset 260)
@@ -176,6 +169,6 @@ struct LevelFile {
// (bsp-scale vector :inline :offset-assert 288)
// (bsp-offset vector :inline :offset-assert 304)
std::vector<u8> save_object_file() const;
std::vector<u8> save_object_file();
};
} // namespace jak2
+423
View File
@@ -0,0 +1,423 @@
#include "Region.h"
#include "Entity.h"
#include "common/log/log.h"
namespace jak2 {
std::optional<std::map<std::string, size_t>> g_entity_slots;
std::optional<std::map<int, size_t>> g_actor_group_slots;
std::map<std::string, size_t> make_entity_slot_map(std::vector<EntityActor>* actors) {
std::map<std::string, size_t> result{};
if (actors) {
for (const auto& e : *actors) {
result.emplace(e.name, e.slot);
}
}
return result;
}
std::map<int, size_t> make_actor_group_slot_list(std::vector<ActorGroup>* actor_groups) {
std::map<int, size_t> result{};
if (actor_groups) {
for (const auto& e : *actor_groups) {
result.emplace(e.id, e.slot);
}
}
return result;
}
size_t gen_pair(DataObjectGenerator& gen, const goos::Object& pair) {
return gen.add_pair(pair, g_entity_slots, g_actor_group_slots);
}
void Region::generate_pairs(DataObjectGenerator& gen, const std::vector<size_t>& pair_slots) {
auto on_enter_slot = pair_slots[0];
auto on_inside_slot = pair_slots[1];
auto on_exit_slot = pair_slots[2];
size_t on_enter_byte = 0;
size_t on_inside_byte = 0;
size_t on_exit_byte = 0;
if (on_enter.has_value()) {
on_enter_byte = gen_pair(gen, *on_enter.value());
} else {
gen.link_word_to_symbol("#f", on_enter_slot);
}
if (on_inside.has_value()) {
on_inside_byte = gen_pair(gen, *on_inside.value());
} else {
gen.link_word_to_symbol("#f", on_inside_slot);
}
if (on_exit.has_value()) {
on_exit_byte = gen_pair(gen, *on_exit.value());
} else {
gen.link_word_to_symbol("#f", on_exit_slot);
}
if (on_enter.has_value()) {
gen.link_word_to_byte(on_enter_slot, on_enter_byte);
}
if (on_inside.has_value()) {
gen.link_word_to_byte(on_inside_slot, on_inside_byte);
}
if (on_exit.has_value()) {
gen.link_word_to_byte(on_exit_slot, on_exit_byte);
}
}
size_t Region::generate(DataObjectGenerator& gen) const {
auto region_slot = gen.add_word(id);
auto on_enter_slot = gen.add_word(0);
auto on_inside_slot = gen.add_word(0);
auto on_exit_slot = gen.add_word(0);
size_t on_enter_byte = 0;
size_t on_inside_byte = 0;
size_t on_exit_byte = 0;
if (on_enter.has_value()) {
on_enter_byte = gen_pair(gen, *on_enter.value());
} else {
gen.link_word_to_symbol("#f", on_enter_slot);
}
if (on_inside.has_value()) {
on_inside_byte = gen_pair(gen, *on_inside.value());
} else {
gen.link_word_to_symbol("#f", on_inside_slot);
}
if (on_exit.has_value()) {
on_exit_byte = gen_pair(gen, *on_exit.value());
} else {
gen.link_word_to_symbol("#f", on_exit_slot);
}
if (on_enter.has_value()) {
gen.link_word_to_byte(on_enter_slot, on_enter_byte);
}
if (on_inside.has_value()) {
gen.link_word_to_byte(on_inside_slot, on_inside_byte);
}
if (on_exit.has_value()) {
gen.link_word_to_byte(on_exit_slot, on_exit_byte);
}
return region_slot;
}
size_t RegionArray::generate(DataObjectGenerator& gen) {
g_entity_slots = make_entity_slot_map(entities);
g_actor_group_slots = make_actor_group_slot_list(actor_groups);
gen.align_to_basic();
gen.add_type_tag("region-array");
size_t result = gen.current_offset_bytes();
gen.add_word(data.size()); // 4 (length)
gen.add_word(data.size()); // 8 (allocated-length)
gen.add_word(0); // 12
ASSERT((gen.current_offset_bytes() % 16) == 0);
for (auto& region : data) {
std::vector<size_t> pairs;
region->slot = gen.add_word(region->id);
pairs.push_back(gen.add_word(0));
pairs.push_back(gen.add_word(0));
pairs.push_back(gen.add_word(0));
pair_slots.emplace(region->id, pairs);
region_slots.emplace(region->id, region->slot);
// region->slot = region->generate(gen);
}
for (auto& region : data) {
auto pairs = pair_slots.find(region->id);
if (pairs != pair_slots.end()) {
region->generate_pairs(gen, pairs->second);
}
}
return result;
}
size_t generate_drawable_tree_region_prim_array(DataObjectGenerator& gen,
RegionArray& regions,
const std::vector<DrawableTreeRegionPrim>& trees) {
gen.align_to_basic();
gen.add_type_tag("array");
size_t result = gen.current_offset_bytes();
auto length = trees.size();
std::vector<size_t> tree_slots;
gen.add_word(length);
gen.add_word(length);
gen.add_type_tag("drawable-tree-region-prim");
tree_slots.reserve(trees.size());
for (auto& tree : trees) {
tree_slots.push_back(gen.add_word(0));
}
gen.align(4);
regions.slot = regions.generate(gen);
for (int i = 0; i < trees.size(); i++) {
gen.link_word_to_byte(tree_slots[i], trees[i].generate(gen));
}
return result;
}
size_t DrawableTreeRegionPrim::generate(DataObjectGenerator& gen) const {
gen.align_to_basic();
gen.add_type_tag("drawable-tree-region-prim");
size_t result = gen.current_offset_bytes();
gen.add_word(1 << 16); // 4, 6 (length)
gen.add_symbol_link(name); // 8
gen.add_word(0);
gen.add_word_float(bsphere.x());
gen.add_word_float(bsphere.y());
gen.add_word_float(bsphere.z());
gen.add_word_float(bsphere.w());
size_t arr_slot = gen.add_word(0);
gen.align(4);
gen.link_word_to_byte(arr_slot, data2.generate(gen));
return result;
}
size_t DrawableInlineArrayRegionPrim::generate(DataObjectGenerator& gen) const {
gen.align_to_basic();
gen.add_type_tag("drawable-inline-array-region-prim");
size_t result = gen.current_offset_bytes();
gen.add_word(data.size() << 16); // 4, 6 (length)
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
for (auto& prim : data) {
prim->generate(gen);
}
// second pass to fill in face and volume references
for (auto& prim : data) {
if (dynamic_cast<DrawableRegionFace*>(prim)) {
auto face = dynamic_cast<DrawableRegionFace*>(prim);
gen.link_word_to_byte(face->face_data_slot, face->data.generate(gen));
}
if (dynamic_cast<DrawableRegionVolume*>(prim)) {
auto vol = dynamic_cast<DrawableRegionVolume*>(prim);
gen.link_word_to_byte(vol->face_array_slot, vol->faces.generate(gen));
}
}
return result;
}
void fill_region_trees(std::vector<DrawableTreeRegionPrim>& trees,
std::map<int, Region>& regions,
RegionArray& region_arr,
const nlohmann::json& json,
u32 base_id) {
for (const auto& [k, tree_json] : json.items()) {
ASSERT_MSG(tree_json.is_object(), fmt::format("key \"{}\" is not an object.", k));
if (tree_json.is_object() && !tree_json.empty()) {
auto& tree = trees.emplace_back(k);
tree.bsphere = vectorm4_from_json(tree_json.at("bsphere"));
add_regions_from_json(tree_json.at("regions"), tree, regions, base_id);
for (auto& [id, region] : regions) {
if (region.tree == tree.name) {
DrawableRegionPrim* prim = nullptr;
if (region.shape == "sphere") {
prim = new DrawableRegionSphere(&region);
tree.data2.data.push_back(prim);
} else if (region.shape == "face") {
prim = new DrawableRegionFace(&region);
tree.data2.data.push_back(prim);
} else if (region.shape == "volume") {
prim = new DrawableRegionVolume(&region);
tree.data2.data.push_back(prim);
} else {
ASSERT_MSG(false, fmt::format("Invalid region shape \"{}\"", region.shape));
}
}
}
}
}
for (auto& [id, region] : regions) {
region_arr.data.push_back(&region);
}
}
void add_regions_from_json(const nlohmann::json& json,
DrawableTreeRegionPrim& tree,
std::map<int, Region>& regions,
u32 base_id) {
std::vector<Region> result;
auto& reader = pretty_print::get_pretty_printer_reader();
for (const auto& region_json : json) {
auto id = region_json.value("id", base_id + regions.size());
auto p = regions.emplace(id, Region());
if (p.second) {
auto& region = p.first->second;
result.push_back(region);
region.id = region_json.value("id", base_id + regions.size());
region.tree = tree.name;
region.shape = region_json.at("shape").get<std::string>();
region.bsphere = vectorm4_from_json(region_json.at("bsphere"));
ASSERT_MSG(region.shape == "sphere" || region.shape == "face" || region.shape == "volume",
"Region must have shape 'sphere', 'face' or 'volume'.");
if (region.shape == "sphere") {
region.faces = std::nullopt;
}
if (region.shape == "face") {
ASSERT_MSG(region_json.find("face") != region_json.end(),
"Face region must have 'face' key.");
auto face = region_json.at("face").get<RegionFaceData>();
std::vector<RegionFaceData> faces;
faces.push_back(face);
region.faces = std::make_optional<std::vector<RegionFaceData>>(faces);
}
if (region.shape == "volume") {
ASSERT_MSG(region_json.find("volume") != region_json.end(),
"Volume region must have 'volume' key.");
auto arr = region_json.at("volume").get<RegionFaceArray>();
region.faces = std::make_optional<std::vector<RegionFaceData>>(arr.faces);
}
if (region_json.find("on-enter") != region_json.end()) {
region.on_enter = std::make_optional(
&reader.read_from_string(region_json.at("on-enter").get<std::string>(), false)
.as_pair()
->car);
}
if (region_json.find("on-inside") != region_json.end()) {
region.on_inside = std::make_optional(
&reader.read_from_string(region_json.at("on-inside").get<std::string>(), false)
.as_pair()
->car);
}
if (region_json.find("on-exit") != region_json.end()) {
region.on_exit = std::make_optional(
&reader.read_from_string(region_json.at("on-exit").get<std::string>(), false)
.as_pair()
->car);
}
lg::print(region.print());
} else {
lg::warn("Duplicate region with ID {} in tree {}, skipped", p.first->first, tree.name);
}
}
}
std::string Region::print() {
std::string result;
result += fmt::format("Region {} ({}):\n", id, tree);
result += fmt::format(" shape: {}\n", shape);
if (on_enter.has_value()) {
result += fmt::format(" on-enter: {}\n", on_enter.value()->print());
} else {
result += fmt::format(" on-enter: #f\n");
}
if (on_inside.has_value()) {
result += fmt::format(" on-inside: {}\n", on_inside.value()->print());
} else {
result += fmt::format(" on-inside: #f\n");
}
if (on_exit.has_value()) {
result += fmt::format(" on-exit: {}\n", on_exit.value()->print());
} else {
result += fmt::format(" on-exit: #f\n");
}
return result;
}
void from_json(const nlohmann::json& json, RegionFaceData& obj) {
obj.normal = vector_from_json(json.at("normal"));
obj.normal.w() *= METER_LENGTH;
std::vector<math::Vector4f> points;
for (auto& p : json.at("points")) {
points.push_back(vectorm3_from_json(p));
}
obj.num_points = points.size();
obj.points = points;
}
void from_json(const nlohmann::json& json, RegionFaceArray& obj) {
(void)obj;
for (const auto& face_json : json.at("faces")) {
RegionFaceData face;
from_json(face_json, face);
obj.faces.push_back(face);
}
}
size_t RegionFaceData::generate(DataObjectGenerator& gen) {
size_t result = gen.current_offset_bytes();
gen.add_word_float(normal.x());
gen.add_word_float(normal.y());
gen.add_word_float(normal.z());
gen.add_word_float(normal.w());
gen.add_word(num_points);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
for (auto& p : points) {
gen.add_word_float(p.x());
gen.add_word_float(p.y());
gen.add_word_float(p.z());
gen.add_word_float(p.w());
}
return result;
}
size_t RegionFaceArray::generate(DataObjectGenerator& gen) {
gen.align_to_basic();
gen.add_type_tag("region-face-array");
size_t result = gen.current_offset_bytes();
auto length = data.size();
gen.add_word(length);
gen.add_word(length);
gen.add_word(0);
for (auto& face : data) {
face.generate(gen);
}
for (int i = 0; i < data.size(); i++) {
auto face = data[i];
auto face0 = face.data;
gen.link_word_to_byte(face.face_data_slot, face0.generate(gen));
}
return result;
}
size_t DrawableRegionPrim::generate(DataObjectGenerator& gen) {
return 0;
}
size_t DrawableRegionSphere::generate(DataObjectGenerator& gen) {
gen.align_to_basic();
gen.add_type_tag("drawable-region-sphere");
size_t result = gen.current_offset_bytes();
gen.add_word(region->id);
gen.link_word_to_word(gen.add_word(0), region->slot);
gen.add_word(0);
gen.add_word_float(region->bsphere.x());
gen.add_word_float(region->bsphere.y());
gen.add_word_float(region->bsphere.z());
gen.add_word_float(region->bsphere.w());
return result;
}
size_t DrawableRegionVolume::generate(DataObjectGenerator& gen
/*size_t region_face_array_slot*/) {
gen.align_to_basic();
gen.add_type_tag("drawable-region-volume");
size_t result = gen.current_offset_bytes();
gen.add_word(region->id);
gen.link_word_to_word(gen.add_word(0), region->slot);
face_array_slot = gen.add_word(0);
gen.add_word_float(region->bsphere.x());
gen.add_word_float(region->bsphere.y());
gen.add_word_float(region->bsphere.z());
gen.add_word_float(region->bsphere.w());
return result;
}
size_t DrawableRegionFace::generate(DataObjectGenerator& gen) {
gen.align_to_basic();
gen.add_type_tag("drawable-region-face");
size_t result = gen.current_offset_bytes();
gen.add_word(region->id);
gen.link_word_to_word(gen.add_word(0), region->slot);
face_data_slot = gen.add_word(0);
const auto& bsphere = bsphere_override.has_value() ? bsphere_override.value() : region->bsphere;
gen.add_word_float(bsphere.x());
gen.add_word_float(bsphere.y());
gen.add_word_float(bsphere.z());
gen.add_word_float(bsphere.w());
return result;
}
} // namespace jak2
+207
View File
@@ -0,0 +1,207 @@
#pragma once
#include <cmath>
#include <string>
#include "Entity.h"
#include "common/common_types.h"
#include "common/goos/ParseHelpers.h"
#include "common/goos/Printer.h"
#include "common/math/Vector.h"
#include "goalc/data_compiler/DataObjectGenerator.h"
#include "third-party/json.hpp"
namespace jak2 {
struct RegionFaceData {
// (normal vector :inline :offset-assert 0)
// (normal-offset float :offset 12)
// (num-points uint32 :offset-assert 16)
// (points vector :inline :dynamic :offset-assert 32) ;; guess
math::Vector4f normal; // w component is normal offset
u32 num_points;
std::vector<math::Vector4f> points;
size_t generate(DataObjectGenerator& gen);
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
};
void from_json(const json& j, RegionFaceData& obj);
struct RegionFaceArray;
struct Region {
// (id uint32 :offset-assert 0)
// (on-enter pair :offset-assert 4)
// (on-inside pair :offset-assert 8)
// (on-exit pair :offset-assert 12)
u32 id;
std::optional<goos::Object*> on_enter;
std::optional<goos::Object*> on_inside;
std::optional<goos::Object*> on_exit;
math::Vector4f trans;
math::Vector4f bsphere;
std::string tree; // target, camera, data, water, city_vis, sample, light, entity
std::string shape; // sphere, face, volume
std::optional<std::vector<RegionFaceData>> faces;
size_t slot;
std::optional<std::map<std::string, size_t>> actor_slots;
size_t generate(DataObjectGenerator& gen) const;
void generate_pairs(DataObjectGenerator& gen, const std::vector<size_t>& pair_slots);
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
std::string print();
};
struct RegionArray {
// (data region :inline :dynamic :offset-assert 16)
std::vector<Region*> data;
std::map<int, std::vector<size_t>> pair_slots;
std::map<int, size_t> region_slots;
std::map<std::string, size_t> entity_actor_slots;
std::vector<EntityActor>* entities;
std::vector<ActorGroup>* actor_groups;
size_t slot;
size_t generate(DataObjectGenerator& gen);
size_t add_to_object_file(DataObjectGenerator& gen, size_t region_array) const;
};
struct DrawableRegionPrim {
// (deftype drawable-region-prim (drawable)
// ((region region :offset 8)
// )
Region* region;
explicit DrawableRegionPrim(Region* region) { this->region = region; }
virtual size_t generate(DataObjectGenerator& gen);
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
};
struct DrawableInlineArrayRegionPrim {
// (deftype drawable-inline-array-region-prim (drawable-inline-array)
// ((data drawable-region-prim 1 :inline :offset-assert 32)
// )
std::vector<DrawableRegionPrim*> data;
size_t generate(DataObjectGenerator& gen) const;
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
};
struct DrawableTreeRegionPrim {
// (deftype drawable-tree-region-prim (drawable-tree)
// ((id int16 :offset-assert 4)
// (length int16 :offset 6)
// (name symbol :offset 8)
// (bsphere vector :inline :offset-assert 16)
// (data2 drawable-inline-array :dynamic :offset 32 :score 1))
// )
std::string name;
math::Vector4f bsphere;
DrawableInlineArrayRegionPrim data2;
DrawableTreeRegionPrim() = default;
explicit DrawableTreeRegionPrim(std::string name_) : name(std::move(name_)) {}
size_t generate(DataObjectGenerator& gen) const;
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
};
size_t generate_drawable_tree_region_prim_array(DataObjectGenerator& gen,
RegionArray& regions,
const std::vector<DrawableTreeRegionPrim>& trees);
void fill_region_trees(std::vector<DrawableTreeRegionPrim>& trees,
std::map<int, Region>& regions,
RegionArray& region_arr,
const nlohmann::json& json,
u32 base_id);
void add_regions_from_json(const nlohmann::json& json,
DrawableTreeRegionPrim& tree,
std::map<int, Region>& regions,
u32 base_id);
struct DrawableRegionSphere : DrawableRegionPrim {
using DrawableRegionPrim::DrawableRegionPrim;
size_t generate(DataObjectGenerator& gen) override;
};
struct DrawableRegionFace : DrawableRegionPrim {
// (deftype drawable-region-face (drawable-region-prim)
// ((data region-face-data :offset 12)
// )
RegionFaceData data;
size_t face_data_slot;
std::optional<math::Vector4f> bsphere_override;
explicit DrawableRegionFace(Region* region) : DrawableRegionPrim(region) {
if (region->faces.has_value()) {
data = region->faces.value().at(0);
}
}
size_t generate(DataObjectGenerator& gen) override;
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
};
struct RegionFaceArray {
// (deftype region-face-array (inline-array-class)
// ((data drawable-region-face :inline :dynamic :offset 16)
// )
std::vector<DrawableRegionFace> data;
std::vector<RegionFaceData> faces;
size_t generate(DataObjectGenerator& gen);
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
};
void from_json(const json& j, RegionFaceArray& obj);
struct DrawableRegionVolume : DrawableRegionPrim {
// (deftype drawable-region-volume (drawable-region-prim)
// ((faces region-face-array :offset 12)
// )
RegionFaceArray faces;
size_t face_array_slot;
explicit DrawableRegionVolume(Region* region) : DrawableRegionPrim(region) {
if (this->region->faces.has_value()) {
auto face_arr = this->region->faces.value();
for (const auto& f : face_arr) {
auto& face = faces.data.emplace_back(region);
face.data = f;
// compute per-face bsphere from the face's points for volumes
if (!f.points.empty()) {
math::Vector4f center = {0, 0, 0, 0};
for (auto& pt : f.points) {
center.x() += pt.x();
center.y() += pt.y();
center.z() += pt.z();
}
auto n = static_cast<float>(f.points.size());
center.x() /= n;
center.y() /= n;
center.z() /= n;
float max_dist_sq = 0;
for (auto& pt : f.points) {
float dx = pt.x() - center.x();
float dy = pt.y() - center.y();
float dz = pt.z() - center.z();
max_dist_sq = std::max(max_dist_sq, dx * dx + dy * dy + dz * dz);
}
center.w() = std::sqrt(max_dist_sq);
face.bsphere_override = center;
}
}
}
}
size_t generate(DataObjectGenerator& gen) override;
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
};
} // namespace jak2
+27
View File
@@ -9,6 +9,7 @@
#include "goalc/build_level/jak2/Entity.h"
#include "goalc/build_level/jak2/FileInfo.h"
#include "goalc/build_level/jak2/LevelFile.h"
#include <decompiler/level_extractor/extract_collide_frags.h>
namespace jak2 {
bool run_build_level(const std::string& input_file,
@@ -60,9 +61,19 @@ bool run_build_level(const std::string& input_file,
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++) {
@@ -89,6 +100,22 @@ bool run_build_level(const std::string& input_file,
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
+65 -4
View File
@@ -39,8 +39,7 @@ size_t generate_drawable_actor(DataObjectGenerator& gen,
return result;
}
size_t generate_inline_array_actors(DataObjectGenerator& gen,
const std::vector<EntityActor>& actors) {
size_t generate_inline_array_actors(DataObjectGenerator& gen, std::vector<EntityActor>& actors) {
std::vector<size_t> actor_locs;
for (auto& actor : actors) {
actor_locs.push_back(actor.generate(gen));
@@ -62,6 +61,7 @@ size_t generate_inline_array_actors(DataObjectGenerator& gen,
ASSERT((gen.current_offset_bytes() % 16) == 0);
for (size_t i = 0; i < actors.size(); i++) {
actors[i].slot = actor_locs[i];
generate_drawable_actor(gen, actors[i], actor_locs[i]);
}
return result;
@@ -94,10 +94,13 @@ void add_actors_from_json(const nlohmann::json& json,
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()) {
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] == '\'') {
if (key == "name") {
actor.name = value_string;
}
if (!value_string.empty() && value_string[0] == '\'') {
actor.res_lump.add_res(
std::make_unique<ResSymbol>(key, value_string.substr(1), -1000000000.0000));
} else {
@@ -115,4 +118,62 @@ void add_actors_from_json(const nlohmann::json& json,
actor.res_lump.sort_res();
}
}
size_t generate_actor_group(DataObjectGenerator& gen, const ActorGroup& group) {
gen.align_to_basic();
gen.add_type_tag("actor-group");
size_t result = gen.current_offset_bytes();
auto length = group.actors.size();
gen.add_word(length);
gen.add_word(length);
gen.add_type_tag("entity-actor");
for (auto& actor : group.actors) {
gen.link_word_to_byte(gen.add_word(0), actor->slot);
gen.add_word(actor->aid);
}
gen.align(4);
return result;
}
size_t generate_actor_group_array(DataObjectGenerator& gen, std::vector<ActorGroup>& actor_groups) {
gen.align_to_basic();
gen.add_type_tag("array");
size_t result = gen.current_offset_bytes();
auto length = actor_groups.size();
gen.add_word(length);
gen.add_word(length);
gen.add_type_tag("actor-group");
std::vector<size_t> group_slots;
for (auto& group : actor_groups) {
group_slots.push_back(gen.add_word(0));
}
gen.align(4);
for (int i = 0; i < group_slots.size(); i++) {
actor_groups.at(i).slot = generate_actor_group(gen, actor_groups.at(i));
gen.link_word_to_byte(group_slots.at(i), actor_groups.at(i).slot);
}
return result;
}
void add_actor_groups_from_json(const nlohmann::json& json,
const std::vector<EntityActor>& actors,
std::vector<ActorGroup>& actor_groups,
u32 base_id) {
for (const auto& actor_group : json) {
auto& group = actor_groups.emplace_back();
group.id = actor_group.value("id", base_id + actor_groups.size());
auto entity_list = actor_group.value<std::vector<std::string>>("entities", {});
for (auto& ent_name : entity_list) {
auto ent = std::ranges::find_if(
actors, [ent_name](const EntityActor& actor) { return actor.name == ent_name; });
if (ent != actors.end()) {
group.actors.push_back(&*ent);
} else {
ASSERT_MSG(false, fmt::format("Failed to find actor \"{}\", declared in actor group id {}.",
ent_name, group.id));
}
}
}
}
} // namespace jak3
+15 -2
View File
@@ -13,6 +13,7 @@ namespace jak3 {
* (quat quaternion :inline :offset-assert 64)
*/
struct EntityActor {
std::string name;
ResLump res_lump;
math::Vector4f trans; // w = 1 here
u32 aid = 0;
@@ -24,14 +25,26 @@ struct EntityActor {
math::Vector4f bsphere;
size_t slot;
size_t generate(DataObjectGenerator& gen) const;
};
size_t generate_inline_array_actors(DataObjectGenerator& gen,
const std::vector<EntityActor>& actors);
struct ActorGroup {
u64 id;
std::vector<const EntityActor*> actors;
size_t slot;
};
size_t generate_inline_array_actors(DataObjectGenerator& gen, std::vector<EntityActor>& actors);
size_t generate_actor_group_array(DataObjectGenerator& gen, std::vector<ActorGroup>& actor_groups);
void add_actors_from_json(const nlohmann::json& json,
std::vector<EntityActor>& actor_list,
u32 base_aid,
decompiler::DecompilerTypeSystem& dts);
void add_actor_groups_from_json(const nlohmann::json& json,
const std::vector<EntityActor>& actors,
std::vector<ActorGroup>& actor_groups,
u32 base_id);
} // namespace jak3
+5 -1
View File
@@ -66,7 +66,7 @@ size_t generate_u32_array(const std::vector<u32>& array, DataObjectGenerator& ge
return result;
}
std::vector<u8> LevelFile::save_object_file() const {
std::vector<u8> LevelFile::save_object_file() {
DataObjectGenerator gen;
gen.add_type_tag("bsp-header");
@@ -114,8 +114,12 @@ std::vector<u8> LevelFile::save_object_file() const {
//(light-hash light-hash :offset-assert 176)
//(nav-meshes (array entity-nav-mesh) :offset-assert 180)
//(actor-groups (array actor-group) :offset-assert 184)
gen.link_word_to_byte(184 / 4, generate_actor_group_array(gen, actor_groups));
//(region-trees (array drawable-tree-region-prim) :offset-assert 188)
gen.link_word_to_byte(188 / 4,
generate_drawable_tree_region_prim_array(gen, region_array, region_trees));
//(region-array region-array :offset-assert 192)
gen.link_word_to_byte(192 / 4, region_array.slot);
//(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)
+5 -11
View File
@@ -12,6 +12,7 @@
#include "goalc/build_level/common/Tie.h"
#include "goalc/build_level/jak3/Entity.h"
#include "goalc/build_level/jak3/FileInfo.h"
#include "goalc/build_level/jak3/Region.h"
namespace jak3 {
struct VisibilityString {
@@ -22,8 +23,6 @@ struct DrawableTreeActor {};
struct DrawableTreeInstanceShrub {};
struct DrawableTreeRegionPrim {};
struct DrawableTreeArray {
std::vector<DrawableTreeTfrag> tfrags;
std::vector<DrawableTreeInstanceTie> ties;
@@ -49,12 +48,6 @@ struct LightHash {};
struct EntityNavMesh {};
struct ActorGroup {};
struct RegionTree {};
struct RegionArray {};
struct CityLevelInfo {};
struct TextureMasksArray {};
@@ -141,7 +134,8 @@ struct LevelFile {
// (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;
std::map<int, Region> regions;
std::vector<DrawableTreeRegionPrim> region_trees;
// (region-array region-array :offset-assert 192)
RegionArray region_array;
// (collide-hash collide-hash :offset-assert 196)
@@ -156,7 +150,7 @@ struct LevelFile {
// (vis-spheres-length uint32 :offset 248)
// (region-tree drawable-tree-region-prim :offset 252)
RegionTree region_tree;
DrawableTreeRegionPrim region_tree;
// (tfrag-masks texture-masks-array :offset-assert 256)
// (tfrag-closest (pointer float) :offset-assert 260)
// (tfrag-mask-count uint32 :offset 260)
@@ -176,6 +170,6 @@ struct LevelFile {
// (bsp-scale vector :inline :offset-assert 288)
// (bsp-offset vector :inline :offset-assert 304)
std::vector<u8> save_object_file() const;
std::vector<u8> save_object_file();
};
} // namespace jak3
+423
View File
@@ -0,0 +1,423 @@
#include "Region.h"
#include "Entity.h"
#include "common/log/log.h"
namespace jak3 {
std::optional<std::map<std::string, size_t>> g_entity_slots;
std::optional<std::map<int, size_t>> g_actor_group_slots;
std::map<std::string, size_t> make_entity_slot_map(std::vector<EntityActor>* actors) {
std::map<std::string, size_t> result{};
if (actors) {
for (const auto& e : *actors) {
result.emplace(e.name, e.slot);
}
}
return result;
}
std::map<int, size_t> make_actor_group_slot_list(std::vector<ActorGroup>* actor_groups) {
std::map<int, size_t> result{};
if (actor_groups) {
for (const auto& e : *actor_groups) {
result.emplace(e.id, e.slot);
}
}
return result;
}
size_t gen_pair(DataObjectGenerator& gen, const goos::Object& pair) {
return gen.add_pair(pair, g_entity_slots, g_actor_group_slots);
}
void Region::generate_pairs(DataObjectGenerator& gen, const std::vector<size_t>& pair_slots) {
auto on_enter_slot = pair_slots[0];
auto on_inside_slot = pair_slots[1];
auto on_exit_slot = pair_slots[2];
size_t on_enter_byte = 0;
size_t on_inside_byte = 0;
size_t on_exit_byte = 0;
if (on_enter.has_value()) {
on_enter_byte = gen_pair(gen, *on_enter.value());
} else {
gen.link_word_to_symbol("#f", on_enter_slot);
}
if (on_inside.has_value()) {
on_inside_byte = gen_pair(gen, *on_inside.value());
} else {
gen.link_word_to_symbol("#f", on_inside_slot);
}
if (on_exit.has_value()) {
on_exit_byte = gen_pair(gen, *on_exit.value());
} else {
gen.link_word_to_symbol("#f", on_exit_slot);
}
if (on_enter.has_value()) {
gen.link_word_to_byte(on_enter_slot, on_enter_byte);
}
if (on_inside.has_value()) {
gen.link_word_to_byte(on_inside_slot, on_inside_byte);
}
if (on_exit.has_value()) {
gen.link_word_to_byte(on_exit_slot, on_exit_byte);
}
}
size_t Region::generate(DataObjectGenerator& gen) const {
auto region_slot = gen.add_word(id);
auto on_enter_slot = gen.add_word(0);
auto on_inside_slot = gen.add_word(0);
auto on_exit_slot = gen.add_word(0);
size_t on_enter_byte = 0;
size_t on_inside_byte = 0;
size_t on_exit_byte = 0;
if (on_enter.has_value()) {
on_enter_byte = gen_pair(gen, *on_enter.value());
} else {
gen.link_word_to_symbol("#f", on_enter_slot);
}
if (on_inside.has_value()) {
on_inside_byte = gen_pair(gen, *on_inside.value());
} else {
gen.link_word_to_symbol("#f", on_inside_slot);
}
if (on_exit.has_value()) {
on_exit_byte = gen_pair(gen, *on_exit.value());
} else {
gen.link_word_to_symbol("#f", on_exit_slot);
}
if (on_enter.has_value()) {
gen.link_word_to_byte(on_enter_slot, on_enter_byte);
}
if (on_inside.has_value()) {
gen.link_word_to_byte(on_inside_slot, on_inside_byte);
}
if (on_exit.has_value()) {
gen.link_word_to_byte(on_exit_slot, on_exit_byte);
}
return region_slot;
}
size_t RegionArray::generate(DataObjectGenerator& gen) {
g_entity_slots = make_entity_slot_map(entities);
g_actor_group_slots = make_actor_group_slot_list(actor_groups);
gen.align_to_basic();
gen.add_type_tag("region-array");
size_t result = gen.current_offset_bytes();
gen.add_word(data.size()); // 4 (length)
gen.add_word(data.size()); // 8 (allocated-length)
gen.add_word(0); // 12
ASSERT((gen.current_offset_bytes() % 16) == 0);
for (auto& region : data) {
std::vector<size_t> pairs;
region->slot = gen.add_word(region->id);
pairs.push_back(gen.add_word(0));
pairs.push_back(gen.add_word(0));
pairs.push_back(gen.add_word(0));
pair_slots.emplace(region->id, pairs);
region_slots.emplace(region->id, region->slot);
// region->slot = region->generate(gen);
}
for (auto& region : data) {
auto pairs = pair_slots.find(region->id);
if (pairs != pair_slots.end()) {
region->generate_pairs(gen, pairs->second);
}
}
return result;
}
size_t generate_drawable_tree_region_prim_array(DataObjectGenerator& gen,
RegionArray& regions,
const std::vector<DrawableTreeRegionPrim>& trees) {
gen.align_to_basic();
gen.add_type_tag("array");
size_t result = gen.current_offset_bytes();
auto length = trees.size();
std::vector<size_t> tree_slots;
gen.add_word(length);
gen.add_word(length);
gen.add_type_tag("drawable-tree-region-prim");
tree_slots.reserve(trees.size());
for (auto& tree : trees) {
tree_slots.push_back(gen.add_word(0));
}
gen.align(4);
regions.slot = regions.generate(gen);
for (int i = 0; i < trees.size(); i++) {
gen.link_word_to_byte(tree_slots[i], trees[i].generate(gen));
}
return result;
}
size_t DrawableTreeRegionPrim::generate(DataObjectGenerator& gen) const {
gen.align_to_basic();
gen.add_type_tag("drawable-tree-region-prim");
size_t result = gen.current_offset_bytes();
gen.add_word(1 << 16); // 4, 6 (length)
gen.add_symbol_link(name); // 8
gen.add_word(0);
gen.add_word_float(bsphere.x());
gen.add_word_float(bsphere.y());
gen.add_word_float(bsphere.z());
gen.add_word_float(bsphere.w());
size_t arr_slot = gen.add_word(0);
gen.align(4);
gen.link_word_to_byte(arr_slot, data2.generate(gen));
return result;
}
size_t DrawableInlineArrayRegionPrim::generate(DataObjectGenerator& gen) const {
gen.align_to_basic();
gen.add_type_tag("drawable-inline-array-region-prim");
size_t result = gen.current_offset_bytes();
gen.add_word(data.size() << 16); // 4, 6 (length)
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
for (auto& prim : data) {
prim->generate(gen);
}
// second pass to fill in face and volume references
for (auto& prim : data) {
if (dynamic_cast<DrawableRegionFace*>(prim)) {
auto face = dynamic_cast<DrawableRegionFace*>(prim);
gen.link_word_to_byte(face->face_data_slot, face->data.generate(gen));
}
if (dynamic_cast<DrawableRegionVolume*>(prim)) {
auto vol = dynamic_cast<DrawableRegionVolume*>(prim);
gen.link_word_to_byte(vol->face_array_slot, vol->faces.generate(gen));
}
}
return result;
}
void fill_region_trees(std::vector<DrawableTreeRegionPrim>& trees,
std::map<int, Region>& regions,
RegionArray& region_arr,
const nlohmann::json& json,
u32 base_id) {
for (const auto& [k, tree_json] : json.items()) {
ASSERT_MSG(tree_json.is_object(), fmt::format("key \"{}\" is not an object.", k));
if (tree_json.is_object() && !tree_json.empty()) {
auto& tree = trees.emplace_back(k);
tree.bsphere = vectorm4_from_json(tree_json.at("bsphere"));
add_regions_from_json(tree_json.at("regions"), tree, regions, base_id);
for (auto& [id, region] : regions) {
if (region.tree == tree.name) {
DrawableRegionPrim* prim = nullptr;
if (region.shape == "sphere") {
prim = new DrawableRegionSphere(&region);
tree.data2.data.push_back(prim);
} else if (region.shape == "face") {
prim = new DrawableRegionFace(&region);
tree.data2.data.push_back(prim);
} else if (region.shape == "volume") {
prim = new DrawableRegionVolume(&region);
tree.data2.data.push_back(prim);
} else {
ASSERT_MSG(false, fmt::format("Invalid region shape \"{}\"", region.shape));
}
}
}
}
}
for (auto& [id, region] : regions) {
region_arr.data.push_back(&region);
}
}
void add_regions_from_json(const nlohmann::json& json,
DrawableTreeRegionPrim& tree,
std::map<int, Region>& regions,
u32 base_id) {
std::vector<Region> result;
auto& reader = pretty_print::get_pretty_printer_reader();
for (const auto& region_json : json) {
auto id = region_json.value("id", base_id + regions.size());
auto p = regions.emplace(id, Region());
if (p.second) {
auto& region = p.first->second;
result.push_back(region);
region.id = region_json.value("id", base_id + regions.size());
region.tree = tree.name;
region.shape = region_json.at("shape").get<std::string>();
region.bsphere = vectorm4_from_json(region_json.at("bsphere"));
ASSERT_MSG(region.shape == "sphere" || region.shape == "face" || region.shape == "volume",
"Region must have shape 'sphere', 'face' or 'volume'.");
if (region.shape == "sphere") {
region.faces = std::nullopt;
}
if (region.shape == "face") {
ASSERT_MSG(region_json.find("face") != region_json.end(),
"Face region must have 'face' key.");
auto face = region_json.at("face").get<RegionFaceData>();
std::vector<RegionFaceData> faces;
faces.push_back(face);
region.faces = std::make_optional<std::vector<RegionFaceData>>(faces);
}
if (region.shape == "volume") {
ASSERT_MSG(region_json.find("volume") != region_json.end(),
"Volume region must have 'volume' key.");
auto arr = region_json.at("volume").get<RegionFaceArray>();
region.faces = std::make_optional<std::vector<RegionFaceData>>(arr.faces);
}
if (region_json.find("on-enter") != region_json.end()) {
region.on_enter = std::make_optional(
&reader.read_from_string(region_json.at("on-enter").get<std::string>(), false)
.as_pair()
->car);
}
if (region_json.find("on-inside") != region_json.end()) {
region.on_inside = std::make_optional(
&reader.read_from_string(region_json.at("on-inside").get<std::string>(), false)
.as_pair()
->car);
}
if (region_json.find("on-exit") != region_json.end()) {
region.on_exit = std::make_optional(
&reader.read_from_string(region_json.at("on-exit").get<std::string>(), false)
.as_pair()
->car);
}
lg::print(region.print());
} else {
lg::warn("Duplicate region with ID {} in tree {}, skipped", p.first->first, tree.name);
}
}
}
std::string Region::print() {
std::string result;
result += fmt::format("Region {} ({}):\n", id, tree);
result += fmt::format(" shape: {}\n", shape);
if (on_enter.has_value()) {
result += fmt::format(" on-enter: {}\n", on_enter.value()->print());
} else {
result += fmt::format(" on-enter: #f\n");
}
if (on_inside.has_value()) {
result += fmt::format(" on-inside: {}\n", on_inside.value()->print());
} else {
result += fmt::format(" on-inside: #f\n");
}
if (on_exit.has_value()) {
result += fmt::format(" on-exit: {}\n", on_exit.value()->print());
} else {
result += fmt::format(" on-exit: #f\n");
}
return result;
}
void from_json(const nlohmann::json& json, RegionFaceData& obj) {
obj.normal = vector_from_json(json.at("normal"));
obj.normal.w() *= METER_LENGTH;
std::vector<math::Vector4f> points;
for (auto& p : json.at("points")) {
points.push_back(vectorm3_from_json(p));
}
obj.num_points = points.size();
obj.points = points;
}
void from_json(const nlohmann::json& json, RegionFaceArray& obj) {
(void)obj;
for (const auto& face_json : json.at("faces")) {
RegionFaceData face;
from_json(face_json, face);
obj.faces.push_back(face);
}
}
size_t RegionFaceData::generate(DataObjectGenerator& gen) {
size_t result = gen.current_offset_bytes();
gen.add_word_float(normal.x());
gen.add_word_float(normal.y());
gen.add_word_float(normal.z());
gen.add_word_float(normal.w());
gen.add_word(num_points);
gen.add_word(0);
gen.add_word(0);
gen.add_word(0);
for (auto& p : points) {
gen.add_word_float(p.x());
gen.add_word_float(p.y());
gen.add_word_float(p.z());
gen.add_word_float(p.w());
}
return result;
}
size_t RegionFaceArray::generate(DataObjectGenerator& gen) {
gen.align_to_basic();
gen.add_type_tag("region-face-array");
size_t result = gen.current_offset_bytes();
auto length = data.size();
gen.add_word(length);
gen.add_word(length);
gen.add_word(0);
for (auto& face : data) {
face.generate(gen);
}
for (int i = 0; i < data.size(); i++) {
auto face = data[i];
auto face0 = face.data;
gen.link_word_to_byte(face.face_data_slot, face0.generate(gen));
}
return result;
}
size_t DrawableRegionPrim::generate(DataObjectGenerator& gen) {
return 0;
}
size_t DrawableRegionSphere::generate(DataObjectGenerator& gen) {
gen.align_to_basic();
gen.add_type_tag("drawable-region-sphere");
size_t result = gen.current_offset_bytes();
gen.add_word(region->id);
gen.link_word_to_word(gen.add_word(0), region->slot);
gen.add_word(0);
gen.add_word_float(region->bsphere.x());
gen.add_word_float(region->bsphere.y());
gen.add_word_float(region->bsphere.z());
gen.add_word_float(region->bsphere.w());
return result;
}
size_t DrawableRegionVolume::generate(DataObjectGenerator& gen
/*size_t region_face_array_slot*/) {
gen.align_to_basic();
gen.add_type_tag("drawable-region-volume");
size_t result = gen.current_offset_bytes();
gen.add_word(region->id);
gen.link_word_to_word(gen.add_word(0), region->slot);
face_array_slot = gen.add_word(0);
gen.add_word_float(region->bsphere.x());
gen.add_word_float(region->bsphere.y());
gen.add_word_float(region->bsphere.z());
gen.add_word_float(region->bsphere.w());
return result;
}
size_t DrawableRegionFace::generate(DataObjectGenerator& gen) {
gen.align_to_basic();
gen.add_type_tag("drawable-region-face");
size_t result = gen.current_offset_bytes();
gen.add_word(region->id);
gen.link_word_to_word(gen.add_word(0), region->slot);
face_data_slot = gen.add_word(0);
const auto& bsphere = bsphere_override.has_value() ? bsphere_override.value() : region->bsphere;
gen.add_word_float(bsphere.x());
gen.add_word_float(bsphere.y());
gen.add_word_float(bsphere.z());
gen.add_word_float(bsphere.w());
return result;
}
} // namespace jak3
+207
View File
@@ -0,0 +1,207 @@
#pragma once
#include <cmath>
#include <string>
#include "Entity.h"
#include "common/common_types.h"
#include "common/goos/ParseHelpers.h"
#include "common/goos/Printer.h"
#include "common/math/Vector.h"
#include "goalc/data_compiler/DataObjectGenerator.h"
#include "third-party/json.hpp"
namespace jak3 {
struct RegionFaceData {
// (normal vector :inline :offset-assert 0)
// (normal-offset float :offset 12)
// (num-points uint32 :offset-assert 16)
// (points vector :inline :dynamic :offset-assert 32) ;; guess
math::Vector4f normal; // w component is normal offset
u32 num_points;
std::vector<math::Vector4f> points;
size_t generate(DataObjectGenerator& gen);
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
};
void from_json(const json& j, RegionFaceData& obj);
struct RegionFaceArray;
struct Region {
// (id uint32 :offset-assert 0)
// (on-enter pair :offset-assert 4)
// (on-inside pair :offset-assert 8)
// (on-exit pair :offset-assert 12)
u32 id;
std::optional<goos::Object*> on_enter;
std::optional<goos::Object*> on_inside;
std::optional<goos::Object*> on_exit;
math::Vector4f trans;
math::Vector4f bsphere;
std::string tree; // target, camera, data, water, city_vis, sample, light, entity
std::string shape; // sphere, face, volume
std::optional<std::vector<RegionFaceData>> faces;
size_t slot;
std::optional<std::map<std::string, size_t>> actor_slots;
size_t generate(DataObjectGenerator& gen) const;
void generate_pairs(DataObjectGenerator& gen, const std::vector<size_t>& pair_slots);
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
std::string print();
};
struct RegionArray {
// (data region :inline :dynamic :offset-assert 16)
std::vector<Region*> data;
std::map<int, std::vector<size_t>> pair_slots;
std::map<int, size_t> region_slots;
std::map<std::string, size_t> entity_actor_slots;
std::vector<EntityActor>* entities;
std::vector<ActorGroup>* actor_groups;
size_t slot;
size_t generate(DataObjectGenerator& gen);
size_t add_to_object_file(DataObjectGenerator& gen, size_t region_array) const;
};
struct DrawableRegionPrim {
// (deftype drawable-region-prim (drawable)
// ((region region :offset 8)
// )
Region* region;
explicit DrawableRegionPrim(Region* region) { this->region = region; }
virtual size_t generate(DataObjectGenerator& gen);
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
};
struct DrawableInlineArrayRegionPrim {
// (deftype drawable-inline-array-region-prim (drawable-inline-array)
// ((data drawable-region-prim 1 :inline :offset-assert 32)
// )
std::vector<DrawableRegionPrim*> data;
size_t generate(DataObjectGenerator& gen) const;
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
};
struct DrawableTreeRegionPrim {
// (deftype drawable-tree-region-prim (drawable-tree)
// ((id int16 :offset-assert 4)
// (length int16 :offset 6)
// (name symbol :offset 8)
// (bsphere vector :inline :offset-assert 16)
// (data2 drawable-inline-array :dynamic :offset 32 :score 1))
// )
std::string name;
math::Vector4f bsphere;
DrawableInlineArrayRegionPrim data2;
DrawableTreeRegionPrim() = default;
explicit DrawableTreeRegionPrim(std::string name_) : name(std::move(name_)) {}
size_t generate(DataObjectGenerator& gen) const;
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
};
size_t generate_drawable_tree_region_prim_array(DataObjectGenerator& gen,
RegionArray& regions,
const std::vector<DrawableTreeRegionPrim>& trees);
void fill_region_trees(std::vector<DrawableTreeRegionPrim>& trees,
std::map<int, Region>& regions,
RegionArray& region_arr,
const nlohmann::json& json,
u32 base_id);
void add_regions_from_json(const nlohmann::json& json,
DrawableTreeRegionPrim& tree,
std::map<int, Region>& regions,
u32 base_id);
struct DrawableRegionSphere : DrawableRegionPrim {
using DrawableRegionPrim::DrawableRegionPrim;
size_t generate(DataObjectGenerator& gen) override;
};
struct DrawableRegionFace : DrawableRegionPrim {
// (deftype drawable-region-face (drawable-region-prim)
// ((data region-face-data :offset 12)
// )
RegionFaceData data;
size_t face_data_slot;
std::optional<math::Vector4f> bsphere_override;
explicit DrawableRegionFace(Region* region) : DrawableRegionPrim(region) {
if (region->faces.has_value()) {
data = region->faces.value().at(0);
}
}
size_t generate(DataObjectGenerator& gen) override;
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
};
struct RegionFaceArray {
// (deftype region-face-array (inline-array-class)
// ((data drawable-region-face :inline :dynamic :offset 16)
// )
std::vector<DrawableRegionFace> data;
std::vector<RegionFaceData> faces;
size_t generate(DataObjectGenerator& gen);
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
};
void from_json(const json& j, RegionFaceArray& obj);
struct DrawableRegionVolume : DrawableRegionPrim {
// (deftype drawable-region-volume (drawable-region-prim)
// ((faces region-face-array :offset 12)
// )
RegionFaceArray faces;
size_t face_array_slot;
explicit DrawableRegionVolume(Region* region) : DrawableRegionPrim(region) {
if (this->region->faces.has_value()) {
auto face_arr = this->region->faces.value();
for (const auto& f : face_arr) {
auto& face = faces.data.emplace_back(region);
face.data = f;
// compute per-face bsphere from the face's points for volumes
if (!f.points.empty()) {
math::Vector4f center = {0, 0, 0, 0};
for (auto& pt : f.points) {
center.x() += pt.x();
center.y() += pt.y();
center.z() += pt.z();
}
auto n = static_cast<float>(f.points.size());
center.x() /= n;
center.y() /= n;
center.z() /= n;
float max_dist_sq = 0;
for (auto& pt : f.points) {
float dx = pt.x() - center.x();
float dy = pt.y() - center.y();
float dz = pt.z() - center.z();
max_dist_sq = std::max(max_dist_sq, dx * dx + dy * dy + dz * dz);
}
center.w() = std::sqrt(max_dist_sq);
face.bsphere_override = center;
}
}
}
}
size_t generate(DataObjectGenerator& gen) override;
size_t add_to_object_file(DataObjectGenerator& gen, size_t);
};
} // namespace jak3
+27
View File
@@ -7,6 +7,7 @@
#include "goalc/build_level/jak3/Entity.h"
#include "goalc/build_level/jak3/FileInfo.h"
#include "goalc/build_level/jak3/LevelFile.h"
#include <decompiler/level_extractor/extract_collide_frags.h>
namespace jak3 {
bool run_build_level(const std::string& input_file,
@@ -58,9 +59,19 @@ bool run_build_level(const std::string& input_file,
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++) {
@@ -87,6 +98,22 @@ bool run_build_level(const std::string& input_file,
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
+140
View File
@@ -61,6 +61,146 @@ int DataObjectGenerator::add_word_float(float f) {
return result;
}
void DataObjectGenerator::add_goos_obj(
const goos::Object& obj,
const bool last_obj,
const std::optional<std::map<std::string, size_t>>& entity_slots,
const std::optional<std::map<int, size_t>>& actor_group_slots) {
// lg::info("add_goos_obj: {}", obj.inspect());
size_t cur_slot = 0;
size_t next_slot = 0;
if (obj.type == goos::ObjectType::INTEGER || obj.type == goos::ObjectType::FLOAT) {
cur_slot = add_word(0);
next_slot = add_word(0);
}
switch (obj.type) {
case goos::ObjectType::INTEGER: {
add_type_tag("binteger");
link_word_to_word(cur_slot, add_word(obj.as_int()));
} break;
case goos::ObjectType::FLOAT: {
add_type_tag("bfloat");
link_word_to_word(cur_slot, add_word_float(obj.as_float()));
} break;
case goos::ObjectType::SYMBOL:
add_symbol_link(obj.as_symbol().name_ptr);
break;
case goos::ObjectType::STRING: {
std::string str = obj.as_string()->print();
std::erase(str, '\"');
add_ref_to_string_in_pool(str);
} break;
case goos::ObjectType::PAIR: {
auto pair_slot = add_word(0);
if (last_obj) {
add_empty_list();
} else {
next_slot = add_word(0);
}
link_word_to_byte(pair_slot, add_pair(obj, entity_slots, actor_group_slots));
if (!last_obj) {
link_word_to_byte(next_slot, current_offset_bytes() + 2);
}
} break;
default:
ASSERT_MSG(false, fmt::format("Unsupported object type in pair: {}", obj.inspect()));
}
if (!obj.is_pair()) {
if (last_obj) {
if (obj.type == goos::ObjectType::INTEGER || obj.type == goos::ObjectType::FLOAT) {
link_word_to_symbol("_empty_", next_slot);
} else {
add_empty_list();
}
} else {
if (obj.type == goos::ObjectType::INTEGER || obj.type == goos::ObjectType::FLOAT) {
link_word_to_byte(next_slot, current_offset_bytes() + 2);
} else {
link_word_to_byte(add_word(0), current_offset_bytes() + 2);
}
}
}
}
int DataObjectGenerator::add_pair(const goos::Object& obj,
const std::optional<std::map<std::string, size_t>>& entity_slots,
const std::optional<std::map<int, size_t>>& actor_group_slots) {
size_t pair_slot = current_offset_bytes() + 2;
// lg::info("add_pair: parsing {}", obj.print());
goos::Object current = obj;
while (!current.is_empty_list()) {
// lg::info("add_pair: current {}", current.print());
auto next = current.as_pair()->car;
// special case for entities or actor groups: script pairs can have references to actor groups
// or entities from the level data
if (next.is_pair() && next.as_pair()->car.is_symbol()) {
if (!strcmp(next.as_pair()->car.as_symbol().name_ptr, "entity-actor")) {
if (entity_slots.has_value()) {
auto pair = next.as_pair();
auto type = pair->car;
auto val = pair->cdr.as_pair()->car.as_symbol().name_ptr;
auto slot = entity_slots->find(val);
if (slot != entity_slots->end()) {
link_word_to_byte(add_word(0), slot->second);
if (current.as_pair()->cdr.is_empty_list()) {
add_empty_list();
} else {
link_word_to_byte(add_word(0), current_offset_bytes() + 2);
}
} else {
ASSERT_MSG(false, fmt::format("error in pair {}: for {}, entity-actor {} not found",
obj.print(), pair->print(), val));
}
}
} else if (!strcmp(next.as_pair()->car.as_symbol().name_ptr, "actor-group")) {
if (actor_group_slots.has_value()) {
auto pair = next.as_pair();
auto type = pair->car;
auto val = pair->cdr.as_pair()->car.as_int();
auto slot = actor_group_slots->find(val);
if (slot != actor_group_slots->end()) {
link_word_to_byte(add_word(0), slot->second);
if (current.as_pair()->cdr.is_empty_list()) {
add_empty_list();
} else {
link_word_to_byte(add_word(0), current_offset_bytes() + 2);
}
} else {
ASSERT_MSG(false, fmt::format("error in pair {}: for {}, got invalid id {}",
obj.print(), pair->print(), val));
}
}
} else {
if (current.as_pair()->cdr.is_empty_list()) {
// lg::info("add_pair: next (last elem) {}", next.print());
add_goos_obj(next, true, entity_slots, actor_group_slots);
} else {
// lg::info("add_pair: next {}", next.print());
add_goos_obj(next, false, entity_slots, actor_group_slots);
}
}
} else {
if (current.as_pair()->cdr.is_empty_list()) {
// lg::info("add_pair: next (last elem) {}", next.print());
add_goos_obj(next, true, entity_slots, actor_group_slots);
} else {
// lg::info("add_pair: next {}", next.print());
add_goos_obj(next, false, entity_slots, actor_group_slots);
}
}
current = current.as_pair()->cdr;
}
return pair_slot;
}
int DataObjectGenerator::add_empty_list() {
auto result = int(m_words.size());
m_words.push_back(0);
m_symbol_links["_empty_"].push_back(result);
return result;
}
void DataObjectGenerator::set_word(u32 word_idx, u32 val) {
m_words.at(word_idx) = val;
}
+10
View File
@@ -5,11 +5,21 @@
#include <vector>
#include "common/common_types.h"
#include "common/goos/ParseHelpers.h"
#include "common/log/log.h"
class DataObjectGenerator {
public:
int add_word(u32 word);
int add_word_float(float f);
int add_empty_list();
void add_goos_obj(const goos::Object& obj,
bool last_obj,
const std::optional<std::map<std::string, size_t>>& entity_slots,
const std::optional<std::map<int, size_t>>& actor_group_slots);
int add_pair(const goos::Object& pair,
const std::optional<std::map<std::string, size_t>>& entity_slots,
const std::optional<std::map<int, size_t>>& actor_group_slots);
void set_word(u32 word_idx, u32 val);
void link_word_to_word(int source, int target, int offset = 0);
void link_word_to_byte(int source_word, int target_byte);