From 1254d93fbece019ba6952ff57642c818c2e3266d Mon Sep 17 00:00:00 2001 From: Hat Kid <6624576+Hat-Kid@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:39:21 +0200 Subject: [PATCH] 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. --- .../jak2/levels/test-zone/test-zone.jsonc | 194 +++++++- .../jak3/levels/test-zone/test-zone.jsonc | 194 +++++++- .../jak2/levels/test-zone/test-zone-obs.gc | 34 +- .../jak3/levels/test-zone/test-zone-obs.gc | 30 +- goalc/CMakeLists.txt | 2 + goalc/build_level/common/FileInfo.cpp | 19 +- goalc/build_level/jak2/Entity.cpp | 71 ++- goalc/build_level/jak2/Entity.h | 17 +- goalc/build_level/jak2/LevelFile.cpp | 7 +- goalc/build_level/jak2/LevelFile.h | 17 +- goalc/build_level/jak2/Region.cpp | 423 ++++++++++++++++++ goalc/build_level/jak2/Region.h | 207 +++++++++ goalc/build_level/jak2/build_level.cpp | 27 ++ goalc/build_level/jak3/Entity.cpp | 69 ++- goalc/build_level/jak3/Entity.h | 17 +- goalc/build_level/jak3/LevelFile.cpp | 6 +- goalc/build_level/jak3/LevelFile.h | 16 +- goalc/build_level/jak3/Region.cpp | 423 ++++++++++++++++++ goalc/build_level/jak3/Region.h | 207 +++++++++ goalc/build_level/jak3/build_level.cpp | 27 ++ goalc/data_compiler/DataObjectGenerator.cpp | 140 ++++++ goalc/data_compiler/DataObjectGenerator.h | 10 + 22 files changed, 2111 insertions(+), 46 deletions(-) create mode 100644 goalc/build_level/jak2/Region.cpp create mode 100644 goalc/build_level/jak2/Region.h create mode 100644 goalc/build_level/jak3/Region.cpp create mode 100644 goalc/build_level/jak3/Region.h diff --git a/custom_assets/jak2/levels/test-zone/test-zone.jsonc b/custom_assets/jak2/levels/test-zone/test-zone.jsonc index 5953484bd8..7280e29e2e 100644 --- a/custom_assets/jak2/levels/test-zone/test-zone.jsonc +++ b/custom_assets/jak2/levels/test-zone/test-zone.jsonc @@ -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)" + ] + } } ] } diff --git a/custom_assets/jak3/levels/test-zone/test-zone.jsonc b/custom_assets/jak3/levels/test-zone/test-zone.jsonc index af145a51b1..04c969423c 100644 --- a/custom_assets/jak3/levels/test-zone/test-zone.jsonc +++ b/custom_assets/jak3/levels/test-zone/test-zone.jsonc @@ -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)" + ] + } } ] } diff --git a/goal_src/jak2/levels/test-zone/test-zone-obs.gc b/goal_src/jak2/levels/test-zone/test-zone-obs.gc index 8729fbd32b..a4510585df 100644 --- a/goal_src/jak2/levels/test-zone/test-zone-obs.gc +++ b/goal_src/jak2/levels/test-zone/test-zone-obs.gc @@ -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) + ) \ No newline at end of file diff --git a/goal_src/jak3/levels/test-zone/test-zone-obs.gc b/goal_src/jak3/levels/test-zone/test-zone-obs.gc index 0443f60718..39906295d6 100644 --- a/goal_src/jak3/levels/test-zone/test-zone-obs.gc +++ b/goal_src/jak3/levels/test-zone/test-zone-obs.gc @@ -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) + ) \ No newline at end of file diff --git a/goalc/CMakeLists.txt b/goalc/CMakeLists.txt index 89b5d8a96c..3796f14026 100644 --- a/goalc/CMakeLists.txt +++ b/goalc/CMakeLists.txt @@ -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 diff --git a/goalc/build_level/common/FileInfo.cpp b/goalc/build_level/common/FileInfo.cpp index 34fb3c4972..2aa38da677 100644 --- a/goalc/build_level/common/FileInfo.cpp +++ b/goalc/build_level/common/FileInfo.cpp @@ -1,8 +1,23 @@ #include "FileInfo.h" +#include + #include "common/versions/versions.h" #include "goalc/data_compiler/DataObjectGenerator.h" +#include + +std::string get_current_time_and_date() { + auto const now = std::chrono::floor(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; } diff --git a/goalc/build_level/jak2/Entity.cpp b/goalc/build_level/jak2/Entity.cpp index 3eb1739002..c21df3c033 100644 --- a/goalc/build_level/jak2/Entity.cpp +++ b/goalc/build_level/jak2/Entity.cpp @@ -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& actors) { +size_t generate_inline_array_actors(DataObjectGenerator& gen, std::vector& actors) { std::vector 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(); - 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(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& 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 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& actors, + std::vector& 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>("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 diff --git a/goalc/build_level/jak2/Entity.h b/goalc/build_level/jak2/Entity.h index d26678c143..c1aa76d7f0 100644 --- a/goalc/build_level/jak2/Entity.h +++ b/goalc/build_level/jak2/Entity.h @@ -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& actors); +struct ActorGroup { + u64 id; + std::vector actors; + size_t slot; +}; + +size_t generate_inline_array_actors(DataObjectGenerator& gen, std::vector& actors); +size_t generate_actor_group_array(DataObjectGenerator& gen, std::vector& actor_groups); void add_actors_from_json(const nlohmann::json& json, std::vector& actor_list, u32 base_aid, decompiler::DecompilerTypeSystem& dts); +void add_actor_groups_from_json(const nlohmann::json& json, + const std::vector& actors, + std::vector& actor_groups, + u32 base_id); } // namespace jak2 \ No newline at end of file diff --git a/goalc/build_level/jak2/LevelFile.cpp b/goalc/build_level/jak2/LevelFile.cpp index 1741ea34d9..610b6907af 100644 --- a/goalc/build_level/jak2/LevelFile.cpp +++ b/goalc/build_level/jak2/LevelFile.cpp @@ -66,7 +66,7 @@ size_t generate_u32_array(const std::vector& array, DataObjectGenerator& ge return result; } -std::vector LevelFile::save_object_file() const { +std::vector LevelFile::save_object_file() { DataObjectGenerator gen; gen.add_type_tag("bsp-header"); @@ -78,7 +78,6 @@ std::vector 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 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) diff --git a/goalc/build_level/jak2/LevelFile.h b/goalc/build_level/jak2/LevelFile.h index 611f2f4fef..cde320b918 100644 --- a/goalc/build_level/jak2/LevelFile.h +++ b/goalc/build_level/jak2/LevelFile.h @@ -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 tfrags; std::vector ties; std::vector actors; // unused? - std::vector regions; std::vector 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 actor_groups; // (region-trees (array drawable-tree-region-prim) :offset-assert 188) - std::vector region_trees; + std::map regions; + std::vector 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 save_object_file() const; + std::vector save_object_file(); }; } // namespace jak2 \ No newline at end of file diff --git a/goalc/build_level/jak2/Region.cpp b/goalc/build_level/jak2/Region.cpp new file mode 100644 index 0000000000..ae2c884590 --- /dev/null +++ b/goalc/build_level/jak2/Region.cpp @@ -0,0 +1,423 @@ +#include "Region.h" + +#include "Entity.h" + +#include "common/log/log.h" + +namespace jak2 { + +std::optional> g_entity_slots; +std::optional> g_actor_group_slots; + +std::map make_entity_slot_map(std::vector* actors) { + std::map result{}; + if (actors) { + for (const auto& e : *actors) { + result.emplace(e.name, e.slot); + } + } + return result; +} + +std::map make_actor_group_slot_list(std::vector* actor_groups) { + std::map 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& 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 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& trees) { + gen.align_to_basic(); + gen.add_type_tag("array"); + size_t result = gen.current_offset_bytes(); + auto length = trees.size(); + std::vector 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(prim)) { + auto face = dynamic_cast(prim); + gen.link_word_to_byte(face->face_data_slot, face->data.generate(gen)); + } + if (dynamic_cast(prim)) { + auto vol = dynamic_cast(prim); + gen.link_word_to_byte(vol->face_array_slot, vol->faces.generate(gen)); + } + } + return result; +} + +void fill_region_trees(std::vector& trees, + std::map& 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(®ion); + tree.data2.data.push_back(prim); + } else if (region.shape == "face") { + prim = new DrawableRegionFace(®ion); + tree.data2.data.push_back(prim); + } else if (region.shape == "volume") { + prim = new DrawableRegionVolume(®ion); + 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(®ion); + } +} + +void add_regions_from_json(const nlohmann::json& json, + DrawableTreeRegionPrim& tree, + std::map& regions, + u32 base_id) { + std::vector 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(); + 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(); + std::vector faces; + faces.push_back(face); + region.faces = std::make_optional>(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(); + region.faces = std::make_optional>(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(), 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(), 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(), 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 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 diff --git a/goalc/build_level/jak2/Region.h b/goalc/build_level/jak2/Region.h new file mode 100644 index 0000000000..27055b23ee --- /dev/null +++ b/goalc/build_level/jak2/Region.h @@ -0,0 +1,207 @@ +#pragma once + +#include +#include + +#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 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 on_enter; + std::optional on_inside; + std::optional 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> faces; + + size_t slot; + std::optional> actor_slots; + + size_t generate(DataObjectGenerator& gen) const; + void generate_pairs(DataObjectGenerator& gen, const std::vector& 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 data; + std::map> pair_slots; + std::map region_slots; + std::map entity_actor_slots; + std::vector* entities; + std::vector* 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 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& trees); + +void fill_region_trees(std::vector& trees, + std::map& regions, + RegionArray& region_arr, + const nlohmann::json& json, + u32 base_id); +void add_regions_from_json(const nlohmann::json& json, + DrawableTreeRegionPrim& tree, + std::map& 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 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 data; + std::vector 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(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 \ No newline at end of file diff --git a/goalc/build_level/jak2/build_level.cpp b/goalc/build_level/jak2/build_level.cpp index b350ffac9a..0458599f8d 100644 --- a/goalc/build_level/jak2/build_level.cpp +++ b/goalc/build_level/jak2/build_level.cpp @@ -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 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 diff --git a/goalc/build_level/jak3/Entity.cpp b/goalc/build_level/jak3/Entity.cpp index a31fd2c4c2..41a0676d9a 100644 --- a/goalc/build_level/jak3/Entity.cpp +++ b/goalc/build_level/jak3/Entity.cpp @@ -39,8 +39,7 @@ size_t generate_drawable_actor(DataObjectGenerator& gen, return result; } -size_t generate_inline_array_actors(DataObjectGenerator& gen, - const std::vector& actors) { +size_t generate_inline_array_actors(DataObjectGenerator& gen, std::vector& actors) { std::vector 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(); - 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(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& 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 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& actors, + std::vector& 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>("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 diff --git a/goalc/build_level/jak3/Entity.h b/goalc/build_level/jak3/Entity.h index f4ae154583..eab1ba1291 100644 --- a/goalc/build_level/jak3/Entity.h +++ b/goalc/build_level/jak3/Entity.h @@ -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& actors); +struct ActorGroup { + u64 id; + std::vector actors; + size_t slot; +}; + +size_t generate_inline_array_actors(DataObjectGenerator& gen, std::vector& actors); +size_t generate_actor_group_array(DataObjectGenerator& gen, std::vector& actor_groups); void add_actors_from_json(const nlohmann::json& json, std::vector& actor_list, u32 base_aid, decompiler::DecompilerTypeSystem& dts); +void add_actor_groups_from_json(const nlohmann::json& json, + const std::vector& actors, + std::vector& actor_groups, + u32 base_id); } // namespace jak3 \ No newline at end of file diff --git a/goalc/build_level/jak3/LevelFile.cpp b/goalc/build_level/jak3/LevelFile.cpp index 6154b36080..0a51130cb1 100644 --- a/goalc/build_level/jak3/LevelFile.cpp +++ b/goalc/build_level/jak3/LevelFile.cpp @@ -66,7 +66,7 @@ size_t generate_u32_array(const std::vector& array, DataObjectGenerator& ge return result; } -std::vector LevelFile::save_object_file() const { +std::vector LevelFile::save_object_file() { DataObjectGenerator gen; gen.add_type_tag("bsp-header"); @@ -114,8 +114,12 @@ std::vector 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) diff --git a/goalc/build_level/jak3/LevelFile.h b/goalc/build_level/jak3/LevelFile.h index 996592bd75..f7dd7d4d1b 100644 --- a/goalc/build_level/jak3/LevelFile.h +++ b/goalc/build_level/jak3/LevelFile.h @@ -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 tfrags; std::vector 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 actor_groups; // (region-trees (array drawable-tree-region-prim) :offset-assert 188) - std::vector region_trees; + std::map regions; + std::vector 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 save_object_file() const; + std::vector save_object_file(); }; } // namespace jak3 \ No newline at end of file diff --git a/goalc/build_level/jak3/Region.cpp b/goalc/build_level/jak3/Region.cpp new file mode 100644 index 0000000000..c10a00f695 --- /dev/null +++ b/goalc/build_level/jak3/Region.cpp @@ -0,0 +1,423 @@ +#include "Region.h" + +#include "Entity.h" + +#include "common/log/log.h" + +namespace jak3 { + +std::optional> g_entity_slots; +std::optional> g_actor_group_slots; + +std::map make_entity_slot_map(std::vector* actors) { + std::map result{}; + if (actors) { + for (const auto& e : *actors) { + result.emplace(e.name, e.slot); + } + } + return result; +} + +std::map make_actor_group_slot_list(std::vector* actor_groups) { + std::map 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& 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 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& trees) { + gen.align_to_basic(); + gen.add_type_tag("array"); + size_t result = gen.current_offset_bytes(); + auto length = trees.size(); + std::vector 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(prim)) { + auto face = dynamic_cast(prim); + gen.link_word_to_byte(face->face_data_slot, face->data.generate(gen)); + } + if (dynamic_cast(prim)) { + auto vol = dynamic_cast(prim); + gen.link_word_to_byte(vol->face_array_slot, vol->faces.generate(gen)); + } + } + return result; +} + +void fill_region_trees(std::vector& trees, + std::map& 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(®ion); + tree.data2.data.push_back(prim); + } else if (region.shape == "face") { + prim = new DrawableRegionFace(®ion); + tree.data2.data.push_back(prim); + } else if (region.shape == "volume") { + prim = new DrawableRegionVolume(®ion); + 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(®ion); + } +} + +void add_regions_from_json(const nlohmann::json& json, + DrawableTreeRegionPrim& tree, + std::map& regions, + u32 base_id) { + std::vector 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(); + 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(); + std::vector faces; + faces.push_back(face); + region.faces = std::make_optional>(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(); + region.faces = std::make_optional>(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(), 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(), 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(), 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 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 diff --git a/goalc/build_level/jak3/Region.h b/goalc/build_level/jak3/Region.h new file mode 100644 index 0000000000..8c599c9d5f --- /dev/null +++ b/goalc/build_level/jak3/Region.h @@ -0,0 +1,207 @@ +#pragma once + +#include +#include + +#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 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 on_enter; + std::optional on_inside; + std::optional 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> faces; + + size_t slot; + std::optional> actor_slots; + + size_t generate(DataObjectGenerator& gen) const; + void generate_pairs(DataObjectGenerator& gen, const std::vector& 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 data; + std::map> pair_slots; + std::map region_slots; + std::map entity_actor_slots; + std::vector* entities; + std::vector* 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 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& trees); + +void fill_region_trees(std::vector& trees, + std::map& regions, + RegionArray& region_arr, + const nlohmann::json& json, + u32 base_id); +void add_regions_from_json(const nlohmann::json& json, + DrawableTreeRegionPrim& tree, + std::map& 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 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 data; + std::vector 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(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 \ No newline at end of file diff --git a/goalc/build_level/jak3/build_level.cpp b/goalc/build_level/jak3/build_level.cpp index 932ae679ff..781476a8c6 100644 --- a/goalc/build_level/jak3/build_level.cpp +++ b/goalc/build_level/jak3/build_level.cpp @@ -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 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 diff --git a/goalc/data_compiler/DataObjectGenerator.cpp b/goalc/data_compiler/DataObjectGenerator.cpp index 35f1515df5..6fbf91aeac 100644 --- a/goalc/data_compiler/DataObjectGenerator.cpp +++ b/goalc/data_compiler/DataObjectGenerator.cpp @@ -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>& entity_slots, + const std::optional>& 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>& entity_slots, + const std::optional>& 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; } diff --git a/goalc/data_compiler/DataObjectGenerator.h b/goalc/data_compiler/DataObjectGenerator.h index 5ef2175be0..671d9c1e3b 100644 --- a/goalc/data_compiler/DataObjectGenerator.h +++ b/goalc/data_compiler/DataObjectGenerator.h @@ -5,11 +5,21 @@ #include #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>& entity_slots, + const std::optional>& actor_group_slots); + int add_pair(const goos::Object& pair, + const std::optional>& entity_slots, + const std::optional>& 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);