From 41fd0b6cec0c9f5a6a1d68ab202a0fc6a61d71c1 Mon Sep 17 00:00:00 2001 From: Hat Kid <6624576+Hat-Kid@users.noreply.github.com> Date: Sat, 13 Jun 2026 00:29:24 +0200 Subject: [PATCH] custom levels: support camera entities and custom keyframe values (#4310) --- .../jak1/levels/test-zone/test-zone.jsonc | 47 ++++++++++++ .../jak2/levels/test-zone/test-zone.jsonc | 3 + .../jak3/levels/test-zone/test-zone.jsonc | 3 + decompiler/config/jak1/all-types.gc | 2 +- decompiler/config/jak2/all-types.gc | 2 +- decompiler/config/jak3/all-types.gc | 2 +- goal_src/jak1/engine/camera/cam-layout.gc | 2 +- goal_src/jak1/engine/entity/entity-h.gc | 8 +- goal_src/jak2/engine/camera/cam-layout.gc | 2 +- goal_src/jak2/engine/entity/entity-h.gc | 12 ++- goal_src/jak3/engine/camera/cam-layout.gc | 2 +- goal_src/jak3/engine/camera/cam-states.gc | 2 +- goal_src/jak3/engine/entity/entity-h.gc | 2 +- goalc/build_level/common/Entity.cpp | 75 +++++++++++++------ goalc/build_level/jak1/Entity.cpp | 67 +++++++++++++++++ goalc/build_level/jak1/Entity.h | 15 ++++ goalc/build_level/jak1/LevelFile.cpp | 4 +- goalc/build_level/jak1/LevelFile.h | 2 - goalc/build_level/jak1/build_level.cpp | 42 +++++++++-- .../jak1/engine/camera/cam-layout_REF.gc | 2 +- .../jak1/engine/entity/entity-h_REF.gc | 2 +- .../jak2/engine/camera/cam-layout_REF.gc | 2 +- .../jak2/engine/entity/entity-h_REF.gc | 2 +- .../jak3/engine/camera/cam-layout_REF.gc | 2 +- .../jak3/engine/camera/cam-states_REF.gc | 2 +- .../jak3/engine/entity/entity-h_REF.gc | 2 +- 26 files changed, 256 insertions(+), 52 deletions(-) diff --git a/custom_assets/jak1/levels/test-zone/test-zone.jsonc b/custom_assets/jak1/levels/test-zone/test-zone.jsonc index 3e7bd77a05..194bb76f95 100644 --- a/custom_assets/jak1/levels/test-zone/test-zone.jsonc +++ b/custom_assets/jak1/levels/test-zone/test-zone.jsonc @@ -26,6 +26,9 @@ // float types: float, meters (1 meter = 4096.0 units), degrees (65536.0 = 360°) // vector types: vector (normal floats), vector4m (meters), vector3m (meters with w set to 1.0), vector-vol (normal floats with w in meters), movie-pos (meters with w in degrees) // special types: symbol, type, string, eco-info, cell-info, buzzer-info, water-height + // by default, lumps will always use the default keyframe value of -1000000000.0 (DEFAULT_RES_TIME). + // most of the actor code will be using that value, but there are some exceptions (camera entities, water volumes, etc.) + // you can specify a different keyframe by suffixing the lump type with e.g. "@0.5", so for a float at keyframe 0.5, the type would be "float@0.5". // // examples: // @@ -137,6 +140,50 @@ } ], + // Camera entities you want to use in your level. These are used to dynamically adjust camera settings/modes when Jak enters a volume. + // There are a couple of different camera modes, depending on what lumps are present: + // - cam-circular: orbits a specified point. requires "pivot" lump. + // - optional lumps: "maxAngle", "focalPull" + // - cam-standoff: requires "align" lump. + // - cam-string: the default camera mode. used when any of these lumps are present to override the default cam mode behavior: + // - "stringMaxLength", "stringMinLength", "stringMaxHeight", "stringMinHeight" + // + // Some generic lumps that can be used for all camera modes: + // - "fov": set the camera fov (default is 64). + // - "interpTime": set the cam blend time when entering the camera entity volume. + // - "tiltAdjust": set the camera roll value. + // + // There's a "flags" lump that takes a cam-slave-options enum value: + // "flags": ["enum-uint32", "(cam-slave-options SAME_SIDE COLLIDE)"] + // + // There's three different volume types (these all have to be defined in keyframe 0, e.g. ["vector-vol@0.0": ...]): + // vol: sets camera entity when Jak is inside. + // pvol: preferred volume when camera entities overlap. + // cutoutvol: disables camera entity when Jak is inside. + // + // "interesting" is an optional vector lump that can be set to be used as a "point of interest" for the camera to focus on. + "cameras": [ + { + "trans": [17.26, 9.0, 13.2], + "quat": [0, 1, 0, 0], + "lump": { + "name": "test-cam", + "flags": ["enum-uint32", "(cam-slave-options SAME_SIDE)"], + "pivot": ["vector3m", [15.0761, 2.6482, 25.548]], + "interpTime": ["float", 1.0], + "vol": [ + "vector-vol@0", + [-0.09046, 0.025624, 0.99557, 32.528575], + [-0.995854, 0.007292, -0.090673, -8.671137], + [0.09046, -0.025624, -0.99557, -16.528584], + [0.995854, -0.007292, 0.090673, 24.671144], + [-0.009584, -0.999645, 0.024858, 0.695884], + [0.009584, 0.999645, -0.024858, 15.304117] + ] + } + } + ], + "actors": [ { "trans": [-21.6238, 20.0496, 17.1191], // translation diff --git a/custom_assets/jak2/levels/test-zone/test-zone.jsonc b/custom_assets/jak2/levels/test-zone/test-zone.jsonc index 7280e29e2e..74ffb96884 100644 --- a/custom_assets/jak2/levels/test-zone/test-zone.jsonc +++ b/custom_assets/jak2/levels/test-zone/test-zone.jsonc @@ -26,6 +26,9 @@ // float types: float, meters (1 meter = 4096.0 units), degrees (65536.0 = 360°) // vector types: vector (normal floats), vector4m (meters), vector3m (meters with w set to 1.0), vector-vol (normal floats with w in meters), movie-pos (meters with w in degrees) // special types: symbol, type, string, eco-info, cell-info, buzzer-info, water-height + // by default, lumps will always use the default keyframe value of -1000000000.0 (DEFAULT_RES_TIME). + // most of the actor code will be using that value, but there are some exceptions (camera entities, water volumes, etc.) + // you can specify a different keyframe by suffixing the lump type with e.g. "@0.5", so for a float at keyframe 0.5, the type would be "float@0.5". // // examples: // diff --git a/custom_assets/jak3/levels/test-zone/test-zone.jsonc b/custom_assets/jak3/levels/test-zone/test-zone.jsonc index 04c969423c..fef662acec 100644 --- a/custom_assets/jak3/levels/test-zone/test-zone.jsonc +++ b/custom_assets/jak3/levels/test-zone/test-zone.jsonc @@ -26,6 +26,9 @@ // float types: float, meters (1 meter = 4096.0 units), degrees (65536.0 = 360°) // vector types: vector (normal floats), vector4m (meters), vector3m (meters with w set to 1.0), vector-vol (normal floats with w in meters), movie-pos (meters with w in degrees) // special types: symbol, type, string, eco-info, cell-info, buzzer-info, water-height + // by default, lumps will always use the default keyframe value of -1000000000.0 (DEFAULT_RES_TIME). + // most of the actor code will be using that value, but there are some exceptions (camera entities, water volumes, etc.) + // you can specify a different keyframe by suffixing the lump type with e.g. "@0.5", so for a float at keyframe 0.5, the type would be "float@0.5". // // examples: // diff --git a/decompiler/config/jak1/all-types.gc b/decompiler/config/jak1/all-types.gc index 865ee7afca..1d84f48932 100644 --- a/decompiler/config/jak1/all-types.gc +++ b/decompiler/config/jak1/all-types.gc @@ -14549,7 +14549,7 @@ (deftype entity-camera (entity) ( - (connect connectable :inline :offset-assert 64) + (quat quaternion :inline :offset-assert 64) ) :method-count-assert 27 :size-assert #x50 diff --git a/decompiler/config/jak2/all-types.gc b/decompiler/config/jak2/all-types.gc index dd72daa169..a7454f8735 100644 --- a/decompiler/config/jak2/all-types.gc +++ b/decompiler/config/jak2/all-types.gc @@ -22506,7 +22506,7 @@ Levels store a reference to this which holds all the entities in the level." ) (deftype entity-camera (entity) - ((connect connectable :inline :offset-assert 64)) ;; guess from jak 1 + ((quat quaternion :inline :offset-assert 64)) :flag-assert #x1b00000050 ) diff --git a/decompiler/config/jak3/all-types.gc b/decompiler/config/jak3/all-types.gc index e6bd26be06..6195351a74 100644 --- a/decompiler/config/jak3/all-types.gc +++ b/decompiler/config/jak3/all-types.gc @@ -26176,7 +26176,7 @@ ) (deftype entity-camera (entity) - ((connect connectable :inline :offset-assert 64) + ((quat quaternion :inline :offset-assert 64) ) :flag-assert #x1b00000050 ) diff --git a/goal_src/jak1/engine/camera/cam-layout.gc b/goal_src/jak1/engine/camera/cam-layout.gc index 437754a08b..9269166b4b 100644 --- a/goal_src/jak1/engine/camera/cam-layout.gc +++ b/goal_src/jak1/engine/camera/cam-layout.gc @@ -641,7 +641,7 @@ (matrix-rotate-z! s1-1 (- (-> s5-0 z))) (matrix*! sv-192 s1-1 sv-192) (matrix->quaternion (the-as quaternion s0-1) sv-192) - (quaternion-inverse! (the-as quaternion s2-0) (the-as quaternion (-> self cam-entity connect))) + (quaternion-inverse! (the-as quaternion s2-0) (-> self cam-entity quat)) (quaternion*! (the-as quaternion s2-0) (the-as quaternion s0-1) (the-as quaternion s2-0)) (quaternion-normalize! (the-as quaternion s2-0)))))) (set! *camera-read-analog* #f) diff --git a/goal_src/jak1/engine/entity/entity-h.gc b/goal_src/jak1/engine/entity/entity-h.gc index 53d357df3b..48a42d0250 100644 --- a/goal_src/jak1/engine/entity/entity-h.gc +++ b/goal_src/jak1/engine/entity/entity-h.gc @@ -48,6 +48,7 @@ (:methods (update-perm! (_type_ symbol entity-perm-status) _type_))) + (deftype entity-links (structure) ((prev-link entity-links) (next-link entity-links) @@ -63,14 +64,17 @@ (:methods (birth? (_type_ vector) symbol))) + (deftype entity-perm-array (inline-array-class) ((data entity-perm :inline :dynamic))) + (set! (-> entity-perm-array heap-base) (the-as uint 16)) (deftype entity-links-array (inline-array-class) ((data entity-links :inline :dynamic))) + (set! (-> entity-links-array heap-base) (the-as uint 64)) (deftype entity (res-lump) @@ -84,7 +88,7 @@ (get-level (_type_) level))) (deftype entity-camera (entity) - ((connect connectable :inline))) + ((quat quaternion :inline))) (deftype entity-ambient-data (structure) ((user-object object 3) @@ -99,9 +103,11 @@ (user-int8 int8 12 :overlay-at (-> user-object 0)) (user-uint8 uint8 12 :overlay-at (-> user-object 0)))) + (deftype entity-ambient-data-array (inline-array-class) ((data entity-ambient-data :inline :dynamic))) + (set! (-> entity-ambient-data-array heap-base) (the-as uint 16)) (deftype entity-ambient (entity) diff --git a/goal_src/jak2/engine/camera/cam-layout.gc b/goal_src/jak2/engine/camera/cam-layout.gc index e6ea1a9c8c..ff3542b587 100644 --- a/goal_src/jak2/engine/camera/cam-layout.gc +++ b/goal_src/jak2/engine/camera/cam-layout.gc @@ -889,7 +889,7 @@ (matrix-rotate-z! s1-1 (- (-> s5-0 z))) (matrix*! sv-192 s1-1 sv-192) (matrix->quaternion (the-as quaternion s0-1) sv-192) - (quaternion-inverse! (the-as quaternion s2-0) (the-as quaternion (-> self cam-entity connect))) + (quaternion-inverse! (the-as quaternion s2-0) (-> self cam-entity quat)) (quaternion*! (the-as quaternion s2-0) (the-as quaternion s0-1) (the-as quaternion s2-0)) (quaternion-normalize! (the-as quaternion s2-0)) ) diff --git a/goal_src/jak2/engine/entity/entity-h.gc b/goal_src/jak2/engine/entity/entity-h.gc index 62a7f15f9f..1b7669557f 100644 --- a/goal_src/jak2/engine/entity/entity-h.gc +++ b/goal_src/jak2/engine/entity/entity-h.gc @@ -69,6 +69,7 @@ (define *generate-actor-vis-output* #f) (deftype entity-perm (structure) + "Entity information that gets persisted in the save file." ((user-object object 2) (user-uint64 uint64 :overlay-at (-> user-object 0)) (user-float float 2 :overlay-at (-> user-object 0)) @@ -91,6 +92,7 @@ (deftype entity-links (structure) + "A linked list of entities with some extra data about their current status." ((prev-link entity-links) (next-link entity-links) (entity entity) @@ -120,6 +122,8 @@ (set! (-> entity-perm-array heap-base) (the-as uint 16)) (deftype entity-links-array (inline-array-class) + "An array of [[entity-links]]. +Levels store a reference to this which holds all the entities in the level." ((data entity-links :inline :dynamic) ) ) @@ -128,6 +132,11 @@ (set! (-> entity-links-array heap-base) (the-as uint 64)) (deftype entity (res-lump) + "Entities are mainly used to place objects in a level. + +As a child of [[res-lump]], they store various types of metadata +in [[res-tag]]s (such as name, curve data, volume data, etc.) +that gets accessed by the accompanying process." ((trans vector :inline) (aid uint32) ) @@ -141,7 +150,7 @@ ) (deftype entity-camera (entity) - ((connect connectable :inline) + ((quat quaternion :inline) ) ) @@ -164,6 +173,7 @@ ) (deftype entity-actor (entity) + "Child class of [[entity]] used to spawn [[process-drawable]] actors." ((etype type :offset 56) (task game-task) (kill-mask task-mask :offset 52) diff --git a/goal_src/jak3/engine/camera/cam-layout.gc b/goal_src/jak3/engine/camera/cam-layout.gc index 703633034a..630b541a56 100644 --- a/goal_src/jak3/engine/camera/cam-layout.gc +++ b/goal_src/jak3/engine/camera/cam-layout.gc @@ -910,7 +910,7 @@ (matrix-rotate-z! s1-1 (- (-> s5-0 z))) (matrix*! sv-192 s1-1 sv-192) (matrix->quaternion s0-1 sv-192) - (quaternion-inverse! (the-as quaternion s2-0) (the-as quaternion (-> self cam-entity connect))) + (quaternion-inverse! (the-as quaternion s2-0) (-> self cam-entity quat)) (quaternion*! (the-as quaternion s2-0) s0-1 (the-as quaternion s2-0)) (quaternion-normalize! (the-as quaternion s2-0)) ) diff --git a/goal_src/jak3/engine/camera/cam-states.gc b/goal_src/jak3/engine/camera/cam-states.gc index 81b3cb3228..e71209ae17 100644 --- a/goal_src/jak3/engine/camera/cam-states.gc +++ b/goal_src/jak3/engine/camera/cam-states.gc @@ -3046,7 +3046,7 @@ (let ((gp-0 (the-as entity-camera (entity-by-name (-> *camera* settings cam-slope))))) (when gp-0 (set! (-> self slope-adjust target) (res-lump-float gp-0 'scale-factor :default 0.6)) - (quaternion-copy! (-> self slope-quat) (the-as quaternion (-> gp-0 connect))) + (quaternion-copy! (-> self slope-quat) (-> gp-0 quat)) ) ) ) diff --git a/goal_src/jak3/engine/entity/entity-h.gc b/goal_src/jak3/engine/entity/entity-h.gc index 216fffaa4f..0180c512c3 100644 --- a/goal_src/jak3/engine/entity/entity-h.gc +++ b/goal_src/jak3/engine/entity/entity-h.gc @@ -135,7 +135,7 @@ that gets accessed by the accompanying process." ) (deftype entity-camera (entity) - ((connect connectable :inline) + ((quat quaternion :inline) ) ) diff --git a/goalc/build_level/common/Entity.cpp b/goalc/build_level/common/Entity.cpp index 9684797aad..7787ef5ce9 100644 --- a/goalc/build_level/common/Entity.cpp +++ b/goalc/build_level/common/Entity.cpp @@ -101,79 +101,86 @@ std::vector enum_from_json(const nlohmann::json& json, decompiler::Decompiler return result; } -static std::unordered_map(const std::string&, - const nlohmann::json&, - decompiler::DecompilerTypeSystem&)>> +static std::unordered_map< + std::string, + std::function(const std::string&, const nlohmann::json&, float, decompiler::DecompilerTypeSystem&)>> lump_map = { // integers {"int32", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { (void)dts; std::vector data; for (size_t i = 1; i < json.size(); i++) { data.push_back(json[i].get()); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"uint32", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { (void)dts; std::vector data; for (size_t i = 1; i < json.size(); i++) { data.push_back(json[i].get()); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"enum-int32", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { std::vector data; for (size_t i = 1; i < json.size(); i++) { data = enum_from_json(json[i], dts); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"enum-uint32", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { std::vector data; for (size_t i = 1; i < json.size(); i++) { data = enum_from_json(json[i], dts); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, // special lumps {"eco-info", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { std::vector data; // pickup-type data.push_back(static_cast(get_enum_val(json[1].get(), dts))); // amount data.push_back(static_cast(get_enum_or_int(json[2], dts))); - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"cell-info", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { std::vector data; // (pickup-type fuel-cell) data.push_back(6); data.push_back(static_cast(get_enum_or_int(json[1], dts))); - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"buzzer-info", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { std::vector data; // (pickup-type buzzer) @@ -181,11 +188,12 @@ static std::unordered_map(get_enum_val(json[1].get(), dts)); auto buzzer = json[2].get(); data.push_back(task + (buzzer * (1 << 16))); - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"water-height", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { std::vector data; // water-height @@ -200,129 +208,140 @@ static std::unordered_map= 6) { data.push_back(json[5].get() * METER_LENGTH); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"symbol", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { (void)dts; std::vector data; for (size_t i = 1; i < json.size(); i++) { data.push_back(json[i].get()); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"type", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { (void)dts; std::vector data; for (size_t i = 1; i < json.size(); i++) { data.push_back(json[i].get()); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"string", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { (void)dts; std::vector data; for (size_t i = 1; i < json.size(); i++) { data.push_back(json[i].get()); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, // vectors {"vector", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { (void)dts; std::vector data; for (size_t i = 1; i < json.size(); i++) { data.push_back(vector_from_json(json[i])); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"vector4m", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { (void)dts; std::vector data; for (size_t i = 1; i < json.size(); i++) { data.push_back(vectorm4_from_json(json[i])); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"vector3m", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { (void)dts; std::vector data; for (size_t i = 1; i < json.size(); i++) { data.push_back(vectorm3_from_json(json[i])); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"movie-pos", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { (void)dts; std::vector data; for (size_t i = 1; i < json.size(); i++) { data.push_back(movie_pos_from_json(json[i])); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"vector-vol", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { (void)dts; std::vector data; for (size_t i = 1; i < json.size(); i++) { data.push_back(vector_vol_from_json(json[i])); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, // floats {"float", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { (void)dts; std::vector data; for (size_t i = 1; i < json.size(); i++) { data.push_back(json[i].get()); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"meters", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { (void)dts; std::vector data; for (size_t i = 1; i < json.size(); i++) { data.push_back(json[i].get() * METER_LENGTH); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}, {"degrees", [](const std::string& name, const nlohmann::json& json, + float time, decompiler::DecompilerTypeSystem& dts) { (void)dts; std::vector data; for (size_t i = 1; i < json.size(); i++) { data.push_back(json[i].get() * DEGREES_LENGTH); } - return std::make_unique(name, data, DEFAULT_RES_TIME); + return std::make_unique(name, data, time); }}}; std::unique_ptr res_from_json_array(const std::string& name, @@ -338,8 +357,16 @@ std::unique_ptr res_from_json_array(const std::string& name, lump.type_name())); } auto array_type = lump.get(); + + float keyframe = DEFAULT_RES_TIME; + auto at_pos = array_type.find('@'); + if (at_pos != std::string::npos) { + keyframe = std::stof(array_type.substr(at_pos + 1)); + array_type = array_type.substr(0, at_pos); + } + if (lump_map.find(array_type) != lump_map.end()) { - return lump_map[array_type](name, json_array, dts); + return lump_map[array_type](name, json_array, keyframe, dts); } else { throw std::runtime_error( fmt::format("unsupported array type for lump {}: {}\n", name, array_type)); diff --git a/goalc/build_level/jak1/Entity.cpp b/goalc/build_level/jak1/Entity.cpp index 0998154349..e81c26e69d 100644 --- a/goalc/build_level/jak1/Entity.cpp +++ b/goalc/build_level/jak1/Entity.cpp @@ -193,4 +193,71 @@ void add_ambients_from_json(const nlohmann::json& json, ambient.res_lump.sort_res(); } } + +size_t EntityCamera::generate(DataObjectGenerator& gen) const { + size_t result = res_lump.generate_header(gen, "entity-camera"); + for (int i = 0; i < 4; i++) { + gen.add_word_float(trans[i]); + } + gen.add_word(aid); + gen.add_word(0); + gen.add_word(0); + gen.add_word(0); + for (int i = 0; i < 4; i++) { + gen.add_word_float(quat[i]); + } + res_lump.generate_tag_list_and_data(gen, result); + return result; +} + +size_t generate_cameras_array(DataObjectGenerator& gen, const std::vector& cameras) { + std::vector camera_locs; + for (auto& cam : cameras) { + camera_locs.push_back(cam.generate(gen)); + } + gen.align_to_basic(); + gen.add_type_tag("array"); + size_t result = gen.current_offset_bytes(); + gen.add_word(cameras.size()); + gen.add_word(cameras.size()); + gen.add_type_tag("entity-camera"); + for (size_t i = 0; i < cameras.size(); i++) { + gen.link_word_to_byte(gen.add_word(0), camera_locs[i]); + } + gen.align(4); + return result; +} + +void add_cameras_from_json(const nlohmann::json& json, + std::vector& camera_list, + u32 base_aid, + decompiler::DecompilerTypeSystem& dts) { + for (const auto& cam_json : json) { + auto& cam = camera_list.emplace_back(); + cam.aid = cam_json.value("aid", base_aid + (u32)camera_list.size()); + cam.trans = vectorm3_from_json(cam_json.at("trans")); + cam.quat = math::Vector4f(0, 1, 0, 0); + if (cam_json.find("quat") != cam_json.end()) { + cam.quat = vector_from_json(cam_json.at("quat")); + } + if (cam_json.find("lump") != cam_json.end()) { + for (const auto& [key, value] : cam_json.at("lump").items()) { + if (value.is_string()) { + std::string value_string = value.get(); + if (value_string.size() > 0 && value_string[0] == '\'') { + cam.res_lump.add_res( + std::make_unique(key, value_string.substr(1), DEFAULT_RES_TIME)); + } else { + cam.res_lump.add_res(std::make_unique(key, value_string, DEFAULT_RES_TIME)); + } + continue; + } + if (value.is_array()) { + cam.res_lump.add_res(res_from_json_array(key, value, dts)); + } + } + } + cam.res_lump.sort_res(); + } +} } // namespace jak1 diff --git a/goalc/build_level/jak1/Entity.h b/goalc/build_level/jak1/Entity.h index d7b3f5c734..3467103641 100644 --- a/goalc/build_level/jak1/Entity.h +++ b/goalc/build_level/jak1/Entity.h @@ -50,4 +50,19 @@ void add_ambients_from_json(const nlohmann::json& json, std::vector& ambient_list, u32 base_aid, decompiler::DecompilerTypeSystem& dts); + +struct EntityCamera { + ResLump res_lump; + u32 aid = 0; + math::Vector4f trans; + math::Vector4f quat; + + size_t generate(DataObjectGenerator& gen) const; +}; + +size_t generate_cameras_array(DataObjectGenerator& gen, const std::vector& cameras); +void add_cameras_from_json(const nlohmann::json& json, + std::vector& camera_list, + u32 base_aid, + decompiler::DecompilerTypeSystem& dts); } // namespace jak1 \ No newline at end of file diff --git a/goalc/build_level/jak1/LevelFile.cpp b/goalc/build_level/jak1/LevelFile.cpp index a49cc34e50..1589b9f5cc 100644 --- a/goalc/build_level/jak1/LevelFile.cpp +++ b/goalc/build_level/jak1/LevelFile.cpp @@ -125,7 +125,7 @@ std::vector LevelFile::save_object_file() const { auto file_info_slot = info.add_to_object_file(gen); gen.link_word_to_byte(1, file_info_slot); - ambient_arr_slot = jak1::generate_inline_array_ambients(gen, ambients); + ambient_arr_slot = generate_inline_array_ambients(gen, ambients); //(bsphere vector :inline :offset-assert 16) //(all-visible-list (pointer uint16) :offset-assert 32) @@ -153,6 +153,8 @@ std::vector LevelFile::save_object_file() const { //(actors drawable-inline-array-actor :offset-assert 112) gen.link_word_to_byte(112 / 4, generate_inline_array_actors(gen, actors)); //(cameras (array entity-camera) :offset-assert 116) + if (!cameras.empty()) + gen.link_word_to_byte(116 / 4, generate_cameras_array(gen, cameras)); //(nodes (inline-array bsp-node) :offset-assert 120) //(level level :offset-assert 124) //(current-leaf-idx uint16 :offset-assert 128) diff --git a/goalc/build_level/jak1/LevelFile.h b/goalc/build_level/jak1/LevelFile.h index 3f718ec2f0..b196966205 100644 --- a/goalc/build_level/jak1/LevelFile.h +++ b/goalc/build_level/jak1/LevelFile.h @@ -45,8 +45,6 @@ struct TextureId {}; struct VisInfo {}; -struct EntityCamera {}; - struct BspNode {}; struct Box8s {}; diff --git a/goalc/build_level/jak1/build_level.cpp b/goalc/build_level/jak1/build_level.cpp index e1a0f49b4d..7255b64626 100644 --- a/goalc/build_level/jak1/build_level.cpp +++ b/goalc/build_level/jak1/build_level.cpp @@ -55,28 +55,54 @@ bool run_build_level(const std::string& input_file, file.nickname = level_json.at("nickname").get(); // vis infos // actors + std::vector aid_list; std::vector actors; auto dts = decompiler::DecompilerTypeSystem(GameVersion::Jak1); dts.parse_type_defs({"decompiler", "config", "jak1", "all-types.gc"}); add_actors_from_json(level_json.at("actors"), actors, level_json.value("base_id", 1234), dts); - std::sort(actors.begin(), actors.end(), [](auto& a, auto& b) { return a.aid < b.aid; }); - auto duplicates = std::adjacent_find(actors.begin(), actors.end(), - [](auto& a, auto& b) { return a.aid == b.aid; }); - ASSERT_MSG(duplicates == actors.end(), + std::ranges::sort(actors, [](auto& a, auto& b) { return a.aid < b.aid; }); + for (auto& actor : actors) { + aid_list.push_back(actor.aid); + } + auto duplicate_actors = + std::ranges::adjacent_find(actors, [](auto& a, auto& b) { return a.aid == b.aid; }); + ASSERT_MSG(duplicate_actors == actors.end(), fmt::format("Actor IDs must be unique. Found at least two actors with ID {}", - duplicates->aid)); + duplicate_actors->aid)); file.actors = std::move(actors); // ambients std::vector ambients; - jak1::add_ambients_from_json(level_json.at("ambients"), ambients, - level_json.value("base_id", 12345), dts); + add_ambients_from_json(level_json.at("ambients"), ambients, level_json.value("base_id", 12345), + dts); file.ambients = std::move(ambients); auto& ambient_drawable_tree = file.drawable_trees.ambients.emplace_back(); (void)ambient_drawable_tree; // cameras + if (level_json.contains("cameras")) { + std::vector cameras; + add_cameras_from_json( + level_json.at("cameras"), cameras, + std::max(level_json.value("base_id", 2345), (int)*std::ranges::max_element(aid_list)), dts); + std::ranges::sort(cameras, [](auto& a, auto& b) { return a.aid < b.aid; }); + for (auto& cam : cameras) { + aid_list.push_back(cam.aid); + } + std::ranges::sort(aid_list, [](auto& a, auto& b) { return a < b; }); + auto duplicate_cams = + std::ranges::adjacent_find(cameras, [](auto& a, auto& b) { return a.aid == b.aid; }); + ASSERT_MSG( + duplicate_cams == cameras.end(), + fmt::format("Actor IDs must be unique. Found at least two camera entities with ID {}", + duplicate_cams->aid)); + auto duplicate_aids = + std::ranges::adjacent_find(aid_list, [](auto& a, auto& b) { return a == b; }); + ASSERT_MSG(duplicate_aids == aid_list.end(), + fmt::format("Actor IDs must be unique. Found one actor and camera entity with ID {}", + *duplicate_aids)); + file.cameras = std::move(cameras); + } // nodes // boxes - // ambients // subdivs // adgifs // actor birth diff --git a/test/decompiler/reference/jak1/engine/camera/cam-layout_REF.gc b/test/decompiler/reference/jak1/engine/camera/cam-layout_REF.gc index c4608f01fa..2a79beec9b 100644 --- a/test/decompiler/reference/jak1/engine/camera/cam-layout_REF.gc +++ b/test/decompiler/reference/jak1/engine/camera/cam-layout_REF.gc @@ -1060,7 +1060,7 @@ (matrix-rotate-z! s1-1 (- (-> s5-0 z))) (matrix*! sv-192 s1-1 sv-192) (matrix->quaternion (the-as quaternion s0-1) sv-192) - (quaternion-inverse! (the-as quaternion s2-0) (the-as quaternion (-> self cam-entity connect))) + (quaternion-inverse! (the-as quaternion s2-0) (-> self cam-entity quat)) (quaternion*! (the-as quaternion s2-0) (the-as quaternion s0-1) (the-as quaternion s2-0)) (quaternion-normalize! (the-as quaternion s2-0)) ) diff --git a/test/decompiler/reference/jak1/engine/entity/entity-h_REF.gc b/test/decompiler/reference/jak1/engine/entity/entity-h_REF.gc index 8a3cc3b923..a4702a6617 100644 --- a/test/decompiler/reference/jak1/engine/entity/entity-h_REF.gc +++ b/test/decompiler/reference/jak1/engine/entity/entity-h_REF.gc @@ -142,7 +142,7 @@ ;; definition of type entity-camera (deftype entity-camera (entity) - ((connect connectable :inline) + ((quat quaternion :inline) ) ) diff --git a/test/decompiler/reference/jak2/engine/camera/cam-layout_REF.gc b/test/decompiler/reference/jak2/engine/camera/cam-layout_REF.gc index c438d481b7..9f36de938f 100644 --- a/test/decompiler/reference/jak2/engine/camera/cam-layout_REF.gc +++ b/test/decompiler/reference/jak2/engine/camera/cam-layout_REF.gc @@ -1093,7 +1093,7 @@ (matrix-rotate-z! s1-1 (- (-> s5-0 z))) (matrix*! sv-192 s1-1 sv-192) (matrix->quaternion (the-as quaternion s0-1) sv-192) - (quaternion-inverse! (the-as quaternion s2-0) (the-as quaternion (-> self cam-entity connect))) + (quaternion-inverse! (the-as quaternion s2-0) (-> self cam-entity quat)) (quaternion*! (the-as quaternion s2-0) (the-as quaternion s0-1) (the-as quaternion s2-0)) (quaternion-normalize! (the-as quaternion s2-0)) ) diff --git a/test/decompiler/reference/jak2/engine/entity/entity-h_REF.gc b/test/decompiler/reference/jak2/engine/entity/entity-h_REF.gc index 004b6e2683..d510d287ca 100644 --- a/test/decompiler/reference/jak2/engine/entity/entity-h_REF.gc +++ b/test/decompiler/reference/jak2/engine/entity/entity-h_REF.gc @@ -234,7 +234,7 @@ that gets accessed by the accompanying process." ;; definition of type entity-camera (deftype entity-camera (entity) - ((connect connectable :inline) + ((quat quaternion :inline) ) ) diff --git a/test/decompiler/reference/jak3/engine/camera/cam-layout_REF.gc b/test/decompiler/reference/jak3/engine/camera/cam-layout_REF.gc index 830ad885ba..d8cf8c96d2 100644 --- a/test/decompiler/reference/jak3/engine/camera/cam-layout_REF.gc +++ b/test/decompiler/reference/jak3/engine/camera/cam-layout_REF.gc @@ -1054,7 +1054,7 @@ (matrix-rotate-z! s1-1 (- (-> s5-0 z))) (matrix*! sv-192 s1-1 sv-192) (matrix->quaternion s0-1 sv-192) - (quaternion-inverse! (the-as quaternion s2-0) (the-as quaternion (-> self cam-entity connect))) + (quaternion-inverse! (the-as quaternion s2-0) (-> self cam-entity quat)) (quaternion*! (the-as quaternion s2-0) s0-1 (the-as quaternion s2-0)) (quaternion-normalize! (the-as quaternion s2-0)) ) diff --git a/test/decompiler/reference/jak3/engine/camera/cam-states_REF.gc b/test/decompiler/reference/jak3/engine/camera/cam-states_REF.gc index 8e2392b81f..1fd0bf9ab6 100644 --- a/test/decompiler/reference/jak3/engine/camera/cam-states_REF.gc +++ b/test/decompiler/reference/jak3/engine/camera/cam-states_REF.gc @@ -3171,7 +3171,7 @@ (let ((gp-0 (the-as entity-camera (entity-by-name (-> *camera* settings cam-slope))))) (when gp-0 (set! (-> self slope-adjust target) (res-lump-float gp-0 'scale-factor :default 0.6)) - (quaternion-copy! (-> self slope-quat) (the-as quaternion (-> gp-0 connect))) + (quaternion-copy! (-> self slope-quat) (-> gp-0 quat)) ) ) ) diff --git a/test/decompiler/reference/jak3/engine/entity/entity-h_REF.gc b/test/decompiler/reference/jak3/engine/entity/entity-h_REF.gc index 4e9056613d..e3e4394c74 100644 --- a/test/decompiler/reference/jak3/engine/entity/entity-h_REF.gc +++ b/test/decompiler/reference/jak3/engine/entity/entity-h_REF.gc @@ -234,7 +234,7 @@ that gets accessed by the accompanying process." ;; definition of type entity-camera (deftype entity-camera (entity) - ((connect connectable :inline) + ((quat quaternion :inline) ) )