From b442ddd4799fda56e82a247dd44fbef007484a1f Mon Sep 17 00:00:00 2001 From: Malacath-92 Date: Sat, 3 Sep 2022 11:21:22 +0200 Subject: [PATCH] Some random changes (#251) * Add utility for straight up using Ghidra byte strings * Clarify some structure in Entity/EntityDB * Add missing members to RenderInfo * Add get_max_rope_length * Add return to spawn_tree, plus typo fixes * Fix in info_dump * Disable static assert for MSVC * First draft of spawn rope * Update docs * Clang fix number one * Formatting fixes * Add missing new line at EOF * MrAuto requests * Second draft for rope spawn * Add interface to disable all hooks * Tag vanilla machine rooms with a special tag * Add util for super lazy people * Crazy code to allow making single entities climbable * Fix wrong param in update_movable * Add base_behavior binding * Split entity.hpp/.cpp into three * Add API to disable turning, useful when doing custom state machines and turning logic fudges with the result * Add missing patterns * Fix CI * clone types, no crazy code * Relax construct and destroy interface on allocators * Change hasher on EntityDB::animations * Automated clang-format changes * Remove set_climbable and other unnecessary stuff * some documentation for EntityDB * Fix for missing _ENV in some callbacks * Update docs * clang clang * Add new line at EOF Co-authored-by: Dregu Co-authored-by: Clang-Format Bot --- docs/examples/EntityDB.md | 59 + docs/game_data/spel2.lua | 34 +- docs/generate.py | 2 + docs/generate_emmylua.py | 2 + docs/parse_source.py | 2 + docs/src/includes/_globals.md | 17 +- docs/src/includes/_types.md | 69 +- src/game_api/containers/custom_allocator.hpp | 10 +- src/game_api/containers/game_allocator.hpp | 15 +- .../containers/game_unordered_map.hpp | 4 +- src/game_api/containers/identity_hasher.hpp | 23 + src/game_api/entities_chars.hpp | 2 +- src/game_api/entities_items.cpp | 7 +- src/game_api/entity.cpp | 1705 ++++++++--------- src/game_api/entity.hpp | 690 +++---- src/game_api/entity_db.cpp | 131 ++ src/game_api/entity_db.hpp | 132 ++ src/game_api/entity_hooks_info.hpp | 36 + src/game_api/entity_structs.hpp | 111 ++ src/game_api/ghidra_byte_string.hpp | 79 + src/game_api/layer.cpp | 9 +- src/game_api/layer.hpp | 2 + src/game_api/level_api.cpp | 15 +- src/game_api/level_api.hpp | 1 + src/game_api/movable.hpp | 2 +- src/game_api/movable_behavior.cpp | 77 +- src/game_api/movable_behavior.hpp | 2 + src/game_api/render_api.hpp | 5 +- src/game_api/rpc.cpp | 24 +- src/game_api/rpc.hpp | 5 +- src/game_api/screen.cpp | 12 +- src/game_api/script/lua_backend.cpp | 24 +- src/game_api/script/lua_backend.hpp | 7 + src/game_api/script/lua_vm.cpp | 14 +- .../script/usertypes/behavior_lua.cpp | 2 + src/game_api/script/usertypes/entity_lua.cpp | 17 +- src/game_api/search.cpp | 85 +- src/game_api/search.hpp | 2 +- src/game_api/spawn_api.cpp | 129 +- src/game_api/spawn_api.hpp | 6 +- src/game_api/state.cpp | 68 +- src/game_api/state.hpp | 2 + src/game_api/util.hpp | 8 + src/info_dump/main.cpp | 2 +- src/shared/CMakeLists.txt | 2 +- src/shared/tokenize.h | 104 + src/spel2_dll/spel2.cpp | 4 + src/spel2_dll/spel2.h | 1 + 48 files changed, 2286 insertions(+), 1475 deletions(-) create mode 100644 docs/examples/EntityDB.md create mode 100644 src/game_api/containers/identity_hasher.hpp create mode 100644 src/game_api/entity_db.cpp create mode 100644 src/game_api/entity_db.hpp create mode 100644 src/game_api/entity_hooks_info.hpp create mode 100644 src/game_api/entity_structs.hpp create mode 100644 src/game_api/ghidra_byte_string.hpp create mode 100644 src/shared/tokenize.h diff --git a/docs/examples/EntityDB.md b/docs/examples/EntityDB.md new file mode 100644 index 000000000..7634c3b8f --- /dev/null +++ b/docs/examples/EntityDB.md @@ -0,0 +1,59 @@ +Used to store static common data for an ENT_TYPE. You can also clone entity types with the copy constructor to create new custom entities with different common properties. [This tool](https://dregu.github.io/Spelunky2ls/animation.html) can be helpful when messing with the animations. The default values are also listed in [entities.json](https://github.com/spelunky-fyi/overlunky/blob/main/docs/game_data/entities.json). + +> When cloning an entity type, remember to save it in the script for as long as you need it. Otherwise the memory will be freed immediately, which eventually leads to a crash when used or overwritten by other stuff: + +```lua +-- Create a special fast snake type with weird animation +special_snake = EntityDB:new(ENT_TYPE.MONS_SNAKE) +special_snake.max_speed = 1 +special_snake.acceleration = 2 +special_snake.animations[2].num_tiles = 1 + +set_post_entity_spawn(function(snake) + -- 50% chance to make snakes special + if prng:random_chance(2, PRNG_CLASS.PROCEDURAL_SPAWNS) then + -- Assign custom type + snake.type = special_snake + -- This is only really needed if types are changed during the level + snake.current_animation = special_snake.animations[2] + end +end, SPAWN_TYPE.ANY, MASK.MONSTER, ENT_TYPE.MONS_SNAKE) +``` + +> You can also use Entity.user_data to store the custom type: + +```lua +-- Custom player who is buffed a bit every level +set_callback(function() + -- Doing this to include HH + for i,v in ipairs(get_entities_by_mask(MASK.PLAYER)) do + local player = get_entity(v) + + -- Create new custom type on the first level, based on the original type + if not player.user_data then + player.user_data = {} + player.user_data.type = EntityDB:new(player.type.id) + end + + -- Set the player entity type to the custom type every level + player.type = player.user_data.type + + -- Buff the player every subsequent level + if state.level_count > 0 then + player.type.max_speed = player.type.max_speed * 1.1 + player.type.acceleration = player.type.acceleration * 1.1 + player.type.jump = player.type.jump * 1.1 + end + end +end, ON.POST_LEVEL_GENERATION) +``` + +> Illegal bad example, don't do this: + +```lua +set_callback(function() + -- Nobody owns the new type and the memory is freed immediately, eventually leading to a crash + players[1].type = EntityDB:new(players[1].type) + players[1].type.max_speed = 2 +end, ON.POST_LEVEL_GENERATION) +``` diff --git a/docs/game_data/spel2.lua b/docs/game_data/spel2.lua index b1b4efd61..84317cf5d 100644 --- a/docs/game_data/spel2.lua +++ b/docs/game_data/spel2.lua @@ -319,14 +319,14 @@ function spawn_apep(x, y, layer, right) end ---@param x number ---@param y number ---@param layer LAYER ----@return nil +---@return integer function spawn_tree(x, y, layer) end ---Spawns and grows a tree ---@param x number ---@param y number ---@param layer LAYER ---@param height integer ----@return nil +---@return integer function spawn_tree(x, y, layer, height) end ---Spawns and grows mushroom, height relates to the trunk, without it, it will roll the game default 3-5 height ---Regardless, if there is not enough space, it will spawn shorter one or if there is no space even for the smallest one, it will just not spawn at all @@ -345,6 +345,21 @@ function spawn_mushroom(x, y, l) end ---@param height integer ---@return integer function spawn_mushroom(x, y, l, height) end +---Spawns an already unrolled rope as if created by player +---@param x number +---@param y number +---@param layer LAYER +---@param texture TEXTURE +---@return integer +function spawn_unrolled_player_rope(x, y, layer, texture) end +---Spawns an already unrolled rope as if created by player +---@param x number +---@param y number +---@param layer LAYER +---@param texture TEXTURE +---@param max_length integer +---@return integer +function spawn_unrolled_player_rope(x, y, layer, texture, max_length) end ---Spawn a player in given location, if player of that slot already exist it will spawn clone, the game may crash as this is very unexpected situation ---If you want to respawn a player that is a ghost, set in his inventory `health` to above 0, and `time_of_death` to 0 and call this function, the ghost entity will be removed automatically ---@param player_slot integer @@ -1076,9 +1091,9 @@ function poison_entity(entity_uid) end function modify_ankh_health_gain(max_health, beat_add_health) end ---Adds entity as shop item, has to be movable (haven't tested many) ---@param item_uid integer ----@param shop_owner integer +---@param shop_owner_uid integer ---@return nil -function add_item_to_shop(item_uid, shop_owner) end +function add_item_to_shop(item_uid, shop_owner_uid) end ---Change the amount of frames after the damage from poison is applied ---@param frames integer ---@return nil @@ -1937,6 +1952,7 @@ local function PRNG_random(self, min, max) end ---@field set_ucolor fun(self, color: uColor): Color ---@class Animation + ---@field id integer ---@field first_tile integer ---@field num_tiles integer ---@field interval integer @@ -2022,6 +2038,7 @@ local function PRNG_random(self, min, max) end ---@field get_texture fun(self): TEXTURE ---@field set_texture fun(self, texture_id: TEXTURE): boolean ---@field set_draw_depth fun(self, draw_depth: integer): nil + ---@field set_enable_turning fun(self, enabled: boolean): nil ---@field liberate_from_shop any @&Entity::liberate_from_shop ---@field get_held_entity fun(self): Entity ---@field set_layer fun(self, layer: LAYER): nil @@ -3712,6 +3729,7 @@ local function MovableBehavior_get_state_id(self) end ---@class VanillaMovableBehavior : MovableBehavior ---@class CustomMovableBehavior : MovableBehavior + ---@field base_behavior VanillaMovableBehavior ---@field set_force_state fun(self, force_state: fun(): any): nil ---@field set_on_enter fun(self, on_enter: fun(): any): nil ---@field set_on_exit fun(self, on_exit: fun(): any): nil @@ -4971,6 +4989,14 @@ function Color.new(self, color) end ---@return Color function Color.new(self, r_, g_, b_, a_) end +EntityDB = nil +---@param other EntityDB +---@return EntityDB +function EntityDB.new(self, other) end +---@param other ENT_TYPE +---@return EntityDB +function EntityDB.new(self, other) end + CustomTheme = nil ---Create a new theme with an id and base theme, overriding defaults. Check [theme fun(): anys that are default enabled here](https://github.com/spelunky-fyi/overlunky/blob/main/src/game_api/script/usertypes/level_lua.cpp). ---@param theme_id_ integer diff --git a/docs/generate.py b/docs/generate.py index 9a9be5190..d0b384f0c 100644 --- a/docs/generate.py +++ b/docs/generate.py @@ -23,6 +23,8 @@ "vector<": "array<", "span<": "array<", "unordered_map<": "map<", + "game_map<": "map<", + ", identity_hasher<>": "", "const char*": "string", "wstring": "string", "u16string": "string", diff --git a/docs/generate_emmylua.py b/docs/generate_emmylua.py index a8ea62198..c3f439341 100644 --- a/docs/generate_emmylua.py +++ b/docs/generate_emmylua.py @@ -17,6 +17,8 @@ "vector": "Array", "array": "Array", "unordered_map": "table", + "game_table": "table", + ", identity_hasher<>": "", "const char*": "string", "wstring": "string", "u16string": "string", diff --git a/docs/parse_source.py b/docs/parse_source.py index 57ee5a2da..f1d66c12b 100644 --- a/docs/parse_source.py +++ b/docs/parse_source.py @@ -18,6 +18,8 @@ "../src/game_api/script.hpp", "../src/game_api/color.hpp", "../src/game_api/entity.hpp", + "../src/game_api/entity_db.hpp", + "../src/game_api/entity_structs.hpp", "../src/game_api/movable.hpp", "../src/game_api/movable_behavior.hpp", "../src/game_api/game_manager.hpp", diff --git a/docs/src/includes/_globals.md b/docs/src/includes/_globals.md index 3625858fb..7c8653f79 100644 --- a/docs/src/includes/_globals.md +++ b/docs/src/includes/_globals.md @@ -2237,7 +2237,7 @@ Spawn a [RoomOwner](#RoomOwner) (or a few other like CavemanShopkeeper) in the c > Search script examples for [add_item_to_shop](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=add_item_to_shop) -#### nil add_item_to_shop(int item_uid, int shop_owner) +#### nil add_item_to_shop(int item_uid, int shop_owner_uid) Adds entity as shop item, has to be movable (haven't tested many) @@ -2620,12 +2620,23 @@ If you want to respawn a player that is a ghost, set in his inventory `health` t > Search script examples for [spawn_tree](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=spawn_tree) -#### nil spawn_tree(float x, float y, [LAYER](#LAYER) layer) +#### int spawn_tree(float x, float y, [LAYER](#LAYER) layer) -#### nil spawn_tree(float x, float y, [LAYER](#LAYER) layer, int height) +#### int spawn_tree(float x, float y, [LAYER](#LAYER) layer, int height) Spawns and grows a tree +### spawn_unrolled_player_rope + + +> Search script examples for [spawn_unrolled_player_rope](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=spawn_unrolled_player_rope) + +#### int spawn_unrolled_player_rope(float x, float y, [LAYER](#LAYER) layer, [TEXTURE](#TEXTURE) texture) + +#### int spawn_unrolled_player_rope(float x, float y, [LAYER](#LAYER) layer, [TEXTURE](#TEXTURE) texture, int max_length) + +Spawns an already unrolled rope as if created by player + ## String functions diff --git a/docs/src/includes/_types.md b/docs/src/includes/_types.md index 24a003098..779a16aaa 100644 --- a/docs/src/includes/_types.md +++ b/docs/src/includes/_types.md @@ -287,13 +287,14 @@ int | [target_uid](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=targ int | [timer](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=timer) | int | [state](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=state) | AI state (patrol, sleep, attack, aggro...) int | [trust](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=trust) | Levels completed with, 0..3 -int | [whipped](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=whipped) | How many times master has violated us +int | [whipped](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=whipped) | Number of times whipped by player ### Animation Type | Name | Description ---- | ---- | ----------- +int | [id](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=id) | int | [first_tile](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=first_tile) | int | [num_tiles](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=num_tiles) | int | [interval](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=interval) | @@ -302,8 +303,72 @@ int | [interval](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=interv ### EntityDB +Used to store static common data for an ENT_TYPE. You can also clone entity types with the copy constructor to create new custom entities with different common properties. [This tool](https://dregu.github.io/Spelunky2ls/animation.html) can be helpful when messing with the animations. The default values are also listed in [entities.json](https://github.com/spelunky-fyi/overlunky/blob/main/docs/game_data/entities.json). + +> When cloning an entity type, remember to save it in the script for as long as you need it. Otherwise the memory will be freed immediately, which eventually leads to a crash when used or overwritten by other stuff: + +```lua +-- Create a special fast snake type with weird animation +special_snake = EntityDB:new(ENT_TYPE.MONS_SNAKE) +special_snake.max_speed = 1 +special_snake.acceleration = 2 +special_snake.animations[2].num_tiles = 1 + +set_post_entity_spawn(function(snake) + -- 50% chance to make snakes special + if prng:random_chance(2, PRNG_CLASS.PROCEDURAL_SPAWNS) then + -- Assign custom type + snake.type = special_snake + -- This is only really needed if types are changed during the level + snake.current_animation = special_snake.animations[2] + end +end, SPAWN_TYPE.ANY, MASK.MONSTER, ENT_TYPE.MONS_SNAKE) +``` + +> You can also use Entity.user_data to store the custom type: + +```lua +-- Custom player who is buffed a bit every level +set_callback(function() + -- Doing this to include HH + for i,v in ipairs(get_entities_by_mask(MASK.PLAYER)) do + local player = get_entity(v) + + -- Create new custom type on the first level, based on the original type + if not player.user_data then + player.user_data = {} + player.user_data.type = EntityDB:new(player.type.id) + end + + -- Set the player entity type to the custom type every level + player.type = player.user_data.type + + -- Buff the player every subsequent level + if state.level_count > 0 then + player.type.max_speed = player.type.max_speed * 1.1 + player.type.acceleration = player.type.acceleration * 1.1 + player.type.jump = player.type.jump * 1.1 + end + end +end, ON.POST_LEVEL_GENERATION) +``` + +> Illegal bad example, don't do this: + +```lua +set_callback(function() + -- Nobody owns the new type and the memory is freed immediately, eventually leading to a crash + players[1].type = EntityDB:new(players[1].type) + players[1].type.max_speed = 2 +end, ON.POST_LEVEL_GENERATION) +``` + + + Type | Name | Description ---- | ---- | ----------- +[EntityDB](#EntityDB) | [new(EntityDB other)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=EntityDB) | +[EntityDB](#EntityDB) | [new(ENT_TYPE)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=EntityDB) | [ENT_TYPE](#ENT_TYPE) | [id](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=id) | int | [search_flags](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=search_flags) | [MASK](#MASK) float | [width](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=width) | @@ -3111,6 +3176,7 @@ bool | [overlaps_with(Entity other)](https://github.com/spelunky-fyi/overlunky/s [TEXTURE](#TEXTURE) | [get_texture()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_texture) | bool | [set_texture(TEXTURE texture_id)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_texture) | Changes the entity texture, check the [textures.txt](game_data/textures.txt) for available vanilla textures or use [define_texture](#define_texture) to make custom one nil | [set_draw_depth(int draw_depth)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_draw_depth) | +nil | [set_enable_turning(bool enabled)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_enable_turning) | nil | [liberate_from_shop()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=liberate_from_shop) | [Entity](#Entity) | [get_held_entity()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_held_entity) | nil | [set_layer(LAYER layer)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_layer) | Moves the entity to specified layer, nothing else happens, so this does not emulate a door transition @@ -4864,6 +4930,7 @@ Derived from [MovableBehavior](#MovableBehavior) Type | Name | Description ---- | ---- | ----------- +[VanillaMovableBehavior](#VanillaMovableBehavior) | [base_behavior](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=base_behavior) | nil | [set_force_state(function force_state)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_force_state) | Set the `force_state` function of a `CustomMovableBehavior`, this will be called every frame when
the movable is updated. If an `force_state` is already set it will be overridden. The signature
of the function is `bool force_state(movable, base_fun)`, when the function returns `true` the movable will
enter this behavior. If no base behavior is set `base_fun` will be `nil`. nil | [set_on_enter(function on_enter)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_on_enter) | Set the `on_enter` function of a `CustomMovableBehavior`, this will be called when the movable
enters the state. If an `on_enter` is already set it will be overridden. The signature of the
function is `nil on_enter(movable, base_fun))`. If no base behavior is set `base_fun` will be `nil`. nil | [set_on_exit(function on_exit)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_on_exit) | Set the `on_exit` function of a `CustomMovableBehavior`, this will be called when the movable
leaves the state. If an `on_exit` is already set it will be overridden. The signature of the
function is `nil on_exit(movable, base_fun))`. If no base behavior is set `base_fun` will be `nil`. diff --git a/src/game_api/containers/custom_allocator.hpp b/src/game_api/containers/custom_allocator.hpp index e86b886c1..7f4d058aa 100644 --- a/src/game_api/containers/custom_allocator.hpp +++ b/src/game_api/containers/custom_allocator.hpp @@ -59,13 +59,15 @@ struct custom_allocator new (static_cast(p)) T(val); } - void construct(pointer p) + template + void construct(U* const p, Args&&... args) { - new (static_cast(p)) T(); + new (static_cast(p)) U(std::forward(args)...); } - void destroy(pointer p) + template + void destroy(U* const p) { - p->~T(); + p->~U(); } }; diff --git a/src/game_api/containers/game_allocator.hpp b/src/game_api/containers/game_allocator.hpp index c7a487674..91f870b38 100644 --- a/src/game_api/containers/game_allocator.hpp +++ b/src/game_api/containers/game_allocator.hpp @@ -55,18 +55,15 @@ struct game_allocator game_free(p); } - void construct(pointer p, const T& val) + template + void construct(U* const p, Args&&... args) { - new (static_cast(p)) T(val); + new (static_cast(p)) U(std::forward(args)...); } - void construct(pointer p) - { - new (static_cast(p)) T(); - } - - void destroy(pointer p) + template + void destroy(U* const p) { - p->~T(); + p->~U(); } }; diff --git a/src/game_api/containers/game_unordered_map.hpp b/src/game_api/containers/game_unordered_map.hpp index 7d47400e9..8b7006cf1 100644 --- a/src/game_api/containers/game_unordered_map.hpp +++ b/src/game_api/containers/game_unordered_map.hpp @@ -4,5 +4,5 @@ #include -template -using game_unordered_map = std::unordered_map, std::equal_to, game_allocator>>; +template , class Equal = std::equal_to> +using game_unordered_map = std::unordered_map>>; diff --git a/src/game_api/containers/identity_hasher.hpp b/src/game_api/containers/identity_hasher.hpp new file mode 100644 index 000000000..3ed14ff29 --- /dev/null +++ b/src/game_api/containers/identity_hasher.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +template +struct identity_hasher +{ + [[nodiscard]] size_t operator()(const T& val) const noexcept + { + return static_cast(val); + } +}; + +template <> +struct identity_hasher +{ + template + [[nodiscard]] size_t operator()(const T& val) const noexcept + { + return static_cast(val); + } +}; diff --git a/src/game_api/entities_chars.hpp b/src/game_api/entities_chars.hpp index 7b28e6160..980358851 100644 --- a/src/game_api/entities_chars.hpp +++ b/src/game_api/entities_chars.hpp @@ -38,7 +38,7 @@ class Ai int8_t unknown12; /// Levels completed with, 0..3 uint8_t trust; - /// How many times master has violated us + /// Number of times whipped by player uint8_t whipped; int8_t unknown15; int8_t unknown16; diff --git a/src/game_api/entities_items.cpp b/src/game_api/entities_items.cpp index 252221eb7..ee24038c1 100644 --- a/src/game_api/entities_items.cpp +++ b/src/game_api/entities_items.cpp @@ -4,9 +4,10 @@ #include // for move #include // for vector, _Vector_iterator, _Vector_cons... -#include "entities_chars.hpp" // for PowerupCapable -#include "entity.hpp" // for HookWithId, EntityHooksInfo, to_id, SHAPE -#include "vtable_hook.hpp" // for hook_vtable +#include "entities_chars.hpp" // for PowerupCapable +#include "entity.hpp" // for to_id, SHAPE +#include "entity_hooks_info.hpp" // for HookWithId, EntityHooksInfo +#include "vtable_hook.hpp" // for hook_vtable void ParachutePowerup::deploy() { diff --git a/src/game_api/entity.cpp b/src/game_api/entity.cpp index 7448ff72d..96f6f21e4 100644 --- a/src/game_api/entity.cpp +++ b/src/game_api/entity.cpp @@ -1,898 +1,807 @@ -#include "entity.hpp" - -#include // for IsBadWritePtr -#include // for operator<=>, operator-, operator+ -#include // for round -#include // for operator<, operator<=, operator> -#include // for uint32_t, uint16_t, uint8_t -#include // for abs, NULL, size_t -#include // for _List_const_iterator -#include // for _Tree_iterator, map, _Tree_cons... -#include // for operator new -#include // for allocator, string, operator""sv -#include // for sleep_for -#include // for vector, _Vector_iterator, erase_if - -#include "containers/custom_map.hpp" // for custom_map -#include "entities_chars.hpp" // for Player -#include "memory.hpp" // for write_mem_prot -#include "movable.hpp" // for Movable -#include "movable_behavior.hpp" // for MovableBehavior -#include "render_api.hpp" // for RenderInfo -#include "search.hpp" // for get_address -#include "state.hpp" // for State, StateMemory, enum_to_layer -#include "state_structs.hpp" // for LiquidPhysicsEngine -#include "texture.hpp" // for get_texture, Texture -#include "vtable_hook.hpp" // for hook_vtable, hook_dtor, unregis... - -template -class OnHeapPointer; - -using namespace std::chrono_literals; -using EntityMap = std::unordered_map; - -std::vector g_entity_hooks; - -struct EntityBucket -{ - void** begin; - void** current; // Note, counts down from end to begin instead of up from begin to end :shrug: - void** end; -}; -struct EntityPool -{ - std::uint32_t slot_size; - std::uint32_t initial_slots; - std::uint32_t slots_growth; - std::uint32_t current_slots; - std::uint64_t _ulong_0; - EntityBucket* _some_bucket; - EntityBucket* bucket; -}; -struct EntityFactory -{ - EntityDB types[0x395]; - bool type_set[0x395]; - std::unordered_map> entity_instance_map; - EntityMap entity_map; - void* _ptr_7; -}; - -EntityFactory* entity_factory() -{ - static EntityFactory* cache_entity_factory = *(EntityFactory**)get_address("entity_factory"sv); - while (cache_entity_factory == 0) - { - std::this_thread::sleep_for(500ms); - cache_entity_factory = *(EntityFactory**)get_address("entity_factory"sv); - } - return cache_entity_factory; -} - -std::vector list_entities() -{ - const EntityFactory* entity_factory_ptr = entity_factory(); - if (!entity_factory_ptr) - return {}; - - const EntityMap& map = entity_factory_ptr->entity_map; - - std::vector result; - for (const auto& [name, id] : map) - { - result.emplace_back(name, id); - } - return result; -} - -EntityDB* get_type(uint32_t id) -{ - EntityFactory* entity_factory_ptr = entity_factory(); - - // Special case: map_ptr might be 0 if it's not initialized. - // This only occurs in list_entities; for others, do not check the pointer - // to see if this assumption works. - if (!entity_factory_ptr) - return nullptr; - - return entity_factory_ptr->types + id; -} - -ENT_TYPE to_id(std::string_view name) -{ - const EntityFactory* entity_factory_ptr = entity_factory(); - if (!entity_factory_ptr) - return {}; - const EntityMap& map = entity_factory_ptr->entity_map; - auto it = map.find(std::string(name)); - return it != map.end() ? it->second : -1; -} - -std::string_view to_name(ENT_TYPE id) -{ - const EntityFactory* entity_factory_ptr = entity_factory(); - if (entity_factory_ptr) - { - for (const auto& [name, type_id] : entity_factory_ptr->entity_map) - { - if (type_id == id) - { - return name; - } - } - } - - return {}; -} - -void Entity::teleport(float dx, float dy, bool s, float vx, float vy, bool snap) -{ - if (overlay) - overlay->remove_item(uid); - overlay = NULL; - auto topmost = topmost_mount(); - if (!s) - { - auto [x_pos, y_pos] = topmost->position(); - // player relative coordinates - x_pos += dx; - y_pos += dy; - if (snap) - { - x_pos = round(x_pos); - y_pos = round(y_pos); - } - topmost->x = x_pos; - topmost->y = y_pos; - } - else - { - // screen coordinates -1..1 - // log::debug!("Teleporting to screen {}, {}", x, y); - auto state = State::get(); - auto [x_pos, y_pos] = state.click_position(dx, dy); - if (snap && abs(vx) + abs(vy) <= 0.04f) - { - x_pos = round(x_pos); - y_pos = round(y_pos); - } - // log::debug!("Teleporting to {}, {}", x, y); - topmost->x = x_pos; - topmost->y = y_pos; - } - // set velocity - if (topmost->is_movable()) - { - auto movable_ent = (Movable*)topmost; - movable_ent->velocityx = vx; - movable_ent->velocityy = vy; - } - return; -} - -void Entity::teleport_abs(float dx, float dy, float vx, float vy) -{ - if (overlay) - overlay->remove_item(uid); - overlay = NULL; - x = dx; - y = dy; - if (is_movable()) - { - auto movable_ent = this->as(); - movable_ent->velocityx = vx; - movable_ent->velocityy = vy; - } -} - -void Entity::set_layer(LAYER layer_to) -{ - uint8_t dest_layer = enum_to_layer(layer_to); - if (layer == dest_layer) - return; - - auto state = State::get(); - if (this != this->topmost_mount()) - this->topmost_mount()->set_layer(layer_to); - - if (layer == 0 || layer == 1) - { - auto ptr_from = state.ptr()->layers[layer]; - - using RemoveFromLayer = void(Layer*, Entity*); - static RemoveFromLayer* remove_from_layer = (RemoveFromLayer*)get_address("remove_from_layer"); - remove_from_layer(ptr_from, this); - } - - auto ptr_to = state.ptr()->layers[dest_layer]; - - using AddToLayer = void(Layer*, Entity*); - static AddToLayer* add_to_layer = (AddToLayer*)get_address("add_to_layer"); - add_to_layer(ptr_to, this); - - for (auto item : items.entities()) - { - item->set_layer(layer_to); - } -} - -void Entity::remove() -{ - if (layer != 2) - { - auto state = State::get(); - auto ptr_from = state.ptr()->layers[layer]; - if ((this->type->search_flags & 1) == 0 || ((Player*)this)->ai != 0) - { - using RemoveFromLayer = void(Layer*, Entity*); - static RemoveFromLayer* remove_from_layer = (RemoveFromLayer*)get_address("remove_from_layer"); - remove_from_layer(ptr_from, this); - - for (auto item : items.entities()) - { - item->remove(); - } - } - layer = 2; - } -} - -void Entity::respawn(LAYER layer_to) -{ - set_layer(layer_to); -} - -void Entity::perform_teleport(uint8_t delta_x, uint8_t delta_y) -{ - using TeleportFun = void(Entity*, uint8_t, uint8_t); - static TeleportFun* tp = (TeleportFun*)get_address("teleport"); - tp(this, delta_x, delta_y); -} - -std::pair Entity::position() -{ - auto [x_pos, y_pos] = position_self(); - - // overlay exists if player is riding something / etc - Entity* overlay_nested = overlay; - while (overlay_nested != nullptr) - { - x_pos += overlay_nested->x; - y_pos += overlay_nested->y; - overlay_nested = overlay_nested->overlay; - } - return {x_pos, y_pos}; -} - -std::pair Entity::position_self() const -{ - return std::pair(x, y); -} - -void Entity::remove_item(uint32_t item_uid) -{ - auto entity = get_entity_ptr(item_uid); - if (entity) - remove_item_ptr(entity); -} - -void Movable::poison(int16_t frames) -{ - static size_t offset_first = 0; - static size_t offset_subsequent = 0; - if (offset_first == 0) - { - offset_first = get_address("first_poison_tick_timer_default"); - offset_subsequent = get_address("subsequent_poison_tick_timer_default"); - } - poison_tick_timer = frames; - - if (frames == -1) - { - frames = 1800; - } - write_mem_prot(offset_first, frames, true); - write_mem_prot(offset_subsequent, frames, true); -} - -bool Movable::is_poisoned() -{ - return (poison_tick_timer != -1); -} - -void Movable::broken_damage(uint32_t damage_dealer_uid, int8_t damage_amount, uint16_t stun_time, float velocity_x, float velocity_y) -{ - damage(damage_dealer_uid, damage_amount, stun_time, velocity_x, velocity_y, 80); -} - -void Movable::damage(uint32_t damage_dealer_uid, int8_t damage_amount, uint16_t stun_time, float velocity_x, float velocity_y, uint16_t iframes) -{ - if ((flags & (1 << 28)) > 0) - { - return; - } - - auto dealer = get_entity_ptr(damage_dealer_uid); - if (dealer == nullptr) - { - return; - } - - float velocities[] = {velocity_x, velocity_y}; - float unknown[] = {0.0f, 0.0f}; - on_regular_damage(dealer, damage_amount, 0x1000, velocities, unknown, stun_time, iframes); -} - -bool Movable::is_button_pressed(BUTTON button) -{ - return (buttons & button) == button && (buttons_previous & button) == 0; -} -bool Movable::is_button_held(BUTTON button) -{ - return (buttons & button) == button && (buttons_previous & button) == button; -} -bool Movable::is_button_released(BUTTON button) -{ - return (buttons & button) == 0 && (buttons_previous & button) == button; -} - -void hook_movable_state_machine(Movable* _self) -{ - hook_vtable( - _self, - [](Movable* self, void (*original)(Movable*)) - { - EntityHooksInfo& hook_info = self->get_hooks(); - - bool skip_orig = false; - for (auto& [id, pre] : hook_info.pre_statemachine) - { - if (pre(self)) - { - skip_orig = true; - } - } - - if (!skip_orig) - { - original(self); - } - - for (auto& [id, post] : hook_info.post_statemachine) - { - post(self); - } - }, - 0x2); -} -void Movable::set_pre_statemachine(std::uint32_t reserved_callback_id, std::function pre_state_machine) -{ - EntityHooksInfo& hook_info = get_hooks(); - if (hook_info.pre_statemachine.empty() && hook_info.post_statemachine.empty()) - { - hook_movable_state_machine(this); - } - hook_info.pre_statemachine.push_back({reserved_callback_id, std::move(pre_state_machine)}); -} -void Movable::set_post_statemachine(std::uint32_t reserved_callback_id, std::function post_state_machine) -{ - EntityHooksInfo& hook_info = get_hooks(); - if (hook_info.pre_statemachine.empty() && hook_info.post_statemachine.empty()) - { - hook_movable_state_machine(this); - } - hook_info.post_statemachine.push_back({reserved_callback_id, std::move(post_state_machine)}); -} - -std::tuple get_position(uint32_t uid) -{ - Entity* ent = get_entity_ptr(uid); - if (ent) - return std::make_tuple(ent->position().first, ent->position().second, ent->layer); - - return {0.0f, 0.0f, (uint8_t)0}; -} - -std::tuple get_render_position(uint32_t uid) -{ - Entity* ent = get_entity_ptr(uid); - if (ent) - { - if (ent->rendering_info != nullptr && !ent->rendering_info->render_inactive) - return std::make_tuple(ent->rendering_info->x, ent->rendering_info->y, ent->layer); - else - return get_position(uid); - } - return {0.0f, 0.0f, (uint8_t)0}; -} - -std::tuple get_velocity(uint32_t uid) -{ - if (Entity* ent = get_entity_ptr(uid)) - { - float vx{0.0f}; - float vy{0.0f}; - if (ent->is_movable()) - { - Movable* mov = ent->as(); - vx = mov->velocityx; - vy = mov->velocityy; - } - else if (ent->is_liquid()) - { - auto liquid_engine = State::get().get_correct_liquid_engine(ent->type->id); - vx = liquid_engine->entity_velocities->first; - vy = liquid_engine->entity_velocities->second; - } - if (ent->overlay) - { - auto [ovx, ovy] = get_velocity(ent->overlay->uid); - vx += ovx; - vy += ovy; - } - return std::tuple{vx, vy}; - } - return std::tuple{0.0f, 0.0f}; -} - -AABB get_hitbox(uint32_t uid, bool use_render_pos) -{ - if (Entity* ent = get_entity_ptr(uid)) - { - auto [x, y, l] = (use_render_pos ? get_render_position : get_position)(uid); - return AABB{ - x - ent->hitboxx + ent->offsetx, - y + ent->hitboxy + ent->offsety, - x + ent->hitboxx + ent->offsetx, - y - ent->hitboxy + ent->offsety, - }; - } - return AABB{0.0f, 0.0f, 0.0f, 0.0f}; -} - -TEXTURE Entity::get_texture() -{ - if (texture) - return texture->id; - - return -1; -} -bool Entity::set_texture(TEXTURE texture_id) -{ - if (auto* new_texture = ::get_texture(texture_id)) - { - apply_texture(new_texture); - return true; - } - return false; -} - -void Entity::unhook(std::uint32_t id) -{ - auto it = std::find_if(g_entity_hooks.begin(), g_entity_hooks.end(), [this](auto& hook) - { return hook.entity == uid; }); - if (it != g_entity_hooks.end()) - { - std::erase_if(it->on_dtor, [id](auto& hook) - { return hook.id == id; }); - std::erase_if(it->on_destroy, [id](auto& hook) - { return hook.id == id; }); - std::erase_if(it->on_kill, [id](auto& hook) - { return hook.id == id; }); - std::erase_if(it->on_player_instagib, [id](auto& hook) - { return hook.id == id; }); - std::erase_if(it->on_damage, [id](auto& hook) - { return hook.id == id; }); - std::erase_if(it->pre_floor_update, [id](auto& hook) - { return hook.id == id; }); - std::erase_if(it->post_floor_update, [id](auto& hook) - { return hook.id == id; }); - std::erase_if(it->pre_statemachine, [id](auto& hook) - { return hook.id == id; }); - std::erase_if(it->post_statemachine, [id](auto& hook) - { return hook.id == id; }); - std::erase_if(it->on_open, [id](auto& hook) - { return hook.id == id; }); - std::erase_if(it->pre_collision1, [id](auto& hook) - { return hook.id == id; }); - std::erase_if(it->pre_collision2, [id](auto& hook) - { return hook.id == id; }); - std::erase_if(it->pre_render, [id](auto& hook) - { return hook.id == id; }); - std::erase_if(it->post_render, [id](auto& hook) - { return hook.id == id; }); - } -} -EntityHooksInfo& Entity::get_hooks() -{ - auto it = std::find_if(g_entity_hooks.begin(), g_entity_hooks.end(), [this](auto& hook) - { return hook.entity == uid; }); - if (it == g_entity_hooks.end()) - { - hook_dtor(this, [](void* self) - { - auto _it = std::find_if(g_entity_hooks.begin(), g_entity_hooks.end(), [self](auto& hook) - { return hook.entity == ((Entity*)self)->uid; }); - if (_it != g_entity_hooks.end()) - { - for (auto& cb : _it->on_dtor) - { - cb.fun((Entity*)self); - } - g_entity_hooks.erase(_it); - } }); - g_entity_hooks.push_back({uid}); - return g_entity_hooks.back(); - } - return *it; -} - -std::uint32_t Entity::set_on_dtor(std::function cb) -{ - EntityHooksInfo& hook_info = get_hooks(); - hook_info.on_dtor.push_back({hook_info.cbcount++, std::move(cb)}); - return hook_info.on_dtor.back().id; -} -std::uint32_t Entity::reserve_callback_id() -{ - EntityHooksInfo& hook_info = get_hooks(); - return hook_info.cbcount++; -} -void Entity::set_on_destroy(std::uint32_t reserved_callback_id, std::function on_destroy) -{ - EntityHooksInfo& hook_info = get_hooks(); - if (hook_info.on_destroy.empty()) - { - hook_vtable( - this, - [](Entity* self, void (*original)(Entity*)) - { - EntityHooksInfo& _hook_info = self->get_hooks(); - for (auto& [id, on_destroy] : _hook_info.on_destroy) - { - on_destroy(self); - } - original(self); - }, - 0x5); - } - hook_info.on_destroy.push_back({reserved_callback_id, std::move(on_destroy)}); -} -void Entity::set_on_kill(std::uint32_t reserved_callback_id, std::function on_kill) -{ - EntityHooksInfo& hook_info = get_hooks(); - if (hook_info.on_kill.empty()) - { - hook_vtable( - this, - [](Entity* self, bool _some_bool, Entity* from, void (*original)(Entity*, bool, Entity*)) - { - EntityHooksInfo& _hook_info = self->get_hooks(); - for (auto& [id, on_kill] : _hook_info.on_kill) - { - on_kill(self, from); - } - original(self, _some_bool, from); - }, - 0x3); - } - hook_info.on_kill.push_back({reserved_callback_id, std::move(on_kill)}); -} - -void Entity::set_on_player_instagib(std::uint32_t reserved_callback_id, std::function on_instagib) -{ - EntityHooksInfo& hook_info = get_hooks(); - // no hooking here, because the instagib function is hooked in rpc.cpp - hook_info.on_player_instagib.push_back({reserved_callback_id, std::move(on_instagib)}); -} - -void Entity::set_on_damage(std::uint32_t reserved_callback_id, std::function on_damage) -{ - EntityHooksInfo& hook_info = get_hooks(); - if (hook_info.on_damage.empty()) - { - if ((this->type->search_flags & 0x1) == 0x1) - { - // Can't hook player::on_damage here, because this is permanently hooked for the god function. - // The god function takes care of calling the script hooks in rpc.cpp - } - else - { - hook_vtable( - this, - [](Entity* self, Entity* damage_dealer, int8_t damage_amount, uint32_t unknown1, float* velocities, float* unknown2, uint16_t stun_amount, uint8_t iframes, void (*original)(Entity*, Entity*, int8_t, uint32_t, float*, float*, uint16_t, uint8_t)) - { - EntityHooksInfo& _hook_info = self->get_hooks(); - bool skip_orig = false; - for (auto& [id, on_damage] : _hook_info.on_damage) - { - if (on_damage(self, damage_dealer, damage_amount, velocities[0], velocities[1], stun_amount, iframes)) - { - skip_orig = true; - } - } - - if (!skip_orig) - { - original(self, damage_dealer, damage_amount, unknown1, velocities, unknown2, stun_amount, iframes); - } - }, - 0x30); - } - } - hook_info.on_damage.push_back({reserved_callback_id, std::move(on_damage)}); -} - -auto hook_update_callback(Entity* self) -{ - hook_vtable( - self, - [](Entity* entity, void (*original)(Entity*)) - { - EntityHooksInfo& _hook_info = entity->get_hooks(); - bool skip_original{false}; - for (auto& [id, pre] : _hook_info.pre_floor_update) - { - if (pre(entity)) - { - skip_original = true; - break; - } - } - if (!skip_original) - { - original(entity); - } - for (auto& [id, post] : _hook_info.post_floor_update) - { - post(entity); - } - }, - 0x26); -} -void Entity::set_pre_floor_update(std::uint32_t reserved_callback_id, std::function pre_update) -{ - EntityHooksInfo& hook_info = get_hooks(); - if (hook_info.pre_floor_update.empty() && hook_info.post_floor_update.empty()) - { - hook_update_callback(this); - } - hook_info.pre_floor_update.push_back({reserved_callback_id, std::move(pre_update)}); -} -void Entity::set_post_floor_update(std::uint32_t reserved_callback_id, std::function post_update) -{ - EntityHooksInfo& hook_info = get_hooks(); - if (hook_info.pre_floor_update.empty() && hook_info.post_floor_update.empty()) - { - hook_update_callback(this); - } - hook_info.post_floor_update.push_back({reserved_callback_id, std::move(post_update)}); -} - -bool Entity::is_player() -{ - if (type->search_flags & 1) - { - Player* pl = this->as(); - return pl->ai == nullptr; - } - return false; -} - -bool Entity::is_movable() -{ - static const ENT_TYPE first_logical = to_id("ENT_TYPE_LOGICAL_CONSTELLATION"); - if (type->search_flags & 0b11111111) // PLAYER | MOUNT | MONSTER | ITEM | ROPE | EXPLOSION | FX | ACTIVEFLOOR - return true; - else if (type->search_flags & 0x1000) // LOGICAL - as it has some movable entities - if (type->id < first_logical) // actually check if it's not logical - return true; - - return false; -} - -bool Entity::is_liquid() -{ - static const ENT_TYPE liquid_water = to_id("ENT_TYPE_LIQUID_WATER"); - static const ENT_TYPE liquid_coarse_water = to_id("ENT_TYPE_LIQUID_COARSE_WATER"); - static const ENT_TYPE liquid_lava = to_id("ENT_TYPE_LIQUID_LAVA"); - static const ENT_TYPE liquid_stagnant_lava = to_id("ENT_TYPE_LIQUID_STAGNANT_LAVA"); - static const ENT_TYPE liquid_coarse_lava = to_id("ENT_TYPE_LIQUID_COARSE_LAVA"); - - if (type->id == liquid_water || type->id == liquid_coarse_water || type->id == liquid_lava || type->id == liquid_stagnant_lava || type->id == liquid_coarse_lava) - return true; - - return false; -} - -void Entity::set_pre_collision1(std::uint32_t reserved_callback_id, std::function pre_collision1) -{ - EntityHooksInfo& hook_info = get_hooks(); - if (hook_info.pre_collision1.empty()) - { - hook_vtable( - this, - [](Entity* self, Entity* collision_entity, void (*original)(Entity*, Entity*)) - { - EntityHooksInfo& _hook_info = self->get_hooks(); - - bool skip_orig = false; - for (auto& [id, pre] : _hook_info.pre_collision1) - { - if (pre(self, collision_entity)) - { - skip_orig = true; - } - } - - if (!skip_orig) - { - original(self, collision_entity); - } - }, - 0x4); - } - hook_info.pre_collision1.push_back({reserved_callback_id, std::move(pre_collision1)}); -} - -void Entity::set_pre_collision2(std::uint32_t reserved_callback_id, std::function pre_collision2) -{ - EntityHooksInfo& hook_info = get_hooks(); - if (hook_info.pre_collision2.empty()) - { - hook_vtable( - this, - [](Entity* self, Entity* collision_entity, void (*original)(Entity*, Entity*)) - { - EntityHooksInfo& _hook_info = self->get_hooks(); - - bool skip_orig = false; - for (auto& [id, pre] : _hook_info.pre_collision2) - { - if (pre(self, collision_entity)) - { - skip_orig = true; - } - } - - if (!skip_orig) - { - original(self, collision_entity); - } - }, - 0x1A); - } - hook_info.pre_collision2.push_back({reserved_callback_id, std::move(pre_collision2)}); -} - -auto hook_render_callback(Entity* self, RenderInfo* self_rendering_info) -{ - hook_vtable( - self_rendering_info, - [uid = self->uid](RenderInfo* render_info, float* floats, void (*original)(RenderInfo*, float* floats)) - { - Entity* entity = get_entity_ptr_local(uid); - EntityHooksInfo& _hook_info = entity->get_hooks(); - bool skip_original{false}; - for (auto& [id, pre] : _hook_info.pre_render) - { - if (pre(entity)) - { - skip_original = true; - break; - } - } - if (!skip_original) - { - original(render_info, floats); - } - for (auto& [id, post] : _hook_info.post_render) - { - post(entity); - } - }, - 0x3); -} -void Entity::set_pre_render(std::uint32_t reserved_callback_id, std::function pre_render) -{ - EntityHooksInfo& hook_info = get_hooks(); - if (hook_info.pre_render.empty() && hook_info.post_render.empty()) - { - hook_render_callback(this, rendering_info); - } - hook_info.pre_render.push_back({reserved_callback_id, std::move(pre_render)}); -} -void Entity::set_post_render(std::uint32_t reserved_callback_id, std::function post_render) -{ - EntityHooksInfo& hook_info = get_hooks(); - if (hook_info.pre_render.empty() && hook_info.post_render.empty()) - { - hook_render_callback(this, rendering_info); - } - hook_info.post_render.push_back({reserved_callback_id, std::move(post_render)}); -} - -std::span Entity::get_items() -{ - if (items.size) - return std::span(items.uids().begin(), items.uids().end()); - - return {}; -} - -Entity* get_entity_ptr(uint32_t uid) -{ - auto state = State::get(); - auto p = state.find(uid); - if (IsBadWritePtr(p, 0x178)) - return nullptr; - return p; -} - -Entity* get_entity_ptr_local(uint32_t uid) -{ - auto state = State::get(); - auto p = state.find_local(uid); - if (IsBadWritePtr(p, 0x178)) - return nullptr; - return p; -} - -std::vector Movable::get_all_behaviors() -{ - std::vector anims; - anims.reserve(behaviors_map.size()); - - for (auto& cur : behaviors_map) - { - anims.push_back(cur.first); - } - return anims; -} - -bool Movable::set_behavior(uint32_t an) -{ - const auto& it = behaviors_map.find(an); - if (it != behaviors_map.end()) - { - if (current_behavior != nullptr) - { - current_behavior->on_exit(this); - } - current_behavior = it->second; - if (current_behavior != nullptr) - { - current_behavior->on_enter(this); - } - return true; - } - return false; -} - -uint32_t Movable::get_behavior() -{ - for (auto& cur : behaviors_map) - { - if (cur.second == current_behavior) - { - return cur.first; - } - } - return 0; // there is no id 0, but i can be wrong -} - -void Movable::set_gravity(float gravity) -{ - hook_vtable( - this, - [gravity](Movable* ent, [[maybe_unused]] float _gravity, void (*original)(Movable*, float)) - { - original(ent, gravity); - }, - 0x53); -} - -void Movable::reset_gravity() -{ - unregister_hook_function((void***)this, 0x53); -} +#include "entity.hpp" + +#include // for IsBadWritePtr +#include // for operator<=>, operator-, operator+ +#include // for round +#include // for operator<, operator<=, operator> +#include // for uint32_t, uint16_t, uint8_t +#include // for abs, NULL, size_t +#include // for _List_const_iterator +#include // for _Tree_iterator, map, _Tree_cons... +#include // for operator new +#include // for allocator, string, operator""sv +#include // for sleep_for +#include // for vector, _Vector_iterator, erase_if + +#include "containers/custom_map.hpp" // for custom_map +#include "entities_chars.hpp" // for Player +#include "entity_hooks_info.hpp" // for EntityHooksInfo +#include "memory.hpp" // for write_mem_prot +#include "movable.hpp" // for Movable +#include "movable_behavior.hpp" // for MovableBehavior +#include "render_api.hpp" // for RenderInfo +#include "search.hpp" // for get_address +#include "state.hpp" // for State, StateMemory, enum_to_layer +#include "state_structs.hpp" // for LiquidPhysicsEngine +#include "texture.hpp" // for get_texture, Texture +#include "vtable_hook.hpp" // for hook_vtable, hook_dtor, unregis... + +using namespace std::chrono_literals; + +std::vector g_entity_hooks; + +void Entity::teleport(float dx, float dy, bool s, float vx, float vy, bool snap) +{ + if (overlay) + overlay->remove_item(uid); + overlay = NULL; + auto topmost = topmost_mount(); + if (!s) + { + auto [x_pos, y_pos] = topmost->position(); + // player relative coordinates + x_pos += dx; + y_pos += dy; + if (snap) + { + x_pos = round(x_pos); + y_pos = round(y_pos); + } + topmost->x = x_pos; + topmost->y = y_pos; + } + else + { + // screen coordinates -1..1 + // log::debug!("Teleporting to screen {}, {}", x, y); + auto state = State::get(); + auto [x_pos, y_pos] = state.click_position(dx, dy); + if (snap && abs(vx) + abs(vy) <= 0.04f) + { + x_pos = round(x_pos); + y_pos = round(y_pos); + } + // log::debug!("Teleporting to {}, {}", x, y); + topmost->x = x_pos; + topmost->y = y_pos; + } + // set velocity + if (topmost->is_movable()) + { + auto movable_ent = (Movable*)topmost; + movable_ent->velocityx = vx; + movable_ent->velocityy = vy; + } + return; +} + +void Entity::teleport_abs(float dx, float dy, float vx, float vy) +{ + if (overlay) + overlay->remove_item(uid); + overlay = NULL; + x = dx; + y = dy; + if (is_movable()) + { + auto movable_ent = this->as(); + movable_ent->velocityx = vx; + movable_ent->velocityy = vy; + } +} + +void Entity::set_layer(LAYER layer_to) +{ + uint8_t dest_layer = enum_to_layer(layer_to); + if (layer == dest_layer) + return; + + auto state = State::get(); + if (this != this->topmost_mount()) + this->topmost_mount()->set_layer(layer_to); + + if (layer == 0 || layer == 1) + { + auto ptr_from = state.ptr()->layers[layer]; + + using RemoveFromLayer = void(Layer*, Entity*); + static RemoveFromLayer* remove_from_layer = (RemoveFromLayer*)get_address("remove_from_layer"); + remove_from_layer(ptr_from, this); + } + + auto ptr_to = state.ptr()->layers[dest_layer]; + + using AddToLayer = void(Layer*, Entity*); + static AddToLayer* add_to_layer = (AddToLayer*)get_address("add_to_layer"); + add_to_layer(ptr_to, this); + + for (auto item : items.entities()) + { + item->set_layer(layer_to); + } +} + +void Entity::remove() +{ + if (layer != 2) + { + auto state = State::get(); + auto ptr_from = state.ptr()->layers[layer]; + if ((this->type->search_flags & 1) == 0 || ((Player*)this)->ai != 0) + { + using RemoveFromLayer = void(Layer*, Entity*); + static RemoveFromLayer* remove_from_layer = (RemoveFromLayer*)get_address("remove_from_layer"); + remove_from_layer(ptr_from, this); + + for (auto item : items.entities()) + { + item->remove(); + } + } + layer = 2; + } +} + +void Entity::respawn(LAYER layer_to) +{ + set_layer(layer_to); +} + +void Entity::perform_teleport(uint8_t delta_x, uint8_t delta_y) +{ + using TeleportFun = void(Entity*, uint8_t, uint8_t); + static TeleportFun* tp = (TeleportFun*)get_address("teleport"); + tp(this, delta_x, delta_y); +} + +std::pair Entity::position() +{ + auto [x_pos, y_pos] = position_self(); + + // overlay exists if player is riding something / etc + Entity* overlay_nested = overlay; + while (overlay_nested != nullptr) + { + x_pos += overlay_nested->x; + y_pos += overlay_nested->y; + overlay_nested = overlay_nested->overlay; + } + return {x_pos, y_pos}; +} + +std::pair Entity::position_self() const +{ + return std::pair(x, y); +} + +void Entity::remove_item(uint32_t item_uid) +{ + auto entity = get_entity_ptr(item_uid); + if (entity) + remove_item_ptr(entity); +} + +void Movable::poison(int16_t frames) +{ + static size_t offset_first = 0; + static size_t offset_subsequent = 0; + if (offset_first == 0) + { + offset_first = get_address("first_poison_tick_timer_default"); + offset_subsequent = get_address("subsequent_poison_tick_timer_default"); + } + poison_tick_timer = frames; + + if (frames == -1) + { + frames = 1800; + } + write_mem_prot(offset_first, frames, true); + write_mem_prot(offset_subsequent, frames, true); +} + +bool Movable::is_poisoned() +{ + return (poison_tick_timer != -1); +} + +void Movable::broken_damage(uint32_t damage_dealer_uid, int8_t damage_amount, uint16_t stun_time, float velocity_x, float velocity_y) +{ + damage(damage_dealer_uid, damage_amount, stun_time, velocity_x, velocity_y, 80); +} + +void Movable::damage(uint32_t damage_dealer_uid, int8_t damage_amount, uint16_t stun_time, float velocity_x, float velocity_y, uint16_t iframes) +{ + if ((flags & (1 << 28)) > 0) + { + return; + } + + auto dealer = get_entity_ptr(damage_dealer_uid); + if (dealer == nullptr) + { + return; + } + + float velocities[] = {velocity_x, velocity_y}; + float unknown[] = {0.0f, 0.0f}; + on_regular_damage(dealer, damage_amount, 0x1000, velocities, unknown, stun_time, iframes); +} + +bool Movable::is_button_pressed(BUTTON button) +{ + return (buttons & button) == button && (buttons_previous & button) == 0; +} +bool Movable::is_button_held(BUTTON button) +{ + return (buttons & button) == button && (buttons_previous & button) == button; +} +bool Movable::is_button_released(BUTTON button) +{ + return (buttons & button) == 0 && (buttons_previous & button) == button; +} + +void hook_movable_state_machine(Movable* _self) +{ + hook_vtable( + _self, + [](Movable* self, void (*original)(Movable*)) + { + EntityHooksInfo& hook_info = self->get_hooks(); + + bool skip_orig = false; + for (auto& [id, pre] : hook_info.pre_statemachine) + { + if (pre(self)) + { + skip_orig = true; + } + } + + if (!skip_orig) + { + original(self); + } + + for (auto& [id, post] : hook_info.post_statemachine) + { + post(self); + } + }, + 0x2); +} +void Movable::set_pre_statemachine(std::uint32_t reserved_callback_id, std::function pre_state_machine) +{ + EntityHooksInfo& hook_info = get_hooks(); + if (hook_info.pre_statemachine.empty() && hook_info.post_statemachine.empty()) + { + hook_movable_state_machine(this); + } + hook_info.pre_statemachine.push_back({reserved_callback_id, std::move(pre_state_machine)}); +} +void Movable::set_post_statemachine(std::uint32_t reserved_callback_id, std::function post_state_machine) +{ + EntityHooksInfo& hook_info = get_hooks(); + if (hook_info.pre_statemachine.empty() && hook_info.post_statemachine.empty()) + { + hook_movable_state_machine(this); + } + hook_info.post_statemachine.push_back({reserved_callback_id, std::move(post_state_machine)}); +} + +std::tuple get_position(uint32_t uid) +{ + Entity* ent = get_entity_ptr(uid); + if (ent) + return std::make_tuple(ent->position().first, ent->position().second, ent->layer); + + return {0.0f, 0.0f, (uint8_t)0}; +} + +std::tuple get_render_position(uint32_t uid) +{ + Entity* ent = get_entity_ptr(uid); + if (ent) + { + if (ent->rendering_info != nullptr && !ent->rendering_info->render_inactive) + return std::make_tuple(ent->rendering_info->x, ent->rendering_info->y, ent->layer); + else + return get_position(uid); + } + return {0.0f, 0.0f, (uint8_t)0}; +} + +std::tuple get_velocity(uint32_t uid) +{ + if (Entity* ent = get_entity_ptr(uid)) + { + float vx{0.0f}; + float vy{0.0f}; + if (ent->is_movable()) + { + Movable* mov = ent->as(); + vx = mov->velocityx; + vy = mov->velocityy; + } + else if (ent->is_liquid()) + { + auto liquid_engine = State::get().get_correct_liquid_engine(ent->type->id); + vx = liquid_engine->entity_velocities->first; + vy = liquid_engine->entity_velocities->second; + } + if (ent->overlay) + { + auto [ovx, ovy] = get_velocity(ent->overlay->uid); + vx += ovx; + vy += ovy; + } + return std::tuple{vx, vy}; + } + return std::tuple{0.0f, 0.0f}; +} + +AABB get_hitbox(uint32_t uid, bool use_render_pos) +{ + if (Entity* ent = get_entity_ptr(uid)) + { + auto [x, y, l] = (use_render_pos ? get_render_position : get_position)(uid); + return AABB{ + x - ent->hitboxx + ent->offsetx, + y + ent->hitboxy + ent->offsety, + x + ent->hitboxx + ent->offsetx, + y - ent->hitboxy + ent->offsety, + }; + } + return AABB{0.0f, 0.0f, 0.0f, 0.0f}; +} + +TEXTURE Entity::get_texture() +{ + if (texture) + return texture->id; + + return -1; +} +bool Entity::set_texture(TEXTURE texture_id) +{ + if (auto* new_texture = ::get_texture(texture_id)) + { + apply_texture(new_texture); + return true; + } + return false; +} + +void Entity::unhook(std::uint32_t id) +{ + auto it = std::find_if(g_entity_hooks.begin(), g_entity_hooks.end(), [this](auto& hook) + { return hook.entity == uid; }); + if (it != g_entity_hooks.end()) + { + std::erase_if(it->on_dtor, [id](auto& hook) + { return hook.id == id; }); + std::erase_if(it->on_destroy, [id](auto& hook) + { return hook.id == id; }); + std::erase_if(it->on_kill, [id](auto& hook) + { return hook.id == id; }); + std::erase_if(it->on_player_instagib, [id](auto& hook) + { return hook.id == id; }); + std::erase_if(it->on_damage, [id](auto& hook) + { return hook.id == id; }); + std::erase_if(it->pre_floor_update, [id](auto& hook) + { return hook.id == id; }); + std::erase_if(it->post_floor_update, [id](auto& hook) + { return hook.id == id; }); + std::erase_if(it->pre_statemachine, [id](auto& hook) + { return hook.id == id; }); + std::erase_if(it->post_statemachine, [id](auto& hook) + { return hook.id == id; }); + std::erase_if(it->on_open, [id](auto& hook) + { return hook.id == id; }); + std::erase_if(it->pre_collision1, [id](auto& hook) + { return hook.id == id; }); + std::erase_if(it->pre_collision2, [id](auto& hook) + { return hook.id == id; }); + std::erase_if(it->pre_render, [id](auto& hook) + { return hook.id == id; }); + std::erase_if(it->post_render, [id](auto& hook) + { return hook.id == id; }); + } +} +EntityHooksInfo& Entity::get_hooks() +{ + auto it = std::find_if(g_entity_hooks.begin(), g_entity_hooks.end(), [this](auto& hook) + { return hook.entity == uid; }); + if (it == g_entity_hooks.end()) + { + hook_dtor(this, [](void* self) + { + auto _it = std::find_if(g_entity_hooks.begin(), g_entity_hooks.end(), [self](auto& hook) + { return hook.entity == ((Entity*)self)->uid; }); + if (_it != g_entity_hooks.end()) + { + for (auto& cb : _it->on_dtor) + { + cb.fun((Entity*)self); + } + g_entity_hooks.erase(_it); + } }); + g_entity_hooks.push_back({uid}); + return g_entity_hooks.back(); + } + return *it; +} + +std::uint32_t Entity::set_on_dtor(std::function cb) +{ + EntityHooksInfo& hook_info = get_hooks(); + hook_info.on_dtor.push_back({hook_info.cbcount++, std::move(cb)}); + return hook_info.on_dtor.back().id; +} +std::uint32_t Entity::reserve_callback_id() +{ + EntityHooksInfo& hook_info = get_hooks(); + return hook_info.cbcount++; +} +void Entity::set_on_destroy(std::uint32_t reserved_callback_id, std::function on_destroy) +{ + EntityHooksInfo& hook_info = get_hooks(); + if (hook_info.on_destroy.empty()) + { + hook_vtable( + this, + [](Entity* self, void (*original)(Entity*)) + { + EntityHooksInfo& _hook_info = self->get_hooks(); + for (auto& [id, on_destroy] : _hook_info.on_destroy) + { + on_destroy(self); + } + original(self); + }, + 0x5); + } + hook_info.on_destroy.push_back({reserved_callback_id, std::move(on_destroy)}); +} +void Entity::set_on_kill(std::uint32_t reserved_callback_id, std::function on_kill) +{ + EntityHooksInfo& hook_info = get_hooks(); + if (hook_info.on_kill.empty()) + { + hook_vtable( + this, + [](Entity* self, bool _some_bool, Entity* from, void (*original)(Entity*, bool, Entity*)) + { + EntityHooksInfo& _hook_info = self->get_hooks(); + for (auto& [id, on_kill] : _hook_info.on_kill) + { + on_kill(self, from); + } + original(self, _some_bool, from); + }, + 0x3); + } + hook_info.on_kill.push_back({reserved_callback_id, std::move(on_kill)}); +} + +void Entity::set_on_player_instagib(std::uint32_t reserved_callback_id, std::function on_instagib) +{ + EntityHooksInfo& hook_info = get_hooks(); + // no hooking here, because the instagib function is hooked in rpc.cpp + hook_info.on_player_instagib.push_back({reserved_callback_id, std::move(on_instagib)}); +} + +void Entity::set_on_damage(std::uint32_t reserved_callback_id, std::function on_damage) +{ + EntityHooksInfo& hook_info = get_hooks(); + if (hook_info.on_damage.empty()) + { + if ((this->type->search_flags & 0x1) == 0x1) + { + // Can't hook player::on_damage here, because this is permanently hooked for the god function. + // The god function takes care of calling the script hooks in rpc.cpp + } + else + { + hook_vtable( + this, + [](Entity* self, Entity* damage_dealer, int8_t damage_amount, uint32_t unknown1, float* velocities, float* unknown2, uint16_t stun_amount, uint8_t iframes, void (*original)(Entity*, Entity*, int8_t, uint32_t, float*, float*, uint16_t, uint8_t)) + { + EntityHooksInfo& _hook_info = self->get_hooks(); + bool skip_orig = false; + for (auto& [id, on_damage] : _hook_info.on_damage) + { + if (on_damage(self, damage_dealer, damage_amount, velocities[0], velocities[1], stun_amount, iframes)) + { + skip_orig = true; + } + } + + if (!skip_orig) + { + original(self, damage_dealer, damage_amount, unknown1, velocities, unknown2, stun_amount, iframes); + } + }, + 0x30); + } + } + hook_info.on_damage.push_back({reserved_callback_id, std::move(on_damage)}); +} + +auto hook_update_callback(Entity* self) +{ + hook_vtable( + self, + [](Entity* entity, void (*original)(Entity*)) + { + EntityHooksInfo& _hook_info = entity->get_hooks(); + bool skip_original{false}; + for (auto& [id, pre] : _hook_info.pre_floor_update) + { + if (pre(entity)) + { + skip_original = true; + break; + } + } + if (!skip_original) + { + original(entity); + } + for (auto& [id, post] : _hook_info.post_floor_update) + { + post(entity); + } + }, + 0x26); +} +void Entity::set_pre_floor_update(std::uint32_t reserved_callback_id, std::function pre_update) +{ + EntityHooksInfo& hook_info = get_hooks(); + if (hook_info.pre_floor_update.empty() && hook_info.post_floor_update.empty()) + { + hook_update_callback(this); + } + hook_info.pre_floor_update.push_back({reserved_callback_id, std::move(pre_update)}); +} +void Entity::set_post_floor_update(std::uint32_t reserved_callback_id, std::function post_update) +{ + EntityHooksInfo& hook_info = get_hooks(); + if (hook_info.pre_floor_update.empty() && hook_info.post_floor_update.empty()) + { + hook_update_callback(this); + } + hook_info.post_floor_update.push_back({reserved_callback_id, std::move(post_update)}); +} + +bool Entity::is_player() +{ + if (type->search_flags & 1) + { + Player* pl = this->as(); + return pl->ai == nullptr; + } + return false; +} + +bool Entity::is_movable() +{ + static const ENT_TYPE first_logical = to_id("ENT_TYPE_LOGICAL_CONSTELLATION"); + if (type->search_flags & 0b11111111) // PLAYER | MOUNT | MONSTER | ITEM | ROPE | EXPLOSION | FX | ACTIVEFLOOR + return true; + else if (type->search_flags & 0x1000) // LOGICAL - as it has some movable entities + if (type->id < first_logical) // actually check if it's not logical + return true; + + return false; +} + +bool Entity::is_liquid() +{ + static const ENT_TYPE liquid_water = to_id("ENT_TYPE_LIQUID_WATER"); + static const ENT_TYPE liquid_coarse_water = to_id("ENT_TYPE_LIQUID_COARSE_WATER"); + static const ENT_TYPE liquid_lava = to_id("ENT_TYPE_LIQUID_LAVA"); + static const ENT_TYPE liquid_stagnant_lava = to_id("ENT_TYPE_LIQUID_STAGNANT_LAVA"); + static const ENT_TYPE liquid_coarse_lava = to_id("ENT_TYPE_LIQUID_COARSE_LAVA"); + + if (type->id == liquid_water || type->id == liquid_coarse_water || type->id == liquid_lava || type->id == liquid_stagnant_lava || type->id == liquid_coarse_lava) + return true; + + return false; +} + +void Entity::set_pre_collision1(std::uint32_t reserved_callback_id, std::function pre_collision1) +{ + EntityHooksInfo& hook_info = get_hooks(); + if (hook_info.pre_collision1.empty()) + { + hook_vtable( + this, + [](Entity* self, Entity* collision_entity, void (*original)(Entity*, Entity*)) + { + EntityHooksInfo& _hook_info = self->get_hooks(); + + bool skip_orig = false; + for (auto& [id, pre] : _hook_info.pre_collision1) + { + if (pre(self, collision_entity)) + { + skip_orig = true; + } + } + + if (!skip_orig) + { + original(self, collision_entity); + } + }, + 0x4); + } + hook_info.pre_collision1.push_back({reserved_callback_id, std::move(pre_collision1)}); +} + +void Entity::set_pre_collision2(std::uint32_t reserved_callback_id, std::function pre_collision2) +{ + EntityHooksInfo& hook_info = get_hooks(); + if (hook_info.pre_collision2.empty()) + { + hook_vtable( + this, + [](Entity* self, Entity* collision_entity, void (*original)(Entity*, Entity*)) + { + EntityHooksInfo& _hook_info = self->get_hooks(); + + bool skip_orig = false; + for (auto& [id, pre] : _hook_info.pre_collision2) + { + if (pre(self, collision_entity)) + { + skip_orig = true; + } + } + + if (!skip_orig) + { + original(self, collision_entity); + } + }, + 0x1A); + } + hook_info.pre_collision2.push_back({reserved_callback_id, std::move(pre_collision2)}); +} + +auto hook_render_callback(Entity* self, RenderInfo* self_rendering_info) +{ + hook_vtable( + self_rendering_info, + [uid = self->uid](RenderInfo* render_info, float* floats, void (*original)(RenderInfo*, float* floats)) + { + Entity* entity = get_entity_ptr_local(uid); + EntityHooksInfo& _hook_info = entity->get_hooks(); + bool skip_original{false}; + for (auto& [id, pre] : _hook_info.pre_render) + { + if (pre(entity)) + { + skip_original = true; + break; + } + } + if (!skip_original) + { + original(render_info, floats); + } + for (auto& [id, post] : _hook_info.post_render) + { + post(entity); + } + }, + 0x3); +} +void Entity::set_pre_render(std::uint32_t reserved_callback_id, std::function pre_render) +{ + EntityHooksInfo& hook_info = get_hooks(); + if (hook_info.pre_render.empty() && hook_info.post_render.empty()) + { + hook_render_callback(this, rendering_info); + } + hook_info.pre_render.push_back({reserved_callback_id, std::move(pre_render)}); +} +void Entity::set_post_render(std::uint32_t reserved_callback_id, std::function post_render) +{ + EntityHooksInfo& hook_info = get_hooks(); + if (hook_info.pre_render.empty() && hook_info.post_render.empty()) + { + hook_render_callback(this, rendering_info); + } + hook_info.post_render.push_back({reserved_callback_id, std::move(post_render)}); +} +void Entity::set_enable_turning(bool enabled) +{ + set_entity_turning(this, enabled); +} + +std::span Entity::get_items() +{ + if (items.size) + return std::span(items.uids().begin(), items.uids().end()); + + return {}; +} + +Entity* get_entity_ptr(uint32_t uid) +{ + auto state = State::get(); + auto p = state.find(uid); + if (IsBadWritePtr(p, 0x178)) + return nullptr; + return p; +} + +Entity* get_entity_ptr_local(uint32_t uid) +{ + auto state = State::get(); + auto p = state.find_local(uid); + if (IsBadWritePtr(p, 0x178)) + return nullptr; + return p; +} + +std::vector Movable::get_all_behaviors() +{ + std::vector anims; + anims.reserve(behaviors_map.size()); + + for (auto& cur : behaviors_map) + { + anims.push_back(cur.first); + } + return anims; +} + +bool Movable::set_behavior(uint32_t an) +{ + const auto& it = behaviors_map.find(an); + if (it != behaviors_map.end()) + { + if (current_behavior != nullptr) + { + current_behavior->on_exit(this); + } + current_behavior = it->second; + if (current_behavior != nullptr) + { + current_behavior->on_enter(this); + } + return true; + } + return false; +} + +uint32_t Movable::get_behavior() +{ + for (auto& cur : behaviors_map) + { + if (cur.second == current_behavior) + { + return cur.first; + } + } + return 0; // there is no id 0, but i can be wrong +} + +void Movable::set_gravity(float gravity) +{ + hook_vtable( + this, + [gravity](Movable* ent, [[maybe_unused]] float _gravity, void (*original)(Movable*, float)) + { + original(ent, gravity); + }, + 0x53); +} + +void Movable::reset_gravity() +{ + unregister_hook_function((void***)this, 0x53); +} diff --git a/src/game_api/entity.hpp b/src/game_api/entity.hpp index 26b9db8d8..0bc46bd57 100644 --- a/src/game_api/entity.hpp +++ b/src/game_api/entity.hpp @@ -1,444 +1,246 @@ -#pragma once - -#include // for size_t -#include // for uint8_t, uint32_t, int32_t, uint16_t, int64_t -#include // for function, equal_to -#include // for span -#include // for allocator, string -#include // for string_view -#include // for tuple -#include // for move -#include // for _Umap_traits<>::allocator_type, unordered_map -#include // for pair -#include // for vector - -#include "aliases.hpp" // for ENT_TYPE, LAYER, TEXTURE, STRINGID -#include "color.hpp" // for Color -#include "layer.hpp" // for EntityList -#include "math.hpp" // for AABB - -struct RenderInfo; -struct Texture; - -enum class REPEAT_TYPE : uint8_t -{ - NoRepeat, - Linear, - BackAndForth, -}; - -enum class SHAPE : uint8_t -{ - RECTANGLE = 1, - CIRCLE = 2, -}; - -struct Animation -{ - int32_t texture; - int32_t count; - int32_t interval; - uint8_t key; - REPEAT_TYPE repeat; -}; - -struct Rect -{ - float offsetx; - float offsety; - float hitboxx; - float hitboxy; -}; - -class Entity; -class Movable; -class Container; - -template -struct HookWithId -{ - std::uint32_t id; - std::function fun; -}; -struct EntityHooksInfo -{ - std::int32_t entity; - std::uint32_t cbcount; - std::vector> on_dtor; - std::vector> on_destroy; - std::vector> on_kill; - std::vector> pre_floor_update; - std::vector> post_floor_update; - std::vector> on_player_instagib; - std::vector> on_damage; - std::vector> pre_statemachine; - std::vector> post_statemachine; - std::vector> on_open; - std::vector> pre_collision1; - std::vector> pre_collision2; - std::vector> pre_render; - std::vector> post_render; -}; - -// Creates an instance of this entity -using EntityCreate = Entity* (*)(); -using EntityDestroy = void (*)(Entity*); - -struct EntityDB -{ - EntityCreate create_func; - EntityDestroy destroy_func; - int32_t field_10; - ENT_TYPE id; - /// MASK - uint32_t search_flags; - float width; - float height; - uint8_t draw_depth; - uint8_t default_b3f; // value gets copied into entity.b3f along with draw_depth etc (RVA 0x21F30CC4) - int16_t field_26; - Rect rect_collision; - uint8_t default_shape; - bool default_hitbox_enabled; - uint8_t field_3A; - uint8_t field_3B; - int32_t field_3C; - int32_t field_40; - int32_t field_44; - int32_t default_flags; - int32_t default_more_flags; - int32_t properties_flags; - float friction; - float elasticity; - float weight; - uint8_t field_60; - float acceleration; - float max_speed; - float sprint_factor; - float jump; - union - { - Color default_color; - struct - { - float glow_red; - float glow_green; - float glow_blue; - float glow_alpha; - }; - }; - int32_t texture; - int32_t technique; - int32_t tile_x; - int32_t tile_y; - uint8_t damage; - uint8_t life; - uint8_t field_96; - uint8_t blood_content; - bool leaves_corpse_behind; - uint8_t field_99; - uint8_t field_9A; - uint8_t field_9B; - STRINGID description; - int32_t sound_killed_by_player; - int32_t sound_killed_by_other; - float field_a8; - int32_t field_AC; - std::unordered_map animations; - float default_special_offsetx; - float default_special_offsety; - uint8_t init; -}; - -struct EntityItem -{ - std::string name; - uint32_t id; - - EntityItem(const std::string& name_, uint32_t id_) - : name(name_), id(id_) - { - } - bool operator<(const EntityItem& item) const - { - return id < item.id; - } -}; - -EntityDB* get_type(uint32_t id); - -ENT_TYPE to_id(std::string_view id); - -std::string_view to_name(ENT_TYPE id); - -class Entity -{ - public: - EntityDB* type; - Entity* overlay; - EntityList items; - uint32_t flags; - uint32_t more_flags; - int32_t uid; - uint16_t animation_frame; - /// Don't edit this directly, use `set_draw_depth` - uint8_t draw_depth; - uint8_t b3f; // depth related, changed when going thru doors etc. - float x; - float y; - float abs_x; // only for movable entities, or entities that can be spawned without overlay, for the rest it's FLOAT_MIN? - float abs_y; - float w; - float h; - float special_offsetx; - float special_offsety; - Color color; - float offsetx; - float offsety; - float hitboxx; - float hitboxy; - SHAPE shape; // 1 = rectangle, 2 = circle - bool hitbox_enabled; // probably, off for bg, deco, logical etc - uint8_t b82; - uint8_t b83; - float angle; - RenderInfo* rendering_info; - Texture* texture; - float tilew; - float tileh; - uint8_t layer; - uint8_t b99; // this looks like FLOORSTYLED post-processing - uint8_t b9a; - uint8_t b9b; - uint32_t i9c; - - size_t pointer() - { - return (size_t)this; - } - - std::pair position(); - - void teleport(float dx, float dy, bool s, float vx, float vy, bool snap); - void teleport_abs(float dx, float dy, float vx, float vy); - - /// Moves the entity to specified layer, nothing else happens, so this does not emulate a door transition - void set_layer(LAYER layer); - /// Moves the entity to the limbo-layer where it can later be retrieved from again via `respawn` - void remove(); - /// Moves the entity from the limbo-layer (where it was previously put by `remove`) to `layer` - void respawn(LAYER layer); - /// Performs a teleport as if the entity had a teleporter and used it. The delta coordinates are where you want the entity to teleport to relative to its current position, in tiles (so integers, not floats). Positive numbers = to the right and up, negative left and down. - void perform_teleport(uint8_t delta_x, uint8_t delta_y); - - Entity* topmost() - { - auto cur = this; - while (cur->overlay) - { - cur = cur->overlay; - } - return cur; - } - - Entity* topmost_mount() - { - auto topmost = this; - while (auto cur = topmost->overlay) - { - if (cur->type->search_flags <= 2) - { - topmost = cur; - } - else - break; - } - return topmost; - } - - bool overlaps_with(AABB hitbox) - { - return overlaps_with(hitbox.left, hitbox.bottom, hitbox.right, hitbox.top); - } - - /// Deprecated - /// Use `overlaps_with(AABB hitbox)` instead - bool overlaps_with(float rect_left, float rect_bottom, float rect_right, float rect_top) - { - const auto [posx, posy] = position(); - const float left = posx - hitboxx + offsetx; - const float right = posx + hitboxx + offsetx; - const float bottom = posy - hitboxy + offsety; - const float top = posy + hitboxy + offsety; - - return left < rect_right && rect_left < right && bottom < rect_top && rect_bottom < top; - } - - bool overlaps_with(Entity* other) - { - const auto [other_posx, other_posy] = other->position(); - const float other_left = other_posx - other->hitboxx + other->offsetx; - const float other_right = other_posx + other->hitboxx + other->offsetx; - const float other_top = other_posy + other->hitboxy + other->offsety; - const float other_bottom = other_posy - other->hitboxy + other->offsety; - - return overlaps_with(other_left, other_bottom, other_right, other_top); - } - - std::pair position_self() const; - void remove_item(uint32_t item_uid); - - TEXTURE get_texture(); - /// Changes the entity texture, check the [textures.txt](game_data/textures.txt) for available vanilla textures or use [define_texture](#define_texture) to make custom one - bool set_texture(TEXTURE texture_id); - - void unhook(std::uint32_t id); - struct EntityHooksInfo& get_hooks(); - - bool is_player(); - bool is_movable(); - bool is_liquid(); - - std::uint32_t set_on_dtor(std::function cb); - std::uint32_t reserve_callback_id(); - void set_on_destroy(std::uint32_t reserved_callback_id, std::function on_destroy); - void set_on_kill(std::uint32_t reserved_callback_id, std::function on_kill); - void set_on_player_instagib(std::uint32_t reserved_callback_id, std::function on_instagib); - void set_on_damage(std::uint32_t reserved_callback_id, std::function on_damage); - void set_pre_floor_update(std::uint32_t reserved_callback_id, std::function pre_update); - void set_post_floor_update(std::uint32_t reserved_callback_id, std::function post_update); - void set_pre_collision1(std::uint32_t reserved_callback_id, std::function pre_collision1); - void set_pre_collision2(std::uint32_t reserved_callback_id, std::function pre_collision2); - void set_pre_render(std::uint32_t reserved_callback_id, std::function pre_render); - void set_post_render(std::uint32_t reserved_callback_id, std::function post_render); - - std::span get_items(); - - template - T* as() - { - return static_cast(this); - } - - virtual ~Entity() = 0; // vritual 0 - virtual void create_rendering_info() = 0; - virtual void handle_state_machine() = 0; - - /// Kills the entity, you can set responsible to `nil` to ignore it - virtual void kill(bool destroy_corpse, Entity* responsible) = 0; - - virtual void on_collision1(Entity* other_entity) = 0; // triggers on collision between whip and hit object - - /// Completely removes the entity from existence - virtual void destroy() = 0; - - virtual void apply_texture(Texture*) = 0; - virtual void format_shopitem_name(char16_t*) = 0; - virtual void generate_stomp_damage_particles(Entity* victim) = 0; // particles when jumping on top of enemy - virtual float get_type_field_a8() = 0; - virtual bool can_be_pushed() = 0; // (runs only for activefloors?) checks if entity type is pushblock, for chained push block checks ChainedPushBlock.is_chained, is only a check that allows for the pushing animation - virtual bool v11() = 0; // for arrows: returns true if it's moving (for y possibily checks for some speed as well?) - /// Returns true if entity is in water/lava - virtual bool is_in_liquid() = 0; - virtual bool check_type_properties_flags_19() = 0; // checks (properties_flags >> 0x12) & 1; for hermitcrab checks if he's invisible; can't get it to trigger - virtual uint32_t get_type_field_60() = 0; - virtual void set_invisible(bool value) = 0; - virtual void handle_turning_left(bool apply) = 0; // if disabled, monsters don't turn left and keep walking in the wall (and other right-left issues) - virtual void set_draw_depth(uint8_t draw_depth) = 0; - virtual void resume_ai() = 0; // works on entities with ai_func != 0; runs when companions are let go from being held. AI resumes anyway in 1.23.3 - virtual float friction() = 0; - virtual void v20() = 0; - virtual void remove_item_ptr(Entity*) = 0; - virtual Entity* get_held_entity() = 0; - virtual void v23(Entity* logical_trigger, Entity* who_triggered_it) = 0; // spawns LASERTRAP_SHOT from LASERTRAP, also some trigger entities use this, seam to be called right after "on_collision2", tiggers use self as the first parameter - /// Triggers weapons and other held items like teleportter, mattock etc. You can check the [virtual-availability.md](https://github.com/spelunky-fyi/overlunky/blob/main/docs/virtual-availability.md), if entity has `open` in the `on_open` you can use this function, otherwise it does nothing. Returns false if action could not be performed (cooldown is not 0, no arrow loaded in etc. the animation could still be played thou) - virtual bool trigger_action(Entity* user) = 0; - /// Activates a button prompt (with the Use door/Buy button), e.g. buy shop item, activate drill, read sign, interact in camp, ... `get_entity():activate(players[1])` (make sure player 1 has the udjat eye though) - virtual void activate(Entity* activator) = 0; - - virtual void on_collision2(Entity* other_entity) = 0; // needs investigating, difference between this and on_collision1, maybe this is on_hitbox_overlap as it works for logical tiggers - virtual uint16_t get_metadata() = 0; // e.g. for turkey: stores health, poison/curse state, for mattock: remaining swings (returned value is transferred) - virtual void apply_metadata(uint16_t metadata) = 0; - virtual void on_walked_on_by(Entity* walker) = 0; // hits when monster/player walks on a floor, does something when walker.velocityy<-0.21 (falling onto) and walker.hitboxy * hitboxx > 0.09 - virtual void on_walked_off_by(Entity* walker) = 0; // appears to be disabled in 1.23.3? hits when monster/player walks off a floor, it checks whether the walker has floor as overlay, and if so, removes walker from floor's items by calling virtual remove_item_ptr - virtual void on_ledge_grab(Entity* who) = 0; // only ACTIVEFLOOR_FALLING_PLATFORM, does something with game menager - virtual void on_stood_on_by(Entity* entity) = 0; // e.g. pots, skulls, pushblocks, ... standing on floors - virtual void toggle_backlayer_illumination() = 0; // only for CHAR_*: when going to the backlayer, turns on player emitted light - virtual void v34() = 0; // only ITEM_TORCH, calls Torch.light_up(false), can't get it to trigger - virtual void liberate_from_shop() = 0; // can also be seen as event: when you anger the shopkeeper, this function gets called for each item; can be called on shopitems individually as well and they become 'purchased' - - /// Applies changes made in `entity.type` - virtual void apply_db() = 0; // This is actually just an initialize call that is happening once after the entity is created -}; - -struct SoundInfo -{ - int64_t unknown1; - uint32_t sound_id; - int32_t unknown2; - const char* sound_name; - int64_t unknown3; - int64_t unknown4; - int64_t unknown5; -}; - -struct SoundPosition -{ - size_t __vftable; - float x; - float y; - SoundInfo* sound_effect_info; // param to FMOD::Studio::EventInstance::SetParameterByID (this ptr + 0x30) - uint64_t fmod_param_id; // param to FMOD::Studio::EventInstance::SetParameterByID - float POS_SCREEN_X; // VANILLA_SOUND_PARAM names, for now - float DIST_CENTER_X; - float DIST_CENTER_Y; - float DIST_Z; - float DIST_PLAYER; // seams to be always here, even you you get nil in lua - float SUBMERGED; - float LIQUID_STREAM; - float unknown10; // LIQUID_STREAM related? , maybe LIQUID_INTENSITY? - float VALUE; - float unknown12; - float unknown13; - float unknown14; - float unknown15; - float unknown16; - float unknown17; - float unknown18; - float unknown19; - float unknown20; - float unknown21; - float unknown22; - float unknown23; - float unknown24; - float unknown25; - float unknown26; - float unknown27; - float unknown28; - float unknown29; - float POISONED; - float CURSED; - float unknown32; - float unknown33; - float unknown34; - float unknown35; - float unknown36; - float unknown37; - float unknown38; - float unknown39; - float unknown40; - float unknown41; // all the values repeat from this point, maybe all those floats are just an array? -}; - -std::vector list_entities(); - -std::tuple get_position(uint32_t uid); -std::tuple get_render_position(uint32_t uid); - -std::tuple get_velocity(uint32_t uid); - -AABB get_hitbox(uint32_t uid, bool use_render_pos); - -struct EntityFactory* entity_factory(); -Entity* get_entity_ptr(uint32_t uid); -Entity* get_entity_ptr_local(uint32_t uid); +#pragma once + +#include // for size_t +#include // for uint8_t, uint32_t, int32_t, uint16_t, int64_t +#include // for function, equal_to +#include // for span +#include // for allocator, string +#include // for string_view +#include // for tuple +#include // for move +#include // for _Umap_traits<>::allocator_type, unordered_map +#include // for pair +#include // for vector + +#include "aliases.hpp" // for ENT_TYPE, LAYER, TEXTURE, STRINGID +#include "color.hpp" // for Color +#include "entity_db.hpp" // for EntityDB +#include "entity_structs.hpp" // for CollisionInfo +#include "layer.hpp" // for EntityList +#include "math.hpp" // for AABB + +struct RenderInfo; +struct Texture; + +class Entity; +class Movable; + +struct EntityHooksInfo; + +class Entity +{ + public: + EntityDB* type; + Entity* overlay; + EntityList items; + uint32_t flags; + uint32_t more_flags; + int32_t uid; + uint16_t animation_frame; + /// Don't edit this directly, use `set_draw_depth` + uint8_t draw_depth; + uint8_t b3f; // depth related, changed when going thru doors etc. + float x; + float y; + float abs_x; // only for movable entities, or entities that can be spawned without overlay, for the rest it's FLOAT_MIN? + float abs_y; + float w; + float h; + float special_offsetx; + float special_offsety; + Color color; + union + { + struct + { + float offsetx; + float offsety; + float hitboxx; + float hitboxy; + SHAPE shape; // 1 = rectangle, 2 = circle + bool hitbox_enabled; // probably, off for bg, deco, logical etc + uint8_t b82; + uint8_t b83; + }; + CollisionInfo collision_info; + }; + float angle; + RenderInfo* rendering_info; + Texture* texture; + float tilew; + float tileh; + uint8_t layer; + uint8_t b99; // this looks like FLOORSTYLED post-processing + uint8_t b9a; + uint8_t b9b; + uint32_t i9c; + + size_t pointer() + { + return (size_t)this; + } + + std::pair position(); + + void teleport(float dx, float dy, bool s, float vx, float vy, bool snap); + void teleport_abs(float dx, float dy, float vx, float vy); + + /// Moves the entity to specified layer, nothing else happens, so this does not emulate a door transition + void set_layer(LAYER layer); + /// Moves the entity to the limbo-layer where it can later be retrieved from again via `respawn` + void remove(); + /// Moves the entity from the limbo-layer (where it was previously put by `remove`) to `layer` + void respawn(LAYER layer); + /// Performs a teleport as if the entity had a teleporter and used it. The delta coordinates are where you want the entity to teleport to relative to its current position, in tiles (so integers, not floats). Positive numbers = to the right and up, negative left and down. + void perform_teleport(uint8_t delta_x, uint8_t delta_y); + + Entity* topmost() + { + auto cur = this; + while (cur->overlay) + { + cur = cur->overlay; + } + return cur; + } + + Entity* topmost_mount() + { + auto topmost = this; + while (auto cur = topmost->overlay) + { + if (cur->type->search_flags <= 2) + { + topmost = cur; + } + else + break; + } + return topmost; + } + + bool overlaps_with(AABB hitbox) + { + return overlaps_with(hitbox.left, hitbox.bottom, hitbox.right, hitbox.top); + } + + /// Deprecated + /// Use `overlaps_with(AABB hitbox)` instead + bool overlaps_with(float rect_left, float rect_bottom, float rect_right, float rect_top) + { + const auto [posx, posy] = position(); + const float left = posx - hitboxx + offsetx; + const float right = posx + hitboxx + offsetx; + const float bottom = posy - hitboxy + offsety; + const float top = posy + hitboxy + offsety; + + return left < rect_right && rect_left < right && bottom < rect_top && rect_bottom < top; + } + + bool overlaps_with(Entity* other) + { + const auto [other_posx, other_posy] = other->position(); + const float other_left = other_posx - other->hitboxx + other->offsetx; + const float other_right = other_posx + other->hitboxx + other->offsetx; + const float other_top = other_posy + other->hitboxy + other->offsety; + const float other_bottom = other_posy - other->hitboxy + other->offsety; + + return overlaps_with(other_left, other_bottom, other_right, other_top); + } + + std::pair position_self() const; + void remove_item(uint32_t item_uid); + + TEXTURE get_texture(); + /// Changes the entity texture, check the [textures.txt](game_data/textures.txt) for available vanilla textures or use [define_texture](#define_texture) to make custom one + bool set_texture(TEXTURE texture_id); + + void unhook(std::uint32_t id); + struct EntityHooksInfo& get_hooks(); + + bool is_player(); + bool is_movable(); + bool is_liquid(); + + std::uint32_t set_on_dtor(std::function cb); + std::uint32_t reserve_callback_id(); + void set_on_destroy(std::uint32_t reserved_callback_id, std::function on_destroy); + void set_on_kill(std::uint32_t reserved_callback_id, std::function on_kill); + void set_on_player_instagib(std::uint32_t reserved_callback_id, std::function on_instagib); + void set_on_damage(std::uint32_t reserved_callback_id, std::function on_damage); + void set_pre_floor_update(std::uint32_t reserved_callback_id, std::function pre_update); + void set_post_floor_update(std::uint32_t reserved_callback_id, std::function post_update); + void set_pre_collision1(std::uint32_t reserved_callback_id, std::function pre_collision1); + void set_pre_collision2(std::uint32_t reserved_callback_id, std::function pre_collision2); + void set_pre_render(std::uint32_t reserved_callback_id, std::function pre_render); + void set_post_render(std::uint32_t reserved_callback_id, std::function post_render); + void set_enable_turning(bool enabled); + + std::span get_items(); + + template + T* as() + { + return static_cast(this); + } + + virtual ~Entity() = 0; // vritual 0 + virtual void create_rendering_info() = 0; + virtual void handle_state_machine() = 0; + + /// Kills the entity, you can set responsible to `nil` to ignore it + virtual void kill(bool destroy_corpse, Entity* responsible) = 0; + + virtual void on_collision1(Entity* other_entity) = 0; // triggers on collision between whip and hit object + + /// Completely removes the entity from existence + virtual void destroy() = 0; + + virtual void apply_texture(Texture*) = 0; + virtual void format_shopitem_name(char16_t*) = 0; + virtual void generate_stomp_damage_particles(Entity* victim) = 0; // particles when jumping on top of enemy + virtual float get_type_field_a8() = 0; + virtual bool can_be_pushed() = 0; // (runs only for activefloors?) checks if entity type is pushblock, for chained push block checks ChainedPushBlock.is_chained, is only a check that allows for the pushing animation + virtual bool v11() = 0; // for arrows: returns true if it's moving (for y possibily checks for some speed as well?) + /// Returns true if entity is in water/lava + virtual bool is_in_liquid() = 0; + virtual bool check_type_properties_flags_19() = 0; // checks (properties_flags >> 0x12) & 1; for hermitcrab checks if he's invisible; can't get it to trigger + virtual uint32_t get_type_field_60() = 0; + virtual void set_invisible(bool value) = 0; + virtual void handle_turning_left(bool apply) = 0; // if disabled, monsters don't turn left and keep walking in the wall (and other right-left issues) + virtual void set_draw_depth(uint8_t draw_depth) = 0; + virtual void resume_ai() = 0; // works on entities with ai_func != 0; runs when companions are let go from being held. AI resumes anyway in 1.23.3 + virtual float friction() = 0; + virtual void v20() = 0; + virtual void remove_item_ptr(Entity*) = 0; + virtual Entity* get_held_entity() = 0; + virtual void v23(Entity* logical_trigger, Entity* who_triggered_it) = 0; // spawns LASERTRAP_SHOT from LASERTRAP, also some trigger entities use this, seam to be called right after "on_collision2", tiggers use self as the first parameter + /// Triggers weapons and other held items like teleportter, mattock etc. You can check the [virtual-availability.md](https://github.com/spelunky-fyi/overlunky/blob/main/docs/virtual-availability.md), if entity has `open` in the `on_open` you can use this function, otherwise it does nothing. Returns false if action could not be performed (cooldown is not 0, no arrow loaded in etc. the animation could still be played thou) + virtual bool trigger_action(Entity* user) = 0; + /// Activates a button prompt (with the Use door/Buy button), e.g. buy shop item, activate drill, read sign, interact in camp, ... `get_entity():activate(players[1])` (make sure player 1 has the udjat eye though) + virtual void activate(Entity* activator) = 0; + + virtual void on_collision2(Entity* other_entity) = 0; // needs investigating, difference between this and on_collision1, maybe this is on_hitbox_overlap as it works for logical tiggers + virtual uint16_t get_metadata() = 0; // e.g. for turkey: stores health, poison/curse state, for mattock: remaining swings (returned value is transferred) + virtual void apply_metadata(uint16_t metadata) = 0; + virtual void on_walked_on_by(Entity* walker) = 0; // hits when monster/player walks on a floor, does something when walker.velocityy<-0.21 (falling onto) and walker.hitboxy * hitboxx > 0.09 + virtual void on_walked_off_by(Entity* walker) = 0; // appears to be disabled in 1.23.3? hits when monster/player walks off a floor, it checks whether the walker has floor as overlay, and if so, removes walker from floor's items by calling virtual remove_item_ptr + virtual void on_ledge_grab(Entity* who) = 0; // only ACTIVEFLOOR_FALLING_PLATFORM, does something with game menager + virtual void on_stood_on_by(Entity* entity) = 0; // e.g. pots, skulls, pushblocks, ... standing on floors + virtual void toggle_backlayer_illumination() = 0; // only for CHAR_*: when going to the backlayer, turns on player emitted light + virtual void v34() = 0; // only ITEM_TORCH, calls Torch.light_up(false), can't get it to trigger + virtual void liberate_from_shop() = 0; // can also be seen as event: when you anger the shopkeeper, this function gets called for each item; can be called on shopitems individually as well and they become 'purchased' + + /// Applies changes made in `entity.type` + virtual void apply_db() = 0; // This is actually just an initialize call that is happening once after the entity is created +}; + +std::tuple get_position(uint32_t uid); +std::tuple get_render_position(uint32_t uid); + +std::tuple get_velocity(uint32_t uid); + +AABB get_hitbox(uint32_t uid, bool use_render_pos); + +Entity* get_entity_ptr(uint32_t uid); +Entity* get_entity_ptr_local(uint32_t uid); diff --git a/src/game_api/entity_db.cpp b/src/game_api/entity_db.cpp new file mode 100644 index 000000000..629a682ef --- /dev/null +++ b/src/game_api/entity_db.cpp @@ -0,0 +1,131 @@ +#include "entity.hpp" + +#include // for IsBadWritePtr +#include // for operator<=>, operator-, operator+ +#include // for round +#include // for operator<, operator<=, operator> +#include // for uint32_t, uint16_t, uint8_t +#include // for abs, NULL, size_t +#include // for _List_const_iterator +#include // for _Tree_iterator, map, _Tree_cons... +#include // for operator new +#include // for allocator, string, operator""sv +#include // for sleep_for +#include // for vector, _Vector_iterator, erase_if + +#include "containers/custom_map.hpp" // for custom_map +#include "entities_chars.hpp" // for Player +#include "entity_hooks_info.hpp" // for EntityHooksInfo +#include "memory.hpp" // for write_mem_prot +#include "movable.hpp" // for Movable +#include "movable_behavior.hpp" // for MovableBehavior +#include "render_api.hpp" // for RenderInfo +#include "search.hpp" // for get_address +#include "state.hpp" // for State, StateMemory, enum_to_layer +#include "state_structs.hpp" // for LiquidPhysicsEngine +#include "texture.hpp" // for get_texture, Texture +#include "vtable_hook.hpp" // for hook_vtable, hook_dtor, unregis... + +template +class OnHeapPointer; + +EntityDB::EntityDB(const EntityDB& other) = default; +EntityDB::EntityDB(const ENT_TYPE other) + : EntityDB(*get_type(other)) +{ +} + +using namespace std::chrono_literals; +using EntityMap = std::unordered_map; + +struct EntityBucket +{ + void** begin; + void** current; // Note, counts down from end to begin instead of up from begin to end :shrug: + void** end; +}; +struct EntityPool +{ + std::uint32_t slot_size; + std::uint32_t initial_slots; + std::uint32_t slots_growth; + std::uint32_t current_slots; + std::uint64_t _ulong_0; + EntityBucket* _some_bucket; + EntityBucket* bucket; +}; +struct EntityFactory +{ + EntityDB types[0x395]; + bool type_set[0x395]; + std::unordered_map> entity_instance_map; + EntityMap entity_map; + void* _ptr_7; +}; + +EntityFactory* entity_factory() +{ + static EntityFactory* cache_entity_factory = *(EntityFactory**)get_address("entity_factory"sv); + while (cache_entity_factory == 0) + { + std::this_thread::sleep_for(500ms); + cache_entity_factory = *(EntityFactory**)get_address("entity_factory"sv); + } + return cache_entity_factory; +} + +std::vector list_entities() +{ + const EntityFactory* entity_factory_ptr = entity_factory(); + if (!entity_factory_ptr) + return {}; + + const EntityMap& map = entity_factory_ptr->entity_map; + + std::vector result; + for (const auto& [name, id] : map) + { + result.emplace_back(name, id); + } + return result; +} + +EntityDB* get_type(uint32_t id) +{ + EntityFactory* entity_factory_ptr = entity_factory(); + + // Special case: map_ptr might be 0 if it's not initialized. + // This only occurs in list_entities; for others, do not check the pointer + // to see if this assumption works. + if (!entity_factory_ptr) + return nullptr; + + return entity_factory_ptr->types + id; +} + +ENT_TYPE to_id(std::string_view name) +{ + const EntityFactory* entity_factory_ptr = entity_factory(); + if (!entity_factory_ptr) + return {}; + const EntityMap& map = entity_factory_ptr->entity_map; + auto it = map.find(std::string(name)); + return it != map.end() ? it->second : -1; +} + +std::string_view to_name(ENT_TYPE id) +{ + const EntityFactory* entity_factory_ptr = entity_factory(); + if (entity_factory_ptr) + { + for (const auto& [name, type_id] : entity_factory_ptr->entity_map) + { + if (type_id == id) + { + return name; + } + } + } + + return {}; +} diff --git a/src/game_api/entity_db.hpp b/src/game_api/entity_db.hpp new file mode 100644 index 000000000..f7848eb08 --- /dev/null +++ b/src/game_api/entity_db.hpp @@ -0,0 +1,132 @@ +#pragma once + +#include // for size_t +#include // for uint8_t, uint32_t, int32_t, uint16_t, int64_t +#include // for function, equal_to +#include // for span +#include // for allocator, string +#include // for string_view +#include // for tuple +#include // for move +#include // for _Umap_traits<>::allocator_type, unordered_map +#include // for pair +#include // for vector + +#include "aliases.hpp" // for ENT_TYPE, LAYER, TEXTURE, STRINGID +#include "color.hpp" // for Color +#include "containers/game_unordered_map.hpp" // for game_unordered_map +#include "containers/identity_hasher.hpp" // for identity_hasher +#include "entity_structs.hpp" // for CollisionInfo +#include "layer.hpp" // for EntityList +#include "math.hpp" // for AABB + +struct RenderInfo; +struct Texture; + +// Creates an instance of this entity +using EntityCreate = Entity* (*)(); +using EntityDestroy = void (*)(Entity*); + +struct EntityDB +{ + EntityCreate create_func; + EntityDestroy destroy_func; + int32_t field_10; + ENT_TYPE id; + /// MASK + uint32_t search_flags; + float width; + float height; + uint8_t draw_depth; + uint8_t default_b3f; // value gets copied into entity.b3f along with draw_depth etc (RVA 0x21F30CC4) + int16_t field_26; + union + { + struct + { + float default_offsetx; + float default_offsety; + float default_hitboxx; + float default_hitboxy; + SHAPE default_shape; + bool default_hitbox_enabled; + uint8_t default_b82; + uint8_t default_b83; + }; + CollisionInfo default_collision_info; + }; + int32_t field_3C; + int32_t field_40; + int32_t field_44; + int32_t default_flags; + int32_t default_more_flags; + int32_t properties_flags; + float friction; + float elasticity; + float weight; + uint8_t field_60; + float acceleration; + float max_speed; + float sprint_factor; + float jump; + union + { + Color default_color; + struct + { + float glow_red; + float glow_green; + float glow_blue; + float glow_alpha; + }; + }; + int32_t texture; + int32_t technique; + int32_t tile_x; + int32_t tile_y; + uint8_t damage; + uint8_t life; + uint8_t field_96; + uint8_t blood_content; + bool leaves_corpse_behind; + uint8_t field_99; + uint8_t field_9A; + uint8_t field_9B; + STRINGID description; + int32_t sound_killed_by_player; + int32_t sound_killed_by_other; + float field_a8; + int32_t field_AC; + game_unordered_map> animations; + float default_special_offsetx; + float default_special_offsety; + uint8_t init; + + EntityDB(const EntityDB& other); + EntityDB(const ENT_TYPE other); +}; + +struct EntityItem +{ + std::string name; + uint32_t id; + + EntityItem(const std::string& name_, uint32_t id_) + : name(name_), id(id_) + { + } + bool operator<(const EntityItem& item) const + { + return id < item.id; + } +}; + +EntityDB* get_type(uint32_t id); + +ENT_TYPE to_id(std::string_view id); + +std::string_view to_name(ENT_TYPE id); + +std::vector list_entities(); + +struct EntityFactory* entity_factory(); diff --git a/src/game_api/entity_hooks_info.hpp b/src/game_api/entity_hooks_info.hpp new file mode 100644 index 000000000..8e62ed9f4 --- /dev/null +++ b/src/game_api/entity_hooks_info.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include // for int8_t, uint16_t, ... +#include // for function +#include // for vector + +class Entity; +class Movable; +class Container; + +template +struct HookWithId +{ + uint32_t id; + std::function fun; +}; + +struct EntityHooksInfo +{ + int32_t entity; + uint32_t cbcount; + std::vector> on_dtor; + std::vector> on_destroy; + std::vector> on_kill; + std::vector> pre_floor_update; + std::vector> post_floor_update; + std::vector> on_player_instagib; + std::vector> on_damage; + std::vector> pre_statemachine; + std::vector> post_statemachine; + std::vector> on_open; + std::vector> pre_collision1; + std::vector> pre_collision2; + std::vector> pre_render; + std::vector> post_render; +}; diff --git a/src/game_api/entity_structs.hpp b/src/game_api/entity_structs.hpp new file mode 100644 index 000000000..1cf5495f8 --- /dev/null +++ b/src/game_api/entity_structs.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include // for size_t +#include // for uint8_t, uint32_t, int32_t, uint16_t, int64_t +#include // for function, equal_to +#include // for span +#include // for allocator, string +#include // for string_view +#include // for tuple +#include // for move +#include // for _Umap_traits<>::allocator_type, unordered_map +#include // for pair +#include // for vector + +enum class REPEAT_TYPE : uint8_t +{ + NoRepeat, + Linear, + BackAndForth, +}; + +enum class SHAPE : uint8_t +{ + RECTANGLE = 1, + CIRCLE = 2, +}; + +struct Animation +{ + int32_t texture; + int32_t count; + int32_t interval; + uint8_t key; + REPEAT_TYPE repeat; +}; + +struct Rect +{ + float offsetx; + float offsety; + float hitboxx; + float hitboxy; +}; + +struct CollisionInfo +{ + Rect rect; + SHAPE shape; + bool hitbox_enabled; + uint8_t field_3A; + uint8_t field_3B; +}; + +struct SoundInfo +{ + int64_t unknown1; + uint32_t sound_id; + int32_t unknown2; + const char* sound_name; + int64_t unknown3; + int64_t unknown4; + int64_t unknown5; +}; + +struct SoundPosition +{ + size_t __vftable; + float x; + float y; + SoundInfo* sound_effect_info; // param to FMOD::Studio::EventInstance::SetParameterByID (this ptr + 0x30) + uint64_t fmod_param_id; // param to FMOD::Studio::EventInstance::SetParameterByID + float POS_SCREEN_X; // VANILLA_SOUND_PARAM names, for now + float DIST_CENTER_X; + float DIST_CENTER_Y; + float DIST_Z; + float DIST_PLAYER; // seams to be always here, even you you get nil in lua + float SUBMERGED; + float LIQUID_STREAM; + float unknown10; // LIQUID_STREAM related? , maybe LIQUID_INTENSITY? + float VALUE; + float unknown12; + float unknown13; + float unknown14; + float unknown15; + float unknown16; + float unknown17; + float unknown18; + float unknown19; + float unknown20; + float unknown21; + float unknown22; + float unknown23; + float unknown24; + float unknown25; + float unknown26; + float unknown27; + float unknown28; + float unknown29; + float POISONED; + float CURSED; + float unknown32; + float unknown33; + float unknown34; + float unknown35; + float unknown36; + float unknown37; + float unknown38; + float unknown39; + float unknown40; + float unknown41; // all the values repeat from this point, maybe all those floats are just an array? +}; diff --git a/src/game_api/ghidra_byte_string.hpp b/src/game_api/ghidra_byte_string.hpp new file mode 100644 index 000000000..99cfb2f67 --- /dev/null +++ b/src/game_api/ghidra_byte_string.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include // for size_t +#include // for length_error, runtime_error +#include // for string_view + +#include "tokenize.h" // for Tokenize + +template +struct GhidraByteString +{ + inline static constexpr std::size_t M = (N + 1) / 3; + char cpp_byte_string[M]{}; + + constexpr GhidraByteString(char const (&str)[N]) + { + std::size_t i{}; + for (const auto substr : Tokenize<' '>{str}) + { + if (substr.size() != 2) + { + throw std::length_error{"GhidraByteString must be constructed from a sequence of 2-char long strings."}; + } + else if (substr == "..") + { + cpp_byte_string[i] = '*'; + } + else + { + cpp_byte_string[i] = from_string(substr); + } + + i++; + } + }; + constexpr std::size_t size() const + { + return M; + } + + private: + static constexpr bool is_digit(char c) + { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); + } + static constexpr char tolower(const char c) + { + return (c >= 'A' && c <= 'F') ? c + ('a' - 'A') : c; + } + static constexpr char to_byte(char c) + { + return (c >= '0' && c <= '9') + ? c - '0' + : c - 'a' + 10; + } + + static constexpr char from_string(std::string_view str) + { + const char first = tolower(str[0]); + const char second = tolower(str[1]); + if (!is_digit(first) || !is_digit(second)) + { + throw std::runtime_error{"Not a digit"}; + } + + const char value = (to_byte(first) << 4) | to_byte(second); + return value; + } +}; + +template +constexpr auto operator"" _gh() +{ + return std::string_view{Str.cpp_byte_string, Str.size()}; +} + +#ifndef _MSC_VER +static_assert("0F 0f af 00 12 22 .. .. 12 .."_gh == "\x0F\x0f\xaf\x00\x12\x22**\x12*"sv); +#endif diff --git a/src/game_api/layer.cpp b/src/game_api/layer.cpp index 64b279fd7..d614f5547 100644 --- a/src/game_api/layer.cpp +++ b/src/game_api/layer.cpp @@ -68,7 +68,7 @@ Entity* Layer::spawn_entity_snap_to_floor(ENT_TYPE id, float x, float y) { const EntityDB* type = get_type(id); const float y_center = roundf(y) - 0.5f; - const float snapped_y = y_center + type->rect_collision.hitboxy - type->rect_collision.offsety; + const float snapped_y = y_center + type->default_collision_info.rect.hitboxy - type->default_collision_info.rect.offsety; Entity* ent = spawn_entity(id, x, snapped_y, false, 0.0f, 0.0f, false); if ((type->search_flags & 0x700) == 0) { @@ -111,6 +111,13 @@ Entity* Layer::get_grid_entity_at(float x, float y) return nullptr; } +Entity* Layer::get_entity_at(float x, float y, uint32_t search_flags, uint32_t include_flags, uint32_t exclude_flags, uint32_t one_of_flags) +{ + using get_entity_at_impl_fun = Entity*(Layer*, float, float, size_t, size_t, size_t, size_t); + const auto get_entity_at_impl = (get_entity_at_impl_fun*)get_address("layer_get_entity_at"sv); + return get_entity_at_impl(this, x, y, search_flags, include_flags, exclude_flags, one_of_flags); +} + Entity* Layer::spawn_door(float x, float y, uint8_t w, uint8_t l, uint8_t t) { auto screen = State::get().ptr()->screen_next; diff --git a/src/game_api/layer.hpp b/src/game_api/layer.hpp index da8c442c8..859226679 100644 --- a/src/game_api/layer.hpp +++ b/src/game_api/layer.hpp @@ -193,6 +193,8 @@ struct Layer Entity* get_grid_entity_at(float x, float y); + Entity* get_entity_at(float x, float y, uint32_t search_flags, uint32_t include_flags, uint32_t exclude_flags, uint32_t one_of_flags); + void move_grid_entity(Entity* ent, float x, float y, Layer* dest_layer); void move_grid_entity(Entity* ent, uint32_t x, uint32_t y, Layer* dest_layer); }; diff --git a/src/game_api/level_api.cpp b/src/game_api/level_api.cpp index ba31aa2a0..b8b8056dc 100644 --- a/src/game_api/level_api.cpp +++ b/src/game_api/level_api.cpp @@ -1158,12 +1158,12 @@ void LevelGenData::init() } { - g_room_template_types.push_back({to_uint(RoomTemplate::MachineBigroomPath), RoomTemplateType::MachineRoom}); - g_room_template_types.push_back({to_uint(RoomTemplate::FeelingTomb), RoomTemplateType::MachineRoom}); - g_room_template_types.push_back({to_uint(RoomTemplate::MachineWideroomPath), RoomTemplateType::MachineRoom}); - g_room_template_types.push_back({to_uint(RoomTemplate::MachineWideroomSide), RoomTemplateType::MachineRoom}); - g_room_template_types.push_back({to_uint(RoomTemplate::MachineTallroomPath), RoomTemplateType::MachineRoom}); - g_room_template_types.push_back({to_uint(RoomTemplate::CoffinFrog), RoomTemplateType::MachineRoom}); + g_room_template_types.push_back({to_uint(RoomTemplate::MachineBigroomPath), RoomTemplateType::VanillaMachineRoom}); + g_room_template_types.push_back({to_uint(RoomTemplate::FeelingTomb), RoomTemplateType::VanillaMachineRoom}); + g_room_template_types.push_back({to_uint(RoomTemplate::MachineWideroomPath), RoomTemplateType::VanillaMachineRoom}); + g_room_template_types.push_back({to_uint(RoomTemplate::MachineWideroomSide), RoomTemplateType::VanillaMachineRoom}); + g_room_template_types.push_back({to_uint(RoomTemplate::MachineTallroomPath), RoomTemplateType::VanillaMachineRoom}); + g_room_template_types.push_back({to_uint(RoomTemplate::CoffinFrog), RoomTemplateType::VanillaMachineRoom}); } // Scan tile codes to know what id to start at @@ -1704,7 +1704,8 @@ bool LevelGenSystem::set_room_template(uint32_t x, uint32_t y, int l, uint16_t r level_rooms->rooms[x + y * 8] = room_template; // Unset machine room origin flag if it is a machine room so there's no accidental origins left within the machine room - if (data->get_room_template_type(room_template) == RoomTemplateType::MachineRoom) + const RoomTemplateType type = data->get_room_template_type(room_template); + if (type == RoomTemplateType::MachineRoom || type == RoomTemplateType::VanillaMachineRoom) { machine_room_origin->rooms[x + y * 8] = false; } diff --git a/src/game_api/level_api.hpp b/src/game_api/level_api.hpp index ba76549b5..0133fa9a7 100644 --- a/src/game_api/level_api.hpp +++ b/src/game_api/level_api.hpp @@ -61,6 +61,7 @@ struct SpawnLogicProvider enum class RoomTemplateType { + VanillaMachineRoom = -1, None = 0, Entrance = 1, Exit = 2, diff --git a/src/game_api/movable.hpp b/src/game_api/movable.hpp index b5dab80dd..5a061b6ea 100644 --- a/src/game_api/movable.hpp +++ b/src/game_api/movable.hpp @@ -109,7 +109,7 @@ class Movable : public Entity uint32_t get_behavior(); virtual bool can_jump() = 0; - virtual void v38() = 0; + virtual void get_collision_info(CollisionInfo*) = 0; virtual float sprint_factor() = 0; virtual void calculate_jump_height() = 0; // when disabled, jump height is very high virtual std::unordered_map& get_animation_map() = 0; diff --git a/src/game_api/movable_behavior.cpp b/src/game_api/movable_behavior.cpp index a09911595..1fcc71659 100644 --- a/src/game_api/movable_behavior.cpp +++ b/src/game_api/movable_behavior.cpp @@ -1,14 +1,19 @@ #include "movable_behavior.hpp" -#include // for _List_iterator, _List_const_ite... -#include // for _Tree_iterator, _Tree_const_ite... -#include // for operator""sv -#include // for min, max, pair +#include // for DetourTransactionBegin, DetourUpdateThread, ... +#include // for _List_iterator, _List_const_ite... +#include // for _Tree_iterator, _Tree_const_ite... +#include // for operator""sv +#include // for min, max, pair #include "containers/custom_map.hpp" // for custom_map #include "containers/custom_set.hpp" // for custom_set +#include "memory.hpp" // for Memory #include "movable.hpp" // for Movable #include "search.hpp" // for get_address +#include "util.hpp" // for OnScopeExit +#include "virtual_table.hpp" // for get_virtual_function_address, ... +#include "vtable_hook.hpp" // for register_hook_function class Entity; @@ -190,7 +195,50 @@ void update_movable(Movable* movable, bool disable_gravity) void update_movable(Movable* movable, Vec2 move, float sprint_factor, bool disable_gravity, bool on_rope) { static UpdateMovable* update_movable_impl = (UpdateMovable*)get_address("update_movable"sv); - update_movable_impl(*movable, move, sprint_factor, true, disable_gravity, on_rope, false); + update_movable_impl(*movable, move, sprint_factor, false, disable_gravity, on_rope, false); +} + +struct TurningEntityInfo +{ + Entity* entity; + uint32_t dtor_hook; +}; +std::vector g_entities_disabled_turning{}; +void set_entity_turning(class Entity* entity, bool enabled) +{ + auto find_entity_pred = [=](const TurningEntityInfo& turning_info) + { return turning_info.entity == entity; }; + + if (!enabled) + { + g_entities_disabled_turning.push_back({ + entity, + entity->set_on_dtor([=](Entity*) + { std::erase_if(g_entities_disabled_turning, find_entity_pred); }), + }); + } + else + { + auto it = std::find_if(g_entities_disabled_turning.begin(), g_entities_disabled_turning.end(), find_entity_pred); + if (it == g_entities_disabled_turning.end()) + { + entity->unhook(it->dtor_hook); + g_entities_disabled_turning.erase(it); + } + } +} + +using EntityTurn = void(Entity*, bool); +EntityTurn* g_entity_turn_trampoline{nullptr}; +void entity_turn(Entity* self, bool apply) +{ + auto find_entity_pred = [=](const TurningEntityInfo& turning_info) + { return turning_info.entity == self; }; + + if (std::find_if(g_entities_disabled_turning.begin(), g_entities_disabled_turning.end(), find_entity_pred) == g_entities_disabled_turning.end()) + { + g_entity_turn_trampoline(self, apply); + } } #ifdef HOOK_MOVE_ENTITY @@ -199,8 +247,7 @@ void update_movable(Movable& movable, const Vec2& move_xy, float sprint_factor, { g_update_movable_trampoline(movable, move_xy, sprint_factor, apply_move, disable_gravity, on_ladder, param_7); } - -#include +#endif void init_behavior_hooks() { @@ -208,18 +255,18 @@ void init_behavior_hooks() DetourUpdateThread(GetCurrentThread()); auto memory = Memory::get(); - g_update_movable_trampoline = (UpdateMovable*)memory.at_exe(0x228e3580); - DetourAttach((void**)&g_update_movable_trampoline, (UpdateMovable*)update_movable); + g_entity_turn_trampoline = (EntityTurn*)memory.at_exe(get_virtual_function_address(VTABLE_OFFSET::MONS_SNAKE, 0x10)); + DetourAttach((void**)&g_entity_turn_trampoline, &entity_turn); + +#ifdef HOOK_MOVE_ENTITY + g_update_movable_trampoline = (UpdateMovable*)memory.at_exe(0x228e3580); + DetourAttach((void**)&g_update_movable_trampoline, &update_movable); +#endif const LONG error = DetourTransactionCommit(); if (error != NO_ERROR) { - DEBUG("Failed hooking MoveEntity: {}\n", error); + DEBUG("Failed hooking behavior hooks: {}\n", error); } } -#else -void init_behavior_hooks() -{ -} -#endif diff --git a/src/game_api/movable_behavior.hpp b/src/game_api/movable_behavior.hpp index 043a2099f..fcc7ea397 100644 --- a/src/game_api/movable_behavior.hpp +++ b/src/game_api/movable_behavior.hpp @@ -88,4 +88,6 @@ void update_movable(Movable* movable, bool disable_gravity); /// Will also update `movable.animation_frame` and various timers and counters void update_movable(Movable* movable, Vec2 move, float sprint_factor, bool disable_gravity, bool on_rope); +void set_entity_turning(class Entity* entity, bool enabled); + void init_behavior_hooks(); diff --git a/src/game_api/render_api.hpp b/src/game_api/render_api.hpp index e9fb2962c..a8a05f497 100644 --- a/src/game_api/render_api.hpp +++ b/src/game_api/render_api.hpp @@ -175,7 +175,10 @@ struct RenderInfo float darkness; // 0.0 = completely black ; 1.0 = normal (dark effect like when on fire) uint32_t unknown56; uint32_t unknown57; - uint32_t unknown58; // end, next RenderInfo below + uint32_t unknown58; + float* unknown59; + uint32_t unknown60; + uint32_t unknown61; // end, next RenderInfo below }; struct TextRenderingInfo diff --git a/src/game_api/rpc.cpp b/src/game_api/rpc.cpp index d49918bf5..4a581b912 100644 --- a/src/game_api/rpc.cpp +++ b/src/game_api/rpc.cpp @@ -1239,14 +1239,17 @@ void set_camp_camera_bounds_enabled(bool b) } } -void set_explosion_mask(uint32_t mask) +void set_explosion_mask(int32_t mask) { - static size_t addr = 0; - if (addr == 0) + static size_t addr = get_address("explosion_mask"); + if (mask == -1) { - addr = get_address("explosion_mask"); + recover_mem("explosion_mask"); + } + else + { + write_mem_recoverable("explosion_mask", addr, mask, true); } - write_mem_prot(addr, mask, true); } void set_max_rope_length(uint8_t length) @@ -1268,6 +1271,11 @@ void set_max_rope_length(uint8_t length) write_mem_prot(get_address("process_ropes_three"), length_minus_one_8, true); } +uint8_t get_max_rope_length() +{ + return static_cast(read_u32(get_address("attach_thrown_rope_to_background"))); +} + uint8_t waddler_count_entity(ENT_TYPE entity_type) { auto state = get_state_ptr(); @@ -1703,7 +1711,7 @@ void move_grid_entity(int32_t uid, float x, float y, LAYER layer) } } -void add_item_to_shop(int32_t item_uid, int32_t shop_owner) +void add_item_to_shop(int32_t item_uid, int32_t shop_owner_uid) { struct dummy // dummy struct { @@ -1713,7 +1721,7 @@ void add_item_to_shop(int32_t item_uid, int32_t shop_owner) using AddRestrictedItemFun = size_t(std::set*, dummy, ShopRestrictedItem); Movable* item = get_entity_ptr(item_uid)->as(); - Entity* owner = get_entity_ptr(shop_owner); + Entity* owner = get_entity_ptr(shop_owner_uid); if (item && owner && item->is_movable()) { const static auto room_owners = { @@ -1742,7 +1750,7 @@ void add_item_to_shop(int32_t item_uid, int32_t shop_owner) const auto bucket = add_to_items_set(&state.ptr()->shops.items, {state.ptr()->shops.items.end(), 0}, {item_uid, 0, 0}); auto the_struct = (ShopRestrictedItem*)(bucket + 0x1C); // 0x1C - is just the internal map/set stuff - the_struct->owner_uid = shop_owner; + the_struct->owner_uid = shop_owner_uid; the_struct->owner_type = owner->type->id; return; } diff --git a/src/game_api/rpc.hpp b/src/game_api/rpc.hpp index f3ab395dc..3d75f0966 100644 --- a/src/game_api/rpc.hpp +++ b/src/game_api/rpc.hpp @@ -103,8 +103,9 @@ Illumination* create_illumination(Color color, float size, uint32_t uid); void refresh_illumination(Illumination* illumination); void set_journal_enabled(bool b); void set_camp_camera_bounds_enabled(bool b); -void set_explosion_mask(uint32_t mask); +void set_explosion_mask(int32_t mask); void set_max_rope_length(uint8_t length); +uint8_t get_max_rope_length(); bool is_inside_active_shop_room(float x, float y, LAYER layer); bool is_inside_shop_zone(float x, float y, LAYER layer); uint8_t waddler_count_entity(ENT_TYPE entity_type); @@ -123,7 +124,7 @@ void change_waddler_drop(std::vector ent_types); void poison_entity(int32_t entity_uid); void modify_ankh_health_gain(uint8_t max_health, uint8_t beat_add_health); void move_grid_entity(int32_t uid, float x, float y, LAYER layer); -void add_item_to_shop(int32_t item_uid, int32_t shop_owner); +void add_item_to_shop(int32_t item_uid, int32_t shop_owner_uid); void change_poison_timer(int16_t frames); void set_adventure_seed(int64_t first, int64_t second); std::pair get_adventure_seed(); diff --git a/src/game_api/screen.cpp b/src/game_api/screen.cpp index d10194918..b2ed91fa2 100644 --- a/src/game_api/screen.cpp +++ b/src/game_api/screen.cpp @@ -8,12 +8,12 @@ #include // for find_if, min #include // for vector, _Vector_iterator, allocator, era... -#include "entity.hpp" // for HookWithId -#include "game_manager.hpp" // for GameManager, get_game_manager -#include "logger.h" // for DEBUG -#include "screen_arena.hpp" // for ScreenArenaIntro, ScreenArenaItems, Scre... -#include "state.hpp" // for StateMemory, get_state_ptr -#include "vtable_hook.hpp" // for hook_vtable_no_dtor +#include "entity_hooks_info.hpp" // for HookWithId +#include "game_manager.hpp" // for GameManager, get_game_manager +#include "logger.h" // for DEBUG +#include "screen_arena.hpp" // for ScreenArenaIntro, ScreenArenaItems, Scre... +#include "state.hpp" // for StateMemory, get_state_ptr +#include "vtable_hook.hpp" // for hook_vtable_no_dtor struct ScreenHooksInfo { diff --git a/src/game_api/script/lua_backend.cpp b/src/game_api/script/lua_backend.cpp index e743f3459..13ba051aa 100644 --- a/src/game_api/script/lua_backend.cpp +++ b/src/game_api/script/lua_backend.cpp @@ -1315,20 +1315,42 @@ LuaBackend* LuaBackend::get_backend_by_id(std::string_view id, std::string_view } return nullptr; } + +static LuaBackend* g_CallingBackend{nullptr}; LuaBackend* LuaBackend::get_calling_backend() { + if (g_CallingBackend) + { + return g_CallingBackend; + } + static const sol::state& lua = get_lua_vm(); auto get_script_id = lua["get_script_id"]; if (get_script_id.get_type() == sol::type::function) { auto script_id = get_script_id(); - if (script_id.get_type() == sol::type::string) + if (script_id.get_type() == sol::type::string && script_id.valid()) { return LuaBackend::get_backend(script_id.get()); } + else + { + sol::error e = script_id; + throw std::runtime_error{e.what()}; + } } return nullptr; } +void LuaBackend::push_calling_backend(LuaBackend* calling_backend) +{ + assert(g_CallingBackend == nullptr); + g_CallingBackend = calling_backend; +} +void LuaBackend::pop_calling_backend([[maybe_unused]] LuaBackend* calling_backend) +{ + assert(g_CallingBackend == calling_backend); + g_CallingBackend = nullptr; +} CurrentCallback LuaBackend::get_current_callback() { diff --git a/src/game_api/script/lua_backend.hpp b/src/game_api/script/lua_backend.hpp index b6cb803d4..c4f15ba38 100644 --- a/src/game_api/script/lua_backend.hpp +++ b/src/game_api/script/lua_backend.hpp @@ -28,6 +28,7 @@ #include "level_api.hpp" // IWYU pragma: keep #include "logger.h" // for DEBUG #include "script.hpp" // for ScriptMessage, ScriptImage (ptr only), Scri... +#include "util.hpp" // for ON_SCOPE_EXIT class Player; class JournalPage; @@ -361,7 +362,10 @@ class LuaBackend static void for_each_backend(std::function fun); static LuaBackend* get_backend(std::string_view id); static LuaBackend* get_backend_by_id(std::string_view id, std::string_view ver = ""); + static LuaBackend* get_calling_backend(); + static void push_calling_backend(LuaBackend*); + static void pop_calling_backend(LuaBackend*); CurrentCallback get_current_callback(); void set_current_callback(int uid, int id, CallbackType type); @@ -376,6 +380,9 @@ bool LuaBackend::handle_function(sol::function func, Args&&... args) template std::optional LuaBackend::handle_function_with_return(sol::function func, Args&&... args) { + LuaBackend::push_calling_backend(this); + ON_SCOPE_EXIT(LuaBackend::pop_calling_backend(this)); + auto lua_result = func(std::forward(args)...); if (!lua_result.valid()) { diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp index cf99dc60d..14f553346 100644 --- a/src/game_api/script/lua_vm.cpp +++ b/src/game_api/script/lua_vm.cpp @@ -653,10 +653,10 @@ end lua["layer_door"] = spawn_backdoor_abs; /// Spawns apep with the choice if it going left or right, if you want the game to choose use regular spawn functions with `ENT_TYPE.MONS_APEP_HEAD` lua["spawn_apep"] = spawn_apep; - auto spawn_tree = sol::overload( - static_cast(::spawn_tree), - static_cast(::spawn_tree)); + auto spawn_tree = sol::overload( + static_cast(::spawn_tree), + static_cast(::spawn_tree)); /// Spawns and grows a tree lua["spawn_tree"] = spawn_tree; @@ -667,6 +667,14 @@ end /// Regardless, if there is not enough space, it will spawn shorter one or if there is no space even for the smallest one, it will just not spawn at all /// Returns uid of the base or -1 if it wasn't able to spawn lua["spawn_mushroom"] = spawn_mushroom; + + auto spawn_unrolled_player_rope = sol::overload( + static_cast(::spawn_unrolled_player_rope), + static_cast(::spawn_unrolled_player_rope)); + + /// Spawns an already unrolled rope as if created by player + lua["spawn_unrolled_player_rope"] = spawn_unrolled_player_rope; + /// NoDoc /// Spawns an impostor lake, `top_threshold` determines how much space on top is rendered as liquid but does not have liquid physics, fill that space with real liquid /// There needs to be other liquid in the level for the impostor lake to be visible, there can only be one impostor lake in the level diff --git a/src/game_api/script/usertypes/behavior_lua.cpp b/src/game_api/script/usertypes/behavior_lua.cpp index a34fbb903..9f0170389 100644 --- a/src/game_api/script/usertypes/behavior_lua.cpp +++ b/src/game_api/script/usertypes/behavior_lua.cpp @@ -62,6 +62,8 @@ void register_usertypes(sol::state& lua) lua.new_usertype( "CustomMovableBehavior", sol::no_constructor, + "base_behavior", + &CustomMovableBehavior::base_behavior, sol::base_classes, sol::bases()); diff --git a/src/game_api/script/usertypes/entity_lua.cpp b/src/game_api/script/usertypes/entity_lua.cpp index ef059c689..6cf7fc3b1 100644 --- a/src/game_api/script/usertypes/entity_lua.cpp +++ b/src/game_api/script/usertypes/entity_lua.cpp @@ -79,6 +79,8 @@ void register_usertypes(sol::state& lua) &Color::set_ucolor); lua.new_usertype( "Animation", + "id", + &Animation::key, "first_tile", &Animation::texture, "num_tiles", @@ -89,6 +91,7 @@ void register_usertypes(sol::state& lua) &Animation::repeat); lua.new_usertype( "EntityDB", + sol::constructors{}, "id", &EntityDB::id, "search_flags", @@ -98,17 +101,13 @@ void register_usertypes(sol::state& lua) "height", &EntityDB::height, "offsetx", - sol::property([](EntityDB& e) -> float - { return e.rect_collision.offsetx; }), + &EntityDB::default_offsetx, "offsety", - sol::property([](EntityDB& e) -> float - { return e.rect_collision.offsety; }), + &EntityDB::default_offsety, "hitboxx", - sol::property([](EntityDB& e) -> float - { return e.rect_collision.hitboxx; }), + &EntityDB::default_hitboxx, "hitboxy", - sol::property([](EntityDB& e) -> float - { return e.rect_collision.hitboxy; }), + &EntityDB::default_hitboxy, "draw_depth", &EntityDB::draw_depth, "friction", @@ -299,6 +298,8 @@ void register_usertypes(sol::state& lua) &Entity::set_texture, "set_draw_depth", &Entity::set_draw_depth, + "set_enable_turning", + &Entity::set_enable_turning, "liberate_from_shop", &Entity::liberate_from_shop, "get_held_entity", diff --git a/src/game_api/search.cpp b/src/game_api/search.cpp index 90bd09802..5a3478d04 100644 --- a/src/game_api/search.cpp +++ b/src/game_api/search.cpp @@ -19,9 +19,10 @@ #include // for vector, _Vector_const_iterator, _Vector... // clang-format on -#include "logger.h" // for ByteStr, DEBUG -#include "memory.hpp" // for Memory, function_start -#include "virtual_table.hpp" // for VIRT_FUNC, VTABLE_OFFSET, VIRT_FUNC::LO... +#include "ghidra_byte_string.hpp" // for operator""_gh +#include "logger.h" // for ByteStr, DEBUG +#include "memory.hpp" // for Memory, function_start +#include "virtual_table.hpp" // for VIRT_FUNC, VTABLE_OFFSET, VIRT_FUNC::LO... // Decodes the program counter inside an instruction // The default simple variant is 3 bytes instruction, 4 bytes rel. address, 0 bytes suffix: @@ -32,7 +33,7 @@ // e.g. mov word ptr[XXXXXXXX], 1 = 66:C705 XXXXXXXX 0100 (opcode_suffix_offset = 2) size_t decode_pc(const char* exe, size_t offset, uint8_t opcode_offset, uint8_t opcode_suffix_offset, uint8_t opcode_addr_size) { - off_t rel; + ptrdiff_t rel; switch (opcode_addr_size) { case 1: @@ -45,13 +46,27 @@ size_t decode_pc(const char* exe, size_t offset, uint8_t opcode_offset, uint8_t default: rel = *(int32_t*)(&exe[offset + opcode_offset]); break; + case 8: + rel = *(int64_t*)(&exe[offset + opcode_offset]); + break; } return offset + rel + opcode_offset + opcode_addr_size + opcode_suffix_offset; } -size_t decode_imm(const char* exe, size_t offset, uint8_t opcode_offset) +size_t decode_imm(const char* exe, size_t offset, uint8_t opcode_offset, uint8_t value_size) { - return *(uint32_t*)(&exe[offset + opcode_offset]); + switch (value_size) + { + case 1: + return *(uint8_t*)(&exe[offset + opcode_offset]); + case 2: + return *(uint16_t*)(&exe[offset + opcode_offset]); + case 4: + default: + return *(uint32_t*)(&exe[offset + opcode_offset]); + case 8: + return *(uint64_t*)(&exe[offset + opcode_offset]); + } } PIMAGE_NT_HEADERS RtlImageNtHeader(_In_ PVOID Base) @@ -267,9 +282,9 @@ class PatternCommandBuffer commands.push_back({CommandType::DecodePC, {.decode_pc_args = {opcode_prefix, opcode_suffix, opcode_addr}}}); return *this; } - PatternCommandBuffer& decode_imm(uint8_t opcode_prefix = 3) + PatternCommandBuffer& decode_imm(uint8_t opcode_prefix = 3, uint8_t value_size = 4) { - commands.push_back({CommandType::DecodeIMM, {.decode_imm_prefix = opcode_prefix}}); + commands.push_back({CommandType::DecodeIMM, {.decode_imm_args = {opcode_prefix, value_size}}}); return *this; } PatternCommandBuffer& decode_call() @@ -282,6 +297,11 @@ class PatternCommandBuffer commands.push_back({CommandType::AtExe}); return *this; } + PatternCommandBuffer& from_exe() + { + commands.push_back({CommandType::FromExe}); + return *this; + } PatternCommandBuffer& function_start(uint8_t outside_byte = 0xcc) { commands.push_back({CommandType::FunctionStart, {.outside_byte = outside_byte}}); @@ -359,7 +379,9 @@ class PatternCommandBuffer data.decode_pc_args.as_tuple()); break; case CommandType::DecodeIMM: - offset = ::decode_imm(exe, offset, data.decode_imm_prefix); + offset = std::apply([=](auto... args) + { return ::decode_imm(exe, offset, args...); }, + data.decode_imm_args.as_tuple()); break; case CommandType::DecodeCall: offset = mem.decode_call(offset); @@ -367,6 +389,9 @@ class PatternCommandBuffer case CommandType::AtExe: offset = mem.at_exe(offset); break; + case CommandType::FromExe: + offset = offset - mem.exe_ptr; + break; case CommandType::FunctionStart: offset = ::function_start(offset, data.outside_byte); break; @@ -394,6 +419,16 @@ class PatternCommandBuffer return {opcode_offset, opcode_suffix_offset, opcode_addr_size}; } }; + struct DecodeImmArgs + { + uint8_t opcode_offset; + uint8_t value_size; + + std::tuple as_tuple() const + { + return {opcode_offset, value_size}; + } + }; struct FindInstArgs { std::string_view pattern; @@ -416,6 +451,7 @@ class PatternCommandBuffer DecodeIMM, DecodeCall, AtExe, + FromExe, FunctionStart, FromExeBase, }; @@ -426,7 +462,7 @@ class PatternCommandBuffer FindInstArgs find_inst_args; int64_t offset; DecodePcArgs decode_pc_args; - uint8_t decode_imm_prefix; + DecodeImmArgs decode_imm_args; GetVirtualFunctionAddressArgs get_vfunc_addr_args; uint64_t base_offset; uint8_t outside_byte; @@ -610,6 +646,16 @@ std::unordered_map g_address_rules{ .decode_call() .at_exe(), }, + { + "layer_get_entity_at"sv, + // Set a bp on spawning entity ENT_TYPE_ITEM_CLIMBABLE_ROPE, then throw a rope, skip first bp + // On second bp go back to the caller of load_entity, somewhere upwards this function is called as + // layer_get_entity_at(layer, x, y, 0x180, 4, 8, ???) + PatternCommandBuffer{} + .find_inst("f3 0f 10 5f 7c 0f 28 e2"_gh) + .at_exe() + .function_start(), + }, { "virtual_functions_table"sv, // Look at any entity in memory, dereference the __vftable to see the big table of pointers @@ -1117,6 +1163,25 @@ std::unordered_map g_address_rules{ .offset(0x02) .at_exe(), }, + { + "setup_top_rope_rendering_info_one"sv, + // After creating the top rope entity (see attach_thrown_rope_to_background) + // two functions are called on the rope->rendering_info + PatternCommandBuffer{} + .get_address("attach_thrown_rope_to_background"sv) + .find_after_inst("48 8b 8b 88 00 00 00"_gh) + .decode_call() + .at_exe(), + }, + { + "setup_top_rope_rendering_info_two"sv, + // See setup_top_rope_rendering_info_one + PatternCommandBuffer{} + .get_address("attach_thrown_rope_to_background"sv) + .find_after_inst("41 b8 02 00 00 00"_gh) + .decode_call() + .at_exe(), + }, { "mount_carry"sv, // Set a bp on player's Entity::overlay, then jump on a turkey diff --git a/src/game_api/search.hpp b/src/game_api/search.hpp index fb0884312..539c3c024 100644 --- a/src/game_api/search.hpp +++ b/src/game_api/search.hpp @@ -9,7 +9,7 @@ using namespace std::string_view_literals; size_t decode_pc(const char* exe, size_t offset, uint8_t opcode_offset = 3, uint8_t opcode_suffix_offset = 0, uint8_t opcode_addr_size = 4); -size_t decode_imm(const char* exe, size_t offset, uint8_t opcode_offset = 3); +size_t decode_imm(const char* exe, size_t offset, uint8_t opcode_offset = 3, uint8_t value_size = 4); // Find the location of the instruction (needle) with wildcard (* or \x2a) support // Optional pattern_name for better error messages diff --git a/src/game_api/spawn_api.cpp b/src/game_api/spawn_api.cpp index cfe453bd8..f4822c4cd 100644 --- a/src/game_api/spawn_api.cpp +++ b/src/game_api/spawn_api.cpp @@ -13,6 +13,7 @@ #include // for vector, allocator, _Vector_iterator #include "entities_chars.hpp" // for Player +#include "entities_items.hpp" // for ClimbableRope #include "entities_liquids.hpp" // for Lava #include "entities_monsters.hpp" // for Shopkeeper, RoomOwner #include "entity.hpp" // for to_id, Entity, get_entity_ptr, Enti... @@ -253,12 +254,11 @@ int32_t spawn_apep(float x, float y, LAYER layer, bool right) return State::get().layer_local(actual_layer)->spawn_apep(x + offset_position.first, y + offset_position.second, right)->uid; } -void spawn_tree(float x, float y, LAYER layer) +int32_t spawn_tree(float x, float y, LAYER layer) { - spawn_tree(x, y, layer, 0); + return spawn_tree(x, y, layer, 0); } - -void spawn_tree(float x, float y, LAYER layer, uint16_t height) +int32_t spawn_tree(float x, float y, LAYER layer, uint16_t height) { push_spawn_type_flags(SPAWN_TYPE_SCRIPT); OnScopeExit pop{[] @@ -277,7 +277,7 @@ void spawn_tree(float x, float y, LAYER layer, uint16_t height) layer_ptr->get_grid_entity_at(x, y) != nullptr || layer_ptr->get_grid_entity_at(x, y + 1.0f) != nullptr || layer_ptr->get_grid_entity_at(x, y + 2.0f) != nullptr) - return; + return -1; static const auto tree_base = to_id("ENT_TYPE_FLOOR_TREE_BASE"); static const auto tree_trunk = to_id("ENT_TYPE_FLOOR_TREE_TRUNK"); @@ -288,7 +288,8 @@ void spawn_tree(float x, float y, LAYER layer, uint16_t height) PRNG& prng = PRNG::get_local(); // spawn the base - Entity* current_pice = layer_ptr->spawn_entity(tree_base, x, y, false, 0.0f, 0.0f, true); + Entity* current_piece = layer_ptr->spawn_entity(tree_base, x, y, false, 0.0f, 0.0f, true); + Entity* base_piece = current_piece; // spawn segments if (layer_ptr->get_grid_entity_at(x, y + 3.0f) == nullptr) @@ -301,7 +302,7 @@ void spawn_tree(float x, float y, LAYER layer, uint16_t height) { break; } - current_pice = layer_ptr->spawn_entity_over(tree_trunk, current_pice, 0.0f, 1.0f); + current_piece = layer_ptr->spawn_entity_over(tree_trunk, current_piece, 0.0f, 1.0f); if (height == 0 && prng.random_chance(2, PRNG::PRNG_CLASS::ENTITY_VARIATION)) { break; @@ -309,7 +310,7 @@ void spawn_tree(float x, float y, LAYER layer, uint16_t height) } } // spawn the top - current_pice = layer_ptr->spawn_entity_over(tree_top, current_pice, 0.0f, 1.0f); + current_piece = layer_ptr->spawn_entity_over(tree_top, current_piece, 0.0f, 1.0f); do // spawn branches { @@ -320,32 +321,32 @@ void spawn_tree(float x, float y, LAYER layer, uint16_t height) if (left) deco->flags |= 1U << 16; // flag 17: facing left }; - auto test_pos = current_pice->position(); + auto test_pos = current_piece->position(); if (static_cast(test_pos.first) + 1 < g_level_max_x && layer_ptr->get_grid_entity_at(test_pos.first + 1, test_pos.second) == nullptr && prng.random_chance(2, PRNG::PRNG_CLASS::ENTITY_VARIATION)) { - Entity* branch = layer_ptr->spawn_entity_over(tree_branch, current_pice, 1.02f, 0.0f); + Entity* branch = layer_ptr->spawn_entity_over(tree_branch, current_piece, 1.02f, 0.0f); spawn_deco(branch, false); } if (static_cast(test_pos.first) - 1 > 0 && layer_ptr->get_grid_entity_at(test_pos.first - 1, test_pos.second) == nullptr && prng.random_chance(2, PRNG::PRNG_CLASS::ENTITY_VARIATION)) { - Entity* branch = layer_ptr->spawn_entity_over(tree_branch, current_pice, -1.02f, 0.0f); + Entity* branch = layer_ptr->spawn_entity_over(tree_branch, current_piece, -1.02f, 0.0f); branch->flags |= 1U << 16; // flag 17: facing left spawn_deco(branch, true); } - current_pice = current_pice->overlay; - } while (current_pice->overlay); + current_piece = current_piece->overlay; + } while (current_piece->overlay); + + return base_piece->uid; } int32_t spawn_mushroom(float x, float y, LAYER l) { return spawn_mushroom(x, y, l, 0); } - -// height relates to trunk -int32_t spawn_mushroom(float x, float y, LAYER l, uint16_t height) +int32_t spawn_mushroom(float x, float y, LAYER l, uint16_t height) // height relates to trunk { push_spawn_type_flags(SPAWN_TYPE_SCRIPT); OnScopeExit pop{[] @@ -403,6 +404,102 @@ int32_t spawn_mushroom(float x, float y, LAYER l, uint16_t height) return base_uid; } +int32_t spawn_unrolled_player_rope(float x, float y, LAYER layer, TEXTURE texture) +{ + return spawn_unrolled_player_rope(x, y, layer, texture, static_cast(read_u32(get_address("attach_thrown_rope_to_background")))); +} +int32_t spawn_unrolled_player_rope(float x, float y, LAYER layer, TEXTURE texture, uint16_t max_length) +{ + push_spawn_type_flags(SPAWN_TYPE_SCRIPT); + OnScopeExit pop{[] + { pop_spawn_type_flags(SPAWN_TYPE_SCRIPT); }}; + + using setup_top_rope_rendering_info_one_fun = void(RenderInfo*); + using setup_top_rope_rendering_info_two_fun = void(RenderInfo*, int32_t, int32_t); + static const auto setup_top_rope_rendering_info_one = (setup_top_rope_rendering_info_one_fun*)get_address("setup_top_rope_rendering_info_one"sv); + static const auto setup_top_rope_rendering_info_two = (setup_top_rope_rendering_info_two_fun*)get_address("setup_top_rope_rendering_info_two"sv); + static const auto rope_ent = to_id("ENT_TYPE_ITEM_CLIMBABLE_ROPE"); + + std::pair offset(0.0f, 0.0f); + const auto actual_layer = enum_to_layer(layer, offset); + const auto layer_ptr = State::get().layer_local(actual_layer); + const uint32_t i_x = static_cast(x + offset.first + 0.5f); + const uint32_t i_y = static_cast(y + offset.second + 0.5f); + const float g_x = static_cast(i_x); + const float g_y = static_cast(i_y); + + auto has_solid_ent = [=](float gx, float gy) -> bool + { + { + Movable* ent = static_cast(layer_ptr->get_entity_at(gx, gy, 0x180, 0x4, 0x8, 0)); + if (ent) + { + return ent->type->search_flags == 0x100 || (ent->velocityx == 0.0 && ent->velocityy == 0.0); // see 0x2299c90f + } + } + + { + Entity* grid_ent = layer_ptr->get_grid_entity_at(gx, gy); + if (grid_ent) + { + return (grid_ent->flags & 0x1c) == 0x4; // see 0x2299cd7c + } + } + + return false; + }; + + if (has_solid_ent(g_x, g_y)) + { + return -1; + } + + constexpr uint16_t anim_frame_top = 157; + constexpr uint16_t anim_frame_single = 158; + constexpr uint16_t anim_frame_middle = 192; + constexpr uint16_t anim_frame_bottom = 197; + + ClimbableRope* top_part = static_cast(layer_ptr->spawn_entity(rope_ent, g_x, g_y, false, 0, 0, true)); + top_part->set_texture(texture); + top_part->animation_frame = anim_frame_single; + top_part->idle_counter = 5; + top_part->segment_nr = 0; + top_part->segment_nr_inverse = max_length; + setup_top_rope_rendering_info_one(top_part->rendering_info); + setup_top_rope_rendering_info_two(top_part->rendering_info, 7, 2); + + ClimbableRope* above_part = top_part; + for (size_t i = 1; i <= max_length; i++) + { + if (has_solid_ent(g_x, g_y - static_cast(i))) + { + break; + } + + ClimbableRope* next_part = static_cast(layer_ptr->spawn_entity_over(rope_ent, above_part, 0, -1)); + next_part->set_texture(texture); + next_part->animation_frame = anim_frame_bottom; + next_part->idle_counter = 5; + next_part->segment_nr = static_cast(i); + next_part->segment_nr_inverse = max_length - static_cast(i); + next_part->above_part = above_part; + setup_top_rope_rendering_info_one(next_part->rendering_info); + + above_part->below_part = next_part; + if (i == 1) + { + above_part->animation_frame = anim_frame_top; + } + else + { + above_part->animation_frame = anim_frame_middle; + } + above_part = next_part; + } + + return top_part->uid; +} + Entity* spawn_impostor_lake(AABB aabb, LAYER layer, ENT_TYPE impostor_type, float top_threshold) { static const auto impostor_lake_id = to_id("ENT_TYPE_LIQUID_IMPOSTOR_LAKE"); diff --git a/src/game_api/spawn_api.hpp b/src/game_api/spawn_api.hpp index d683b6ba5..58150e1f3 100644 --- a/src/game_api/spawn_api.hpp +++ b/src/game_api/spawn_api.hpp @@ -33,10 +33,12 @@ void spawn_backdoor_abs(float x, float y); int32_t spawn_apep(float x, float y, LAYER layer, bool right); -void spawn_tree(float x, float y, LAYER layer); -void spawn_tree(float x, float y, LAYER layer, uint16_t height); +int32_t spawn_tree(float x, float y, LAYER layer); +int32_t spawn_tree(float x, float y, LAYER layer, uint16_t height); int32_t spawn_mushroom(float x, float y, LAYER l); int32_t spawn_mushroom(float x, float y, LAYER l, uint16_t height); +int32_t spawn_unrolled_player_rope(float x, float y, LAYER layer, TEXTURE texture); +int32_t spawn_unrolled_player_rope(float x, float y, LAYER layer, TEXTURE texture, uint16_t max_length); Entity* spawn_impostor_lake(AABB aabb, LAYER layer, ENT_TYPE impostor_type, float top_threshold); void setup_impostor_lake(Entity* lake_imposter, AABB aabb, float top_threshold); diff --git a/src/game_api/state.cpp b/src/game_api/state.cpp index b76551b28..2175dc2e1 100644 --- a/src/game_api/state.cpp +++ b/src/game_api/state.cpp @@ -9,22 +9,23 @@ #include // for allocator, operator""sv, operator""s #include // for move -#include "entities_chars.hpp" // for Player -#include "entity.hpp" // for to_id, Entity, HookWithId, EntityDB -#include "game_manager.hpp" // for get_game_manager, GameManager, SaveR... -#include "items.hpp" // for Items, SelectPlayerSlot -#include "level_api.hpp" // for LevelGenSystem, LevelGenSystem::(ano... -#include "logger.h" // for DEBUG -#include "memory.hpp" // for write_mem_prot, read_u64, read_u8 -#include "movable.hpp" // for Movable -#include "movable_behavior.hpp" // for init_behavior_hooks -#include "render_api.hpp" // for init_render_api_hooks -#include "savedata.hpp" // for SaveData -#include "search.hpp" // for get_address -#include "spawn_api.hpp" // for init_spawn_hooks -#include "strings.hpp" // for strings_init -#include "thread_utils.hpp" // for OnHeapPointer -#include "virtual_table.hpp" // for get_virtual_function_address, VTABLE... +#include "entities_chars.hpp" // for Player +#include "entity.hpp" // for to_id, Entity, HookWithId, EntityDB +#include "entity_hooks_info.hpp" // for Player +#include "game_manager.hpp" // for get_game_manager, GameManager, SaveR... +#include "items.hpp" // for Items, SelectPlayerSlot +#include "level_api.hpp" // for LevelGenSystem, LevelGenSystem::(ano... +#include "logger.h" // for DEBUG +#include "memory.hpp" // for write_mem_prot, read_u64, read_u8 +#include "movable.hpp" // for Movable +#include "movable_behavior.hpp" // for init_behavior_hooks +#include "render_api.hpp" // for init_render_api_hooks +#include "savedata.hpp" // for SaveData +#include "search.hpp" // for get_address +#include "spawn_api.hpp" // for init_spawn_hooks +#include "strings.hpp" // for strings_init +#include "thread_utils.hpp" // for OnHeapPointer +#include "virtual_table.hpp" // for get_virtual_function_address, VTABLE... uint16_t StateMemory::get_correct_ushabti() // returns animation_frame of ushabti { @@ -69,6 +70,23 @@ inline bool& get_is_init() return is_init; } +inline bool& get_do_hooks() +{ + static bool do_hooks{true}; + return do_hooks; +} +void State::set_do_hooks(bool do_hooks) +{ + if (get_is_init()) + { + DEBUG("Too late to disable hooks..."); + } + else + { + get_do_hooks() = do_hooks; + } +} + void do_write_load_opt() { write_mem_prot(get_address("write_load_opt"), "\x90\x90"sv, true); @@ -231,13 +249,19 @@ State& State::get() } auto addr_location = get_address("state_location"); STATE = State{addr_location}; - STATE.ptr()->level_gen->init(); - init_spawn_hooks(); - init_behavior_hooks(); - init_render_api_hooks(); - hook_godmode_functions(); + + const bool do_hooks = get_do_hooks(); + if (do_hooks) + { + STATE.ptr()->level_gen->init(); + init_spawn_hooks(); + init_behavior_hooks(); + init_render_api_hooks(); + hook_godmode_functions(); + strings_init(); + } + get_is_init() = true; - strings_init(); } return STATE; } diff --git a/src/game_api/state.hpp b/src/game_api/state.hpp index 3c68dc429..49795e5e6 100644 --- a/src/game_api/state.hpp +++ b/src/game_api/state.hpp @@ -227,6 +227,8 @@ struct State { size_t location; + static void set_do_hooks(bool do_hooks); + static void set_write_load_opt(bool allow); static State& get(); diff --git a/src/game_api/util.hpp b/src/game_api/util.hpp index 578ad3d7b..a02b4ed2a 100644 --- a/src/game_api/util.hpp +++ b/src/game_api/util.hpp @@ -16,6 +16,14 @@ requires std::is_invocable_r_v struct OnScopeExit FunT Fun; }; +#define OL_CONCAT_IMPL(x, y) x##y +#define OL_CONCAT(x, y) OL_CONCAT_IMPL(x, y) +#define ON_SCOPE_EXIT(expr) \ + OnScopeExit OL_CONCAT(on_scope_exit_, __LINE__) \ + { \ + [&]() { expr; } \ + } + inline std::string_view trim(std::string_view str) { constexpr std::string_view white_spaces = " \t\r\n\v\f"; diff --git a/src/info_dump/main.cpp b/src/info_dump/main.cpp index 4d567056b..f4bdf7ba2 100644 --- a/src/info_dump/main.cpp +++ b/src/info_dump/main.cpp @@ -1035,7 +1035,7 @@ void to_json(float_json& j, const EntityDB& ent) {"search_flags", ent.search_flags}, {"width", ent.width}, {"height", ent.height}, - {"rect_collision", ent.rect_collision}, + {"rect_collision", ent.default_collision_info.rect}, {"friction", ent.friction}, {"elasticity", ent.elasticity}, {"weight", ent.weight}, diff --git a/src/shared/CMakeLists.txt b/src/shared/CMakeLists.txt index d06f802a5..c3acee9b1 100644 --- a/src/shared/CMakeLists.txt +++ b/src/shared/CMakeLists.txt @@ -28,4 +28,4 @@ target_precompile_headers(shared INTERFACE ) -target_sources(shared INTERFACE logger.h olfont.h) +target_sources(shared INTERFACE logger.h olfont.h tokenize.h) diff --git a/src/shared/tokenize.h b/src/shared/tokenize.h new file mode 100644 index 000000000..585698ce1 --- /dev/null +++ b/src/shared/tokenize.h @@ -0,0 +1,104 @@ +#pragma once + +#include // for size_t +#include // for string_view + +template +class Tokenize +{ + public: + Tokenize() = delete; + constexpr Tokenize(const Tokenize&) = default; + constexpr Tokenize(Tokenize&&) = default; + constexpr Tokenize& operator=(const Tokenize&) = default; + constexpr Tokenize& operator=(Tokenize&&) = default; + + explicit constexpr Tokenize(std::nullptr_t) + : m_Source{} + { + } + explicit constexpr Tokenize(const char* source) + : m_Source{source} + { + GetNext(); + } + explicit constexpr Tokenize(std::string_view source) + : m_Source{source} + { + GetNext(); + } + + constexpr bool operator==(const Tokenize& rhs) const = default; + constexpr bool operator!=(const Tokenize& rhs) const = default; + + constexpr auto begin() + { + return *this; + } + constexpr auto end() + { + return Tokenize{m_Source, end_tag_t{}}; + } + constexpr auto begin() const + { + return *this; + } + constexpr auto end() const + { + return Tokenize{m_Source, end_tag_t{}}; + } + constexpr auto cbegin() const + { + return *this; + } + constexpr auto cend() const + { + return Tokenize{m_Source, end_tag_t{}}; + } + + constexpr auto operator*() + { + return m_Source.substr(m_Position, m_Next - m_Position); + } + + constexpr decltype(auto) operator++() + { + if (!Advance()) + *this = end(); + return *this; + } + constexpr auto operator++(int) + { + auto copy = *this; + ++(*this); + return copy; + } + + private: + struct end_tag_t + { + }; + constexpr Tokenize(std::string_view source, end_tag_t) + : m_Source{source}, m_Position{source.size()}, m_Next{source.size()} + { + } + + constexpr void GetNext() + { + ++m_Next; + while (m_Next < m_Source.size() && m_Source[m_Next] != Delimiter) + { + ++m_Next; + } + } + constexpr bool Advance() + { + m_Position = m_Next + 1; + GetNext(); + return m_Next != m_Position; + } + + std::string_view m_Source; + size_t m_Position{0}; + size_t m_Next{0}; +}; diff --git a/src/spel2_dll/spel2.cpp b/src/spel2_dll/spel2.cpp index 0b65545e2..2f397aa35 100644 --- a/src/spel2_dll/spel2.cpp +++ b/src/spel2_dll/spel2.cpp @@ -18,6 +18,10 @@ SoundManager* g_SoundManager{nullptr}; SpelunkyConsole* g_Console{nullptr}; +void Spelunky_SetDoHooks(bool do_hooks) +{ + State::set_do_hooks(do_hooks); +} void Spelunky_SetWriteLoadOptimization(bool write_load_opt) { State::set_write_load_opt(write_load_opt); diff --git a/src/spel2_dll/spel2.h b/src/spel2_dll/spel2.h index cabe5da29..d7679114c 100644 --- a/src/spel2_dll/spel2.h +++ b/src/spel2_dll/spel2.h @@ -56,6 +56,7 @@ using Spelunky_GetImageFilePathFunc = bool (*)(const char* root_path, const char class SpelunkyScript; class SpelunkyConsole; +void Spelunky_SetDoHooks(bool do_hooks); void Spelunky_SetWriteLoadOptimization(bool write_load_opt); void Spelunky_RegisterApplicationVersion(const char* version);