diff --git a/docs/game_data/spel2.lua b/docs/game_data/spel2.lua
index 2b6b467d8..ffa664047 100644
--- a/docs/game_data/spel2.lua
+++ b/docs/game_data/spel2.lua
@@ -383,10 +383,11 @@ 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
----@param x number
----@param y number
----@return nil
-function spawn_player(player_slot, x, y) end
+---@param x number?
+---@param y number?
+---@param layer LAYER?
+---@return integer
+function spawn_player(player_slot, x, y, layer) end
---Spawn the PlayerGhost entity, it will not move and not be connected to any player, you can then use [steal_input](https://spelunky-fyi.github.io/overlunky/#steal_input) and send_input to controll it
---or change it's `player_inputs` to the `input` of real player so he can control it directly
---@param char_type ENT_TYPE
@@ -1134,14 +1135,15 @@ function update_liquid_collision_at(x, y, add) end
---@param disable boolean
---@return boolean
function disable_floor_embeds(disable) end
----Get the address for a pattern name
----@param address_name string
----@return integer
-function get_address(address_name) end
----Get the rva for a pattern name
+---Get the rva for a pattern name, used for debugging.
---@param address_name string
----@return integer
+---@return string
function get_rva(address_name) end
+---Get the rva for a vtable offset and index, used for debugging.
+---@param offset VTABLE_OFFSET
+---@param index integer
+---@return string
+function get_virtual_rva(offset, index) end
---Log to spelunky.log
---@param message string
---@return nil
@@ -1233,6 +1235,34 @@ function set_frametime_unfocused(frametime) end
---Get engine target frametime when game is unfocused (1/framerate, default 1/33).
---@return double?
function get_frametime_unfocused() end
+---Destroys all layers and all entities in the level. Usually a bad idea, unless you also call create_level and spawn the player back in.
+---@return nil
+function destroy_level() end
+---Destroys a layer and all entities in it.
+---@param layer integer
+---@return nil
+function destroy_layer(layer) end
+---Initializes an empty front and back layer that don't currently exist. Does nothing(?) if layers already exist.
+---@return nil
+function create_level() end
+---Initializes an empty layer that doesn't currently exist.
+---@param layer integer
+---@return nil
+function create_layer(layer) end
+---Setting to false disables all player logic in SCREEN.LEVEL, mainly the death screen from popping up if all players are dead or missing, but also shop camera zoom and some other small things.
+---@param enable boolean
+---@return nil
+function set_level_logic_enabled(enable) end
+---Converts INPUTS to (x, y, BUTTON)
+---@param inputs INPUTS
+---@return number, number, BUTTON
+function inputs_to_buttons(inputs) end
+---Converts (x, y, BUTTON) to INPUTS
+---@param x number
+---@param y number
+---@param buttons BUTTON
+---@return INPUTS
+function buttons_to_inputs(x, y, buttons) end
---@return boolean
function toast_visible() end
---@return boolean
@@ -2401,6 +2431,9 @@ function Entity:overlaps_with(other) end
---@field set_gravity fun(self, gravity: number): nil @Force the gravity for this entity. Will override anything set by special states like swimming too, unless you reset it. Default 1.0
---@field reset_gravity fun(self): nil @Remove the gravity hook and reset to defaults
---@field set_position fun(self, to_x: number, to_y: number): nil @Set the absolute position of an entity and offset all rendering related things accordingly to teleport without any interpolation or graphical glitches. If the camera is focused on the entity, it is also moved.
+ ---@field process_input fun(self): nil
+ ---@field cutscene CutsceneBehavior
+ ---@field clear_cutscene any @[](Movable&movable){deletemovable.cutscene_behavior
---@field get_base_behavior fun(self, state_id: integer): VanillaMovableBehavior @Gets a vanilla behavior from this movable, needs to be called before `clear_behaviors`
but the returned values are still valid after a call to `clear_behaviors`
---@field add_behavior fun(self, behavior: MovableBehavior): nil @Add a behavior to this movable, can be either a `VanillaMovableBehavior` or a
`CustomMovableBehavior`
---@field clear_behavior fun(self, behavior: MovableBehavior): nil @Clear a specific behavior of this movable, can be either a `VanillaMovableBehavior` or a
`CustomMovableBehavior`, a behavior with this behaviors `state_id` may be required to
run this movables statemachine without crashing, so add a new one if you are not sure
@@ -2479,6 +2512,8 @@ function Movable:generic_update_world(disable_gravity) end
---@return nil
function Movable:generic_update_world(move, sprint_factor, disable_gravity, on_rope) end
+---@class CutsceneBehavior
+
---@class PowerupCapable : Movable
---@field remove_powerup fun(self, powerup_type: ENT_TYPE): nil @Removes a currently applied powerup. Specify `ENT_TYPE.ITEM_POWERUP_xxx`, not `ENT_TYPE.ITEM_PICKUP_xxx`! Removing the Eggplant crown does not seem to undo the throwing of eggplants, the other powerups seem to work.
---@field give_powerup fun(self, powerup_type: ENT_TYPE): nil @Gives the player/monster the specified powerup. Specify `ENT_TYPE.ITEM_POWERUP_xxx`, not `ENT_TYPE.ITEM_PICKUP_xxx`! Giving true crown to a monster crashes the game.
diff --git a/docs/generate.py b/docs/generate.py
index 01bea7844..339d7945c 100644
--- a/docs/generate.py
+++ b/docs/generate.py
@@ -393,7 +393,7 @@ def print_lf(lf):
cat = "Message functions"
elif any(
subs in func["name"]
- for subs in ["get_address", "get_rva", "raise", "dump_network"]
+ for subs in ["get_rva", "get_virtual_rva", "raise", "dump_network"]
):
cat = "Debug functions"
elif any(subs in func["name"] for subs in ["_option"]):
@@ -474,7 +474,7 @@ def print_lf(lf):
cat = "Particle functions"
elif any(subs in func["name"] for subs in ["string", "_name"]):
cat = "String functions"
- elif any(subs in func["name"] for subs in ["udp_"]):
+ elif any(subs in func["name"] for subs in ["udp_", "http_"]):
cat = "Network functions"
elif any(subs in func["name"] for subs in ["illuminati"]):
cat = "Lighting functions"
@@ -630,7 +630,7 @@ def print_lf(lf):
"QuestsInfo",
"PlayerSlot",
"JournalProgressStickerSlot",
- "JournalProgressStainSlot"
+ "JournalProgressStainSlot",
]
):
cat = "State types"
diff --git a/docs/parse_source.py b/docs/parse_source.py
index dbd0cec7c..4eadd652b 100644
--- a/docs/parse_source.py
+++ b/docs/parse_source.py
@@ -125,6 +125,8 @@
cpp_type_exceptions = [
"Players",
+ "CutsceneBehavior",
+ "CustomCutsceneBehavior",
]
not_functions = [
"players",
@@ -892,7 +894,7 @@ def run_parse():
if not var:
continue
var = var.split(",")
- if(len(var) > 1):
+ if len(var) > 1:
vars.append({"name": var[0], "type": var[1]})
enums.append({"name": name, "vars": vars})
data = open(file, "r").read()
diff --git a/docs/src/includes/_enums.md b/docs/src/includes/_enums.md
index f49185efd..3ee27a974 100644
--- a/docs/src/includes/_enums.md
+++ b/docs/src/includes/_enums.md
@@ -783,7 +783,7 @@ Name | Data | Description
[SAVE](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.SAVE) | ON::SAVE | Params: [SaveContext](#SaveContext) save_ctx
Runs at the same times as [ON](#ON).[SCREEN](#SCREEN), but receives the save_ctx
[LOAD](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.LOAD) | ON::LOAD | Params: [LoadContext](#LoadContext) load_ctx
Runs as soon as your script is loaded, including reloads, then never again
[PRE_LOAD_LEVEL_FILES](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LOAD_LEVEL_FILES) | ON::PRE_LOAD_LEVEL_FILES | Params: [PreLoadLevelFilesContext](#PreLoadLevelFilesContext) load_level_ctx
Runs right before level files would be loaded
-[PRE_LEVEL_GENERATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LEVEL_GENERATION) | ON::PRE_LEVEL_GENERATION | Runs before any level generation, no entities should exist at this point
+[PRE_LEVEL_GENERATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LEVEL_GENERATION) | ON::PRE_LEVEL_GENERATION | Runs before any level generation, no entities should exist at this point. Does not work in all level-like screens. Return true to stop normal level generation.
[PRE_LOAD_SCREEN](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LOAD_SCREEN) | ON::PRE_LOAD_SCREEN | Runs right before loading a new screen based on screen_next. Return true from callback to block the screen from loading.
[POST_ROOM_GENERATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_ROOM_GENERATION) | ON::POST_ROOM_GENERATION | Params: [PostRoomGenerationContext](#PostRoomGenerationContext) room_gen_ctx
Runs right after all rooms are generated before entities are spawned
[POST_LEVEL_GENERATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LEVEL_GENERATION) | ON::POST_LEVEL_GENERATION | Runs right after level generation is done, before any entities are updated
@@ -818,6 +818,14 @@ Name | Data | Description
[PRE_UPDATE](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_UPDATE) | ON::PRE_UPDATE | Runs before the State is updated, runs always (menu, settings, camp, game, arena, online etc.) with the game engine, typically 60FPS
Return behavior: return true to stop futher PRE_UPDATE callbacks from executing and don't update the state (this will essentially freeze the game engine)
[POST_UPDATE](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_UPDATE) | ON::POST_UPDATE | Runs right after the State is updated, runs always (menu, settings, camp, game, arena, online etc.) with the game engine, typically 60FPS
[USER_DATA](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.USER_DATA) | ON::USER_DATA | Params: [Entity](#Entity) ent
Runs on all changes to [Entity](#Entity).user_data, including after loading saved user_data in the next level and transition. Also runs the first time user_data is set back to nil, but nil won't be saved to bother you on future levels.
+[PRE_LEVEL_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LEVEL_CREATION) | ON::PRE_LEVEL_CREATION | Runs right before the front layer is created. Runs in all screens that usually have entities, or when creating a layer manually.
+[POST_LEVEL_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LEVEL_CREATION) | ON::POST_LEVEL_CREATION | Runs right after the back layer has been created and you can start spawning entities in it. Runs in all screens that usually have entities, or when creating a layer manually.
+[PRE_LAYER_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LAYER_CREATION) | ON::PRE_LAYER_CREATION | Params: [LAYER](#LAYER) layer
Runs right before a layer is created. Runs in all screens that usually have entities, or when creating a layer manually.
+[POST_LAYER_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LAYER_CREATION) | ON::POST_LAYER_CREATION | Params: [LAYER](#LAYER) layer
Runs right after a layer has been created and you can start spawning entities in it. Runs in all screens that usually have entities, or when creating a layer manually.
+[PRE_LEVEL_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LEVEL_DESTRUCTION) | ON::PRE_LEVEL_DESTRUCTION | Runs right before the current level is unloaded and any entities destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+[POST_LEVEL_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LEVEL_DESTRUCTION) | ON::POST_LEVEL_DESTRUCTION | Runs right after the current level has been unloaded and all entities destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+[PRE_LAYER_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LAYER_DESTRUCTION) | ON::PRE_LAYER_DESTRUCTION | Params: [LAYER](#LAYER) layer
Runs right before a layer is unloaded and any entities there destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+[POST_LAYER_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LAYER_DESTRUCTION) | ON::POST_LAYER_DESTRUCTION | Params: [LAYER](#LAYER) layer
Runs right after a layer has been unloaded and any entities there destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
## PARTICLEEMITTER
diff --git a/docs/src/includes/_events.md b/docs/src/includes/_events.md
index 1e9d26acf..309a790b1 100644
--- a/docs/src/includes/_events.md
+++ b/docs/src/includes/_events.md
@@ -290,7 +290,7 @@ Params: [PreLoadLevelFilesContext](#PreLoadLevelFilesContext) load_level_ctx
Search script examples for [ON.PRE_LEVEL_GENERATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LEVEL_GENERATION)
-Runs before any level generation, no entities should exist at this point
+Runs before any level generation, no entities should exist at this point. Does not work in all level-like screens. Return true to stop normal level generation.
## ON.PRE_LOAD_SCREEN
@@ -549,3 +549,59 @@ Runs right after the State is updated, runs always (menu, settings, camp, game,
> Search script examples for [ON.USER_DATA](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.USER_DATA)
Params: [Entity](#Entity) ent
Runs on all changes to [Entity](#Entity).user_data, including after loading saved user_data in the next level and transition. Also runs the first time user_data is set back to nil, but nil won't be saved to bother you on future levels.
+
+## ON.PRE_LEVEL_CREATION
+
+
+> Search script examples for [ON.PRE_LEVEL_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LEVEL_CREATION)
+
+Runs right before the front layer is created. Runs in all screens that usually have entities, or when creating a layer manually.
+
+## ON.POST_LEVEL_CREATION
+
+
+> Search script examples for [ON.POST_LEVEL_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LEVEL_CREATION)
+
+Runs right after the back layer has been created and you can start spawning entities in it. Runs in all screens that usually have entities, or when creating a layer manually.
+
+## ON.PRE_LAYER_CREATION
+
+
+> Search script examples for [ON.PRE_LAYER_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LAYER_CREATION)
+
+Params: [LAYER](#LAYER) layer
Runs right before a layer is created. Runs in all screens that usually have entities, or when creating a layer manually.
+
+## ON.POST_LAYER_CREATION
+
+
+> Search script examples for [ON.POST_LAYER_CREATION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LAYER_CREATION)
+
+Params: [LAYER](#LAYER) layer
Runs right after a layer has been created and you can start spawning entities in it. Runs in all screens that usually have entities, or when creating a layer manually.
+
+## ON.PRE_LEVEL_DESTRUCTION
+
+
+> Search script examples for [ON.PRE_LEVEL_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LEVEL_DESTRUCTION)
+
+Runs right before the current level is unloaded and any entities destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+
+## ON.POST_LEVEL_DESTRUCTION
+
+
+> Search script examples for [ON.POST_LEVEL_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LEVEL_DESTRUCTION)
+
+Runs right after the current level has been unloaded and all entities destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+
+## ON.PRE_LAYER_DESTRUCTION
+
+
+> Search script examples for [ON.PRE_LAYER_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.PRE_LAYER_DESTRUCTION)
+
+Params: [LAYER](#LAYER) layer
Runs right before a layer is unloaded and any entities there destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+
+## ON.POST_LAYER_DESTRUCTION
+
+
+> Search script examples for [ON.POST_LAYER_DESTRUCTION](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=ON.POST_LAYER_DESTRUCTION)
+
+Params: [LAYER](#LAYER) layer
Runs right after a layer has been unloaded and any entities there destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
diff --git a/docs/src/includes/_globals.md b/docs/src/includes/_globals.md
index 782963d41..138da2c7c 100644
--- a/docs/src/includes/_globals.md
+++ b/docs/src/includes/_globals.md
@@ -351,23 +351,23 @@ If you set such a callback and then play the same sound yourself you have to wai
Hook the sendto and recvfrom functions and start dumping network data to terminal
-### get_address
+### get_rva
-> Search script examples for [get_address](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_address)
+> Search script examples for [get_rva](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_rva)
-#### size_t get_address(string_view address_name)
+#### string get_rva(string_view address_name)
-Get the address for a pattern name
+Get the rva for a pattern name, used for debugging.
-### get_rva
+### get_virtual_rva
-> Search script examples for [get_rva](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_rva)
+> Search script examples for [get_virtual_rva](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_virtual_rva)
-#### size_t get_rva(string_view address_name)
+#### string get_virtual_rva(VTABLE_OFFSET offset, int index)
-Get the rva for a pattern name
+Get the rva for a vtable offset and index, used for debugging.
### raise
@@ -1205,6 +1205,24 @@ Depending on the image size, this can take a moment, preferably don't create the
Create image from file, cropped to the geometry provided. Returns a tuple containing id, width and height.
Depending on the image size, this can take a moment, preferably don't create them dynamically, rather create all you need in global scope so it will load them as soon as the game starts
+### create_layer
+
+
+> Search script examples for [create_layer](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=create_layer)
+
+#### nil create_layer(int layer)
+
+Initializes an empty layer that doesn't currently exist.
+
+### create_level
+
+
+> Search script examples for [create_level](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=create_level)
+
+#### nil create_level()
+
+Initializes an empty front and back layer that don't currently exist. Does nothing(?) if layers already exist.
+
### destroy_grid
@@ -1217,6 +1235,24 @@ Depending on the image size, this can take a moment, preferably don't create the
Destroy the grid entity (by uid or position), and its item entities, removing them from the grid without dropping particles or gold.
Will also destroy monsters or items that are standing on a linked activefloor or chain, though excludes [MASK](#MASK).PLAYER to prevent crashes
+### destroy_layer
+
+
+> Search script examples for [destroy_layer](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=destroy_layer)
+
+#### nil destroy_layer(int layer)
+
+Destroys a layer and all entities in it.
+
+### destroy_level
+
+
+> Search script examples for [destroy_level](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=destroy_level)
+
+#### nil destroy_level()
+
+Destroys all layers and all entities in the level. Usually a bad idea, unless you also call create_level and spawn the player back in.
+
### disable_floor_embeds
@@ -1395,25 +1431,6 @@ Grow pole from `GROWABLE_CLIMBING_POLE` entities in a level, `area` default is w
Grow vines from `GROWABLE_VINE` and `VINE_TREE_TOP` entities in a level, `area` default is whole level, `destroy_broken` default is false
-### http_get
-
-
-> Search script examples for [http_get](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=http_get)
-
-#### optional<string> http_get(string url)
-
-Send a synchronous HTTP GET request and return response as a string or nil on an error
-
-### http_get_async
-
-
-> Search script examples for [http_get_async](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=http_get_async)
-
-#### HttpRequest http_get_async(string url, function on_data)
-
-Send an asynchronous HTTP GET request and run the callback when done. If there is an error, response will be nil and vice versa.
-The callback signature is nil on_data(string response, string error)
-
### import
@@ -1428,6 +1445,15 @@ Load another script by id "author/name" and import its `exports` table. Returns:
- `false` if the script was not found but optional is set to true
- an error if the script was not found and the optional argument was not set
+### inputs_to_buttons
+
+
+> Search script examples for [inputs_to_buttons](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=inputs_to_buttons)
+
+#### tuple<float, float, [BUTTON](#BUTTON)> inputs_to_buttons([INPUTS](#INPUTS) inputs)
+
+Converts [INPUTS](#INPUTS) to (x, y, BUTTON)
+
### intersection
@@ -1655,6 +1681,15 @@ Enables or disables the journal
Set the value for the specified config
+### set_level_logic_enabled
+
+
+> Search script examples for [set_level_logic_enabled](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_level_logic_enabled)
+
+#### nil set_level_logic_enabled(bool enable)
+
+Setting to false disables all player logic in [SCREEN](#SCREEN).LEVEL, mainly the death screen from popping up if all players are dead or missing, but also shop camera zoom and some other small things.
+
### set_mask
@@ -1831,6 +1866,15 @@ Warp to a level immediately.
## Input functions
+### buttons_to_inputs
+
+
+> Search script examples for [buttons_to_inputs](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=buttons_to_inputs)
+
+#### [INPUTS](#INPUTS) buttons_to_inputs(float x, float y, [BUTTON](#BUTTON) buttons)
+
+Converts (x, y, BUTTON) to [INPUTS](#INPUTS)
+
### get_io
@@ -2066,6 +2110,25 @@ be returned instead.
## Network functions
+### http_get
+
+
+> Search script examples for [http_get](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=http_get)
+
+#### optional<string> http_get(string url)
+
+Send a synchronous HTTP GET request and return response as a string or nil on an error
+
+### http_get_async
+
+
+> Search script examples for [http_get_async](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=http_get_async)
+
+#### HttpRequest http_get_async(string url, function on_data)
+
+Send an asynchronous HTTP GET request and run the callback when done. If there is an error, response will be nil and vice versa.
+The callback signature is nil on_data(string response, string error)
+
### udp_listen
@@ -2995,7 +3058,7 @@ Short for [spawn_entity_over](#spawn_entity_over)
> Search script examples for [spawn_player](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=spawn_player)
-#### nil spawn_player(int player_slot, float x, float y)
+#### int spawn_player(int player_slot, optional x, optional y, optional<[LAYER](#LAYER)> layer)
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](#Inventory) `health` to above 0, and `time_of_death` to 0 and call this function, the ghost entity will be removed automatically
diff --git a/docs/src/includes/_types.md b/docs/src/includes/_types.md
index 6637f38fc..b56e62e7b 100644
--- a/docs/src/includes/_types.md
+++ b/docs/src/includes/_types.md
@@ -640,6 +640,12 @@ tuple<int, int, int, int> | [get_rgba()](https://github.com/spelunky-fyi/o
[uColor](#Aliases) | [get_ucolor()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_ucolor) | Returns the `uColor` used in `GuiDrawContext` drawing functions
[Color](#Color)& | [set_ucolor(const uColor color)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_ucolor) | Changes color based on given [uColor](#Aliases)
+### CutsceneBehavior
+
+
+Type | Name | Description
+---- | ---- | -----------
+
### Hud
@@ -6648,6 +6654,9 @@ int | [get_behavior()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=
nil | [set_gravity(float gravity)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_gravity) | Force the gravity for this entity. Will override anything set by special states like swimming too, unless you reset it. Default 1.0
nil | [reset_gravity()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=reset_gravity) | Remove the gravity hook and reset to defaults
nil | [set_position(float to_x, float to_y)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=set_position) | Set the absolute position of an entity and offset all rendering related things accordingly to teleport without any interpolation or graphical glitches. If the camera is focused on the entity, it is also moved.
+nil | [process_input()](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=process_input) |
+[CutsceneBehavior](#CutsceneBehavior) | [cutscene](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=cutscene) |
+ | [clear_cutscene](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=clear_cutscene) |
[VanillaMovableBehavior](#VanillaMovableBehavior) | [get_base_behavior(int state_id)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=get_base_behavior) | Gets a vanilla behavior from this movable, needs to be called before `clear_behaviors`
but the returned values are still valid after a call to `clear_behaviors`
nil | [add_behavior(MovableBehavior behavior)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=add_behavior) | Add a behavior to this movable, can be either a `VanillaMovableBehavior` or a
`CustomMovableBehavior`
nil | [clear_behavior(MovableBehavior behavior)](https://github.com/spelunky-fyi/overlunky/search?l=Lua&q=clear_behavior) | Clear a specific behavior of this movable, can be either a `VanillaMovableBehavior` or a
`CustomMovableBehavior`, a behavior with this behaviors `state_id` may be required to
run this movables statemachine without crashing, so add a new one if you are not sure
diff --git a/src/game_api/flags.hpp b/src/game_api/flags.hpp
index 79aaa2c53..60cbf73e2 100644
--- a/src/game_api/flags.hpp
+++ b/src/game_api/flags.hpp
@@ -756,3 +756,54 @@ const char* pause_types[]{
"32: Ankh (smooth camera, janky audio)",
"Freeze on PRE_UPDATE", // this is not a real state.pause flag, it's only used by ui.cpp for magic
};
+
+const char* levelgen_flags[]{
+ "1: Should generate path",
+ "2: Can spawn vault",
+ "3: Can spawn shops",
+ "4: Can have outpost?",
+ "5: Should spawn hard floor decorations",
+ "6: Apply ambient occlusion",
+ "7: Should spawn behind-floor and below-floorstyled decorations",
+ "8: unknown",
+};
+
+const char* levelgen_flags2[]{
+ "1: Spawns background decorations on ground (ceiling if false)",
+ "2: Spawns fake ladder/chain midbg?",
+ "3: Spawn entrance door background (Ignored in 7-1 to 7-2 transition)",
+ "4: Procedural backlayer door midbg indicator related",
+ "5: Spawn backlayer border/background",
+ "6: Should spawn procedural backlayers",
+ "7: Should spawn backlayer torches",
+ "8: Has ghost",
+};
+
+const char* levelgen_flags3[]{
+ "1: Can spawn angry NPCs",
+ "2: Can echo",
+ "3: Can spawn Dead are Restless",
+ "4: Can spawn procedural skeletons",
+ "5: Can have quests?",
+ "6: Can spawn player coffins",
+ "7: unknown",
+ "8: unknown",
+};
+
+const char* level_chances[]{
+ "backroom",
+ "backroom interconnect",
+ "backroom hidden door",
+ "backroom hidden cache",
+ "mount",
+ "altar",
+ "idol",
+ "floor side spread",
+ "floor bottom spread",
+ "background",
+ "ground background",
+ "bigroom",
+ "wideroom",
+ "tallroom",
+ "rewardroom",
+};
diff --git a/src/game_api/level_api.cpp b/src/game_api/level_api.cpp
index 226e3e854..6f2af4b67 100644
--- a/src/game_api/level_api.cpp
+++ b/src/game_api/level_api.cpp
@@ -826,7 +826,8 @@ void level_gen(LevelGenSystem* level_gen_sys, float param_2, size_t param_3)
g_CustomShopTypes[0] = {};
g_CustomShopTypes[1] = {};
- pre_level_generation();
+ if (pre_level_generation())
+ return;
g_level_gen_trampoline(level_gen_sys, param_2, param_3);
post_level_generation();
@@ -846,6 +847,65 @@ void level_gen(LevelGenSystem* level_gen_sys, float param_2, size_t param_3)
g_levels_to_load.clear();
}
+using TransGenFun = void(ThemeInfo*);
+TransGenFun* g_trans_gen_trampoline{nullptr};
+TransGenFun* g_trans_gen2_trampoline{nullptr};
+using TransGenFun3 = void(size_t, size_t, ThemeInfo*);
+TransGenFun3* g_trans_gen3_trampoline{nullptr};
+using TransGenFun4 = void(size_t, size_t, size_t, size_t, size_t, size_t);
+TransGenFun4* g_trans_gen4_trampoline{nullptr};
+// generic transition hook
+void trans_gen(ThemeInfo* theme)
+{
+ push_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL);
+ OnScopeExit pop{[]
+ { pop_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL); }};
+
+ if (pre_level_generation())
+ return;
+ g_trans_gen_trampoline(theme);
+ post_level_generation();
+}
+// cosmic transition hook
+void trans_gen2(ThemeInfo* theme)
+{
+ push_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL);
+ OnScopeExit pop{[]
+ { pop_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL); }};
+
+ if (pre_level_generation())
+ return;
+ g_trans_gen2_trampoline(theme);
+ post_level_generation();
+}
+// cog-duat transition hook
+void trans_gen3(size_t a, size_t b, ThemeInfo* theme)
+{
+ push_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL);
+ OnScopeExit pop{[]
+ { pop_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL); }};
+
+ auto state = State::get().ptr();
+ // trampoline will call the generic trans_gen if not going to duat
+ if (state->theme_next == 12 && pre_level_generation())
+ return;
+ g_trans_gen3_trampoline(a, b, theme);
+ if (state->theme_next == 12)
+ post_level_generation();
+}
+// olmecship transition hook
+void trans_gen4(size_t a, size_t b, size_t c, size_t d, size_t e, size_t f)
+{
+ push_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL);
+ OnScopeExit pop{[]
+ { pop_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_GENERAL); }};
+
+ if (pre_level_generation())
+ return;
+ g_trans_gen4_trampoline(a, b, c, d, e, f);
+ post_level_generation();
+}
+
using LoadScreenFun = void(StateMemory*, size_t, size_t);
LoadScreenFun* g_load_screen_trampoline{nullptr};
void load_screen(StateMemory* state, size_t param_2, size_t param_3)
@@ -856,6 +916,33 @@ void load_screen(StateMemory* state, size_t param_2, size_t param_3)
post_load_screen();
}
+using UnloadLayerFun = void(Layer*);
+UnloadLayerFun* g_unload_layer_trampoline{nullptr};
+void unload_layer(Layer* layer)
+{
+ if (!layer->is_back_layer && pre_unload_level())
+ return;
+ if (pre_unload_layer((LAYER)layer->is_back_layer))
+ return;
+ g_unload_layer_trampoline(layer);
+ post_unload_layer((LAYER)layer->is_back_layer);
+ if (layer->is_back_layer)
+ post_unload_level();
+}
+
+using InitLayerFun = void(Layer*);
+InitLayerFun* g_init_layer_trampoline{nullptr};
+void load_layer(Layer* layer)
+{
+ if (!layer->is_back_layer)
+ pre_init_level();
+ pre_init_layer((LAYER)layer->is_back_layer);
+ g_init_layer_trampoline(layer);
+ post_init_layer((LAYER)layer->is_back_layer);
+ if (layer->is_back_layer)
+ post_init_level();
+}
+
using HandleTileCodeFun = void(LevelGenSystem*, std::uint32_t, std::uint64_t, float, float, std::uint8_t);
HandleTileCodeFun* g_handle_tile_code_trampoline{nullptr};
void handle_tile_code(LevelGenSystem* self, std::uint32_t tile_code, std::uint16_t room_template, float x, float y, std::uint8_t layer)
@@ -1470,6 +1557,12 @@ void LevelGenData::init()
g_spawn_room_from_tile_codes_trampoline = (SpawnRoomFromTileCodes*)get_address("level_gen_spawn_room_from_tile_codes"sv);
g_load_screen_trampoline = (LoadScreenFun*)get_address("load_screen_func"sv);
+ g_unload_layer_trampoline = (UnloadLayerFun*)get_address("unload_layer"sv);
+ g_init_layer_trampoline = (InitLayerFun*)get_address("init_layer"sv);
+ g_trans_gen_trampoline = (TransGenFun*)get_address("spawn_transition"sv);
+ g_trans_gen2_trampoline = (TransGenFun*)get_address("spawn_transition_cosmic"sv);
+ g_trans_gen3_trampoline = (TransGenFun3*)get_address("spawn_transition_duat"sv);
+ g_trans_gen4_trampoline = (TransGenFun4*)get_address("spawn_transition_olmecship"sv);
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
@@ -1485,6 +1578,12 @@ void LevelGenData::init()
DetourAttach((void**)&g_spawn_room_from_tile_codes_trampoline, spawn_room_from_tile_codes);
DetourAttach((void**)&g_load_screen_trampoline, load_screen);
+ DetourAttach((void**)&g_unload_layer_trampoline, unload_layer);
+ DetourAttach((void**)&g_init_layer_trampoline, load_layer);
+ DetourAttach((void**)&g_trans_gen_trampoline, trans_gen);
+ DetourAttach((void**)&g_trans_gen2_trampoline, trans_gen2);
+ DetourAttach((void**)&g_trans_gen3_trampoline, trans_gen3);
+ DetourAttach((void**)&g_trans_gen4_trampoline, trans_gen4);
const LONG error = DetourTransactionCommit();
if (error != NO_ERROR)
@@ -1797,6 +1896,12 @@ void LevelGenSystem::populate_level_hook(ThemeInfo* self, uint64_t param_2, uint
original(self, param_2, param_3, param_4);
}
+void LevelGenSystem::populate_transition_hook(ThemeInfo* self, PopulateTransitionFun* original)
+{
+ pre_level_generation();
+ original(self);
+ post_level_generation();
+}
void LevelGenSystem::do_procedural_spawn_hook(ThemeInfo* self, SpawnInfo* spawn_info, DoProceduralSpawnFun* original)
{
push_spawn_type_flags(SPAWN_TYPE_LEVEL_GEN_PROCEDURAL);
@@ -1949,6 +2054,15 @@ LevelChanceDef& get_or_emplace_level_chance(game_unordered_mapvalue.second;
}
+std::optional LevelGenSystem::get_procedural_spawn_chance_name(uint32_t chance_id)
+{
+ if (g_monster_chance_id_to_name.contains(chance_id))
+ return g_monster_chance_id_to_name[chance_id];
+ if (g_trap_chance_id_to_name.contains(chance_id))
+ return g_trap_chance_id_to_name[chance_id];
+ return std::nullopt;
+}
+
uint32_t LevelGenSystem::get_procedural_spawn_chance(uint32_t chance_id)
{
if (g_monster_chance_id_to_name.contains(chance_id))
@@ -1957,14 +2071,18 @@ uint32_t LevelGenSystem::get_procedural_spawn_chance(uint32_t chance_id)
if (!this_chances.chances.empty())
{
auto* state = State::get().ptr();
- if (this_chances.chances.size() >= state->level)
+ if (this_chances.chances.size() >= state->level && state->level > 0)
{
- return this_chances.chances[state->level];
+ return this_chances.chances[state->level - 1];
}
- else
+ else if (this_chances.chances.size() == 1)
{
return this_chances.chances[0];
}
+ else
+ {
+ return 0;
+ }
}
}
@@ -1974,14 +2092,18 @@ uint32_t LevelGenSystem::get_procedural_spawn_chance(uint32_t chance_id)
if (!this_chances.chances.empty())
{
auto* state = State::get().ptr();
- if (this_chances.chances.size() >= state->level)
+ if (this_chances.chances.size() >= state->level && state->level > 0)
{
- return this_chances.chances[state->level];
+ return this_chances.chances[state->level - 1];
}
- else
+ else if (this_chances.chances.size() == 1)
{
return this_chances.chances[0];
}
+ else
+ {
+ return 0;
+ }
}
}
diff --git a/src/game_api/level_api.hpp b/src/game_api/level_api.hpp
index cd6d900b3..65988f426 100644
--- a/src/game_api/level_api.hpp
+++ b/src/game_api/level_api.hpp
@@ -423,12 +423,17 @@ struct LevelGenSystem
{
hook_impl.template hook(theme, &populate_level_hook);
hook_impl.template hook(theme, &do_procedural_spawn_hook);
+ // this didn't work right
+ // hook_impl.template hook(theme, &populate_transition_hook);
}
}
using PopulateLevelFun = void(ThemeInfo*, uint64_t, uint64_t, uint64_t);
static void populate_level_hook(ThemeInfo*, uint64_t, uint64_t, uint64_t, PopulateLevelFun*);
+ using PopulateTransitionFun = void(ThemeInfo*);
+ static void populate_transition_hook(ThemeInfo*, PopulateTransitionFun*);
+
using DoProceduralSpawnFun = void(ThemeInfo*, SpawnInfo*);
static void do_procedural_spawn_hook(ThemeInfo*, SpawnInfo*, DoProceduralSpawnFun*);
@@ -531,7 +536,7 @@ struct LevelGenSystem
bool set_shop_type(uint32_t x, uint32_t y, uint8_t l, ShopType shop_type);
std::string_view get_room_template_name(uint16_t room_template);
-
+ std::optional get_procedural_spawn_chance_name(uint32_t chance_id);
uint32_t get_procedural_spawn_chance(uint32_t chance_id);
bool set_procedural_spawn_chance(uint32_t chance_id, uint32_t inverse_chance);
diff --git a/src/game_api/movable.hpp b/src/game_api/movable.hpp
index 06b8d257a..be63d573b 100644
--- a/src/game_api/movable.hpp
+++ b/src/game_api/movable.hpp
@@ -14,7 +14,7 @@ class CutsceneBehavior
{
public:
virtual ~CutsceneBehavior(){};
- virtual void update() = 0;
+ virtual void update(Movable* e) = 0;
// no more virtuals, it's possible that different sub classes have some extra variables as well
};
@@ -24,7 +24,7 @@ class Movable : public Entity
custom_map behaviors_map;
custom_set behaviors;
MovableBehavior* current_behavior;
- CutsceneBehavior* ic8;
+ CutsceneBehavior* cutscene_behavior;
union
{
/// {movex, movey}
diff --git a/src/game_api/rpc.cpp b/src/game_api/rpc.cpp
index 2a8c2fa96..bd3283623 100644
--- a/src/game_api/rpc.cpp
+++ b/src/game_api/rpc.cpp
@@ -2142,3 +2142,69 @@ std::optional get_frametime_inactive()
return memory_read(offset);
return std::nullopt;
}
+
+void destroy_layer(uint8_t layer)
+{
+ static size_t offset = 0;
+ if (offset == 0)
+ {
+ offset = get_address("unload_layer");
+ }
+ if (offset != 0)
+ {
+ auto state = State::get().ptr();
+ for (auto i = 0; i < MAX_PLAYERS; ++i)
+ {
+ if (state->items->players[i] && state->items->players[i]->layer == layer)
+ state->items->players[i] = nullptr;
+ }
+ auto* layer_ptr = State::get().layer(layer);
+ typedef void destroy_func(Layer*);
+ static destroy_func* df = (destroy_func*)(offset);
+ df(layer_ptr);
+ }
+}
+
+void destroy_level()
+{
+ destroy_layer(0);
+ destroy_layer(1);
+}
+
+void create_layer(uint8_t layer)
+{
+ static size_t offset = 0;
+ if (offset == 0)
+ {
+ offset = get_address("init_layer");
+ }
+ if (offset != 0)
+ {
+ auto* layer_ptr = State::get().layer(layer);
+ typedef void init_func(Layer*);
+ static init_func* ilf = (init_func*)(offset);
+ ilf(layer_ptr);
+ }
+}
+
+void create_level()
+{
+ create_layer(0);
+ create_layer(1);
+}
+
+void set_death_enabled(bool enable)
+{
+ static size_t offset = 0;
+ if (offset == 0)
+ {
+ offset = get_address("dead_players");
+ }
+ if (offset != 0)
+ {
+ if (!enable)
+ write_mem_recoverable("death_disable", offset, "\xC3\x90"sv, true);
+ else
+ recover_mem("death_disable");
+ }
+}
diff --git a/src/game_api/rpc.hpp b/src/game_api/rpc.hpp
index fb2ab06ca..92d9690d5 100644
--- a/src/game_api/rpc.hpp
+++ b/src/game_api/rpc.hpp
@@ -150,3 +150,8 @@ void set_frametime(std::optional frametime);
std::optional get_frametime();
void set_frametime_inactive(std::optional frametime);
std::optional get_frametime_inactive();
+void destroy_layer(uint8_t layer);
+void destroy_level();
+void create_layer(uint8_t layer);
+void create_level();
+void set_death_enabled(bool enable);
diff --git a/src/game_api/script/events.cpp b/src/game_api/script/events.cpp
index ff474b3a5..13bc37131 100644
--- a/src/game_api/script/events.cpp
+++ b/src/game_api/script/events.cpp
@@ -16,6 +16,8 @@
class JournalPage;
struct AABB;
+auto g_level_loaded = false;
+
void pre_load_level_files()
{
LuaBackend::for_each_backend(
@@ -25,14 +27,16 @@ void pre_load_level_files()
return true;
});
}
-void pre_level_generation()
+bool pre_level_generation()
{
+ bool block{false};
LuaBackend::for_each_backend(
[&](LuaBackend::LockedBackend backend)
{
- backend->pre_level_generation();
- return true;
+ block = backend->pre_level_generation();
+ return !block;
});
+ return block;
}
bool pre_load_screen()
{
@@ -72,6 +76,55 @@ bool pre_load_screen()
return block;
}
+bool pre_unload_level()
+{
+ bool block{false};
+ LuaBackend::for_each_backend(
+ [&](LuaBackend::LockedBackend backend)
+ {
+ block = backend->pre_unload_level();
+ return !block;
+ });
+ if (!block)
+ g_level_loaded = false;
+ return block;
+}
+bool pre_init_level()
+{
+ bool block{false};
+ LuaBackend::for_each_backend(
+ [&](LuaBackend::LockedBackend backend)
+ {
+ block = backend->pre_init_level();
+ return !block;
+ });
+ if (!block)
+ g_level_loaded = true;
+ return block;
+}
+bool pre_unload_layer(LAYER layer)
+{
+ bool block{false};
+ LuaBackend::for_each_backend(
+ [&](LuaBackend::LockedBackend backend)
+ {
+ block = backend->pre_unload_layer(layer);
+ return !block;
+ });
+ return block;
+}
+bool pre_init_layer(LAYER layer)
+{
+ bool block{false};
+ LuaBackend::for_each_backend(
+ [&](LuaBackend::LockedBackend backend)
+ {
+ block = backend->pre_init_layer(layer);
+ return !block;
+ });
+ return block;
+}
+
void post_room_generation()
{
LuaBackend::for_each_backend(
@@ -90,6 +143,24 @@ void post_level_generation()
return true;
});
}
+void post_init_layer(LAYER layer)
+{
+ LuaBackend::for_each_backend(
+ [&](LuaBackend::LockedBackend backend)
+ {
+ backend->post_init_layer(layer);
+ return true;
+ });
+}
+void post_init_level()
+{
+ LuaBackend::for_each_backend(
+ [&](LuaBackend::LockedBackend backend)
+ {
+ backend->post_init_level();
+ return true;
+ });
+}
void post_load_screen()
{
LuaBackend::for_each_backend(
@@ -99,6 +170,24 @@ void post_load_screen()
return true;
});
}
+void post_unload_level()
+{
+ LuaBackend::for_each_backend(
+ [&](LuaBackend::LockedBackend backend)
+ {
+ backend->post_unload_level();
+ return true;
+ });
+}
+void post_unload_layer(LAYER layer)
+{
+ LuaBackend::for_each_backend(
+ [&](LuaBackend::LockedBackend backend)
+ {
+ backend->post_unload_layer(layer);
+ return true;
+ });
+}
void on_death_message(STRINGID stringid)
{
LuaBackend::for_each_backend(
diff --git a/src/game_api/script/events.hpp b/src/game_api/script/events.hpp
index ad3e06581..fff659bcd 100644
--- a/src/game_api/script/events.hpp
+++ b/src/game_api/script/events.hpp
@@ -16,11 +16,21 @@ struct HudData;
struct Hud;
void pre_load_level_files();
-void pre_level_generation();
+bool pre_level_generation();
bool pre_load_screen();
+bool pre_init_level();
+bool pre_init_layer(LAYER layer);
+bool pre_unload_level();
+bool pre_unload_layer(LAYER layer);
+
void post_room_generation();
void post_level_generation();
void post_load_screen();
+void post_init_level();
+void post_init_layer(LAYER layer);
+void post_unload_level();
+void post_unload_layer(LAYER layer);
+
void on_death_message(STRINGID stringid);
std::optional pre_get_feat(FEAT feat);
bool pre_set_feat(FEAT feat);
diff --git a/src/game_api/script/lua_backend.cpp b/src/game_api/script/lua_backend.cpp
index f64752a9d..3aee0fa04 100644
--- a/src/game_api/script/lua_backend.cpp
+++ b/src/game_api/script/lua_backend.cpp
@@ -751,10 +751,10 @@ void LuaBackend::pre_load_level_files()
}
}
}
-void LuaBackend::pre_level_generation()
+bool LuaBackend::pre_level_generation()
{
if (!get_enabled())
- return;
+ return false;
auto now = get_frame_count();
@@ -766,11 +766,62 @@ void LuaBackend::pre_level_generation()
if (callback.screen == ON::PRE_LEVEL_GENERATION)
{
set_current_callback(-1, id, CallbackType::Normal);
- handle_function(this, callback.func);
+ auto return_value = handle_function(this, callback.func).value_or(false);
+ clear_current_callback();
+ callback.lastRan = now;
+ if (return_value)
+ return return_value;
+ }
+ }
+ return false;
+}
+bool LuaBackend::pre_init_level()
+{
+ if (!get_enabled())
+ return false;
+
+ auto now = get_frame_count();
+
+ for (auto& [id, callback] : callbacks)
+ {
+ if (is_callback_cleared(id))
+ continue;
+
+ if (callback.screen == ON::PRE_LEVEL_CREATION)
+ {
+ set_current_callback(-1, id, CallbackType::Normal);
+ auto return_value = handle_function(this, callback.func).value_or(false);
+ clear_current_callback();
+ callback.lastRan = now;
+ if (return_value)
+ return return_value;
+ }
+ }
+ return false;
+}
+bool LuaBackend::pre_init_layer(LAYER layer)
+{
+ if (!get_enabled())
+ return false;
+
+ auto now = get_frame_count();
+
+ for (auto& [id, callback] : callbacks)
+ {
+ if (is_callback_cleared(id))
+ continue;
+
+ if (callback.screen == ON::PRE_LAYER_CREATION)
+ {
+ set_current_callback(-1, id, CallbackType::Normal);
+ auto return_value = handle_function(this, callback.func, layer).value_or(false);
clear_current_callback();
callback.lastRan = now;
+ if (return_value)
+ return return_value;
}
}
+ return false;
}
bool LuaBackend::pre_load_screen()
{
@@ -865,6 +916,58 @@ bool LuaBackend::pre_load_screen()
return false;
}
+
+bool LuaBackend::pre_unload_level()
+{
+ if (!get_enabled())
+ return false;
+
+ auto now = get_frame_count();
+
+ for (auto& [id, callback] : callbacks)
+ {
+ if (is_callback_cleared(id))
+ continue;
+
+ if (callback.screen == ON::PRE_LEVEL_DESTRUCTION)
+ {
+ set_current_callback(-1, id, CallbackType::Normal);
+ auto return_value = handle_function(this, callback.func).value_or(false);
+ clear_current_callback();
+ callback.lastRan = now;
+ if (return_value)
+ return return_value;
+ }
+ }
+
+ return false;
+}
+bool LuaBackend::pre_unload_layer(LAYER layer)
+{
+ if (!get_enabled())
+ return false;
+
+ auto now = get_frame_count();
+
+ for (auto& [id, callback] : callbacks)
+ {
+ if (is_callback_cleared(id))
+ continue;
+
+ if (callback.screen == ON::PRE_LAYER_DESTRUCTION)
+ {
+ set_current_callback(-1, id, CallbackType::Normal);
+ auto return_value = handle_function(this, callback.func, layer).value_or(false);
+ clear_current_callback();
+ callback.lastRan = now;
+ if (return_value)
+ return return_value;
+ }
+ }
+
+ return false;
+}
+
void LuaBackend::post_room_generation()
{
if (!get_enabled())
@@ -956,6 +1059,48 @@ void LuaBackend::post_level_generation()
}
}
}
+void LuaBackend::post_init_level()
+{
+ if (!get_enabled())
+ return;
+
+ auto now = get_frame_count();
+
+ for (auto& [id, callback] : callbacks)
+ {
+ if (is_callback_cleared(id))
+ continue;
+
+ if (callback.screen == ON::POST_LEVEL_CREATION)
+ {
+ set_current_callback(-1, id, CallbackType::Normal);
+ handle_function(this, callback.func);
+ clear_current_callback();
+ callback.lastRan = now;
+ }
+ }
+}
+void LuaBackend::post_init_layer(LAYER layer)
+{
+ if (!get_enabled())
+ return;
+
+ auto now = get_frame_count();
+
+ for (auto& [id, callback] : callbacks)
+ {
+ if (is_callback_cleared(id))
+ continue;
+
+ if (callback.screen == ON::POST_LAYER_CREATION)
+ {
+ set_current_callback(-1, id, CallbackType::Normal);
+ handle_function(this, callback.func, layer);
+ clear_current_callback();
+ callback.lastRan = now;
+ }
+ }
+}
void LuaBackend::post_load_screen()
{
if (!get_enabled())
@@ -983,6 +1128,49 @@ void LuaBackend::post_load_screen()
}
}
}
+void LuaBackend::post_unload_level()
+{
+ if (!get_enabled())
+ return;
+
+ auto now = get_frame_count();
+
+ for (auto& [id, callback] : callbacks)
+ {
+ if (is_callback_cleared(id))
+ continue;
+
+ if (callback.screen == ON::POST_LEVEL_DESTRUCTION)
+ {
+ set_current_callback(-1, id, CallbackType::Normal);
+ handle_function(this, callback.func);
+ clear_current_callback();
+ callback.lastRan = now;
+ }
+ }
+}
+void LuaBackend::post_unload_layer(LAYER layer)
+{
+ if (!get_enabled())
+ return;
+
+ auto now = get_frame_count();
+
+ for (auto& [id, callback] : callbacks)
+ {
+ if (is_callback_cleared(id))
+ continue;
+
+ if (callback.screen == ON::POST_LAYER_DESTRUCTION)
+ {
+ set_current_callback(-1, id, CallbackType::Normal);
+ handle_function(this, callback.func, layer);
+ clear_current_callback();
+ callback.lastRan = now;
+ }
+ }
+}
+
void LuaBackend::on_death_message(STRINGID stringid)
{
if (!get_enabled())
diff --git a/src/game_api/script/lua_backend.hpp b/src/game_api/script/lua_backend.hpp
index 15e5ed89e..835cd8168 100644
--- a/src/game_api/script/lua_backend.hpp
+++ b/src/game_api/script/lua_backend.hpp
@@ -119,7 +119,15 @@ enum class ON
PRE_SET_FEAT,
PRE_UPDATE,
POST_UPDATE,
- USER_DATA
+ USER_DATA,
+ PRE_LEVEL_CREATION,
+ POST_LEVEL_CREATION,
+ PRE_LAYER_CREATION,
+ POST_LAYER_CREATION,
+ PRE_LEVEL_DESTRUCTION,
+ POST_LEVEL_DESTRUCTION,
+ PRE_LAYER_DESTRUCTION,
+ POST_LAYER_DESTRUCTION,
};
struct IntOption
@@ -360,11 +368,21 @@ class LuaBackend
void post_tile_code(std::string_view tile_code, float x, float y, int layer, uint16_t room_template);
void pre_load_level_files();
- void pre_level_generation();
+ bool pre_level_generation();
bool pre_load_screen();
+ bool pre_init_level();
+ bool pre_init_layer(LAYER layer);
+ bool pre_unload_level();
+ bool pre_unload_layer(LAYER layer);
+
void post_room_generation();
void post_level_generation();
void post_load_screen();
+ void post_init_level();
+ void post_init_layer(LAYER layer);
+ void post_unload_level();
+ void post_unload_layer(LAYER layer);
+
void on_death_message(STRINGID stringid);
std::optional pre_get_feat(FEAT feat);
bool pre_set_feat(FEAT feat);
diff --git a/src/game_api/script/lua_vm.cpp b/src/game_api/script/lua_vm.cpp
index e0e2ae946..f7a2fb1f9 100644
--- a/src/game_api/script/lua_vm.cpp
+++ b/src/game_api/script/lua_vm.cpp
@@ -95,6 +95,7 @@
#include "usertypes/texture_lua.hpp" // for register_usertypes
#include "usertypes/vanilla_render_lua.hpp" // for VanillaRenderContext
#include "usertypes/vtables_lua.hpp" // for register_usertypes
+#include "virtual_table.hpp"
struct Illumination;
@@ -1892,13 +1893,16 @@ end
/// Disable all crust item spawns, returns whether they were already disabled before the call
lua["disable_floor_embeds"] = disable_floor_embeds;
- /// Get the address for a pattern name
- lua["get_address"] = get_address;
+ /// Get the rva for a pattern name, used for debugging.
+ lua["get_rva"] = [](std::string_view address_name) -> std::string
+ {
+ return fmt::format("{:x}", get_address(address_name) - Memory::get().at_exe(0));
+ };
- /// Get the rva for a pattern name
- lua["get_rva"] = [](std::string_view address_name) -> size_t
+ /// Get the rva for a vtable offset and index, used for debugging.
+ lua["get_virtual_rva"] = [](VTABLE_OFFSET offset, uint32_t index) -> std::string
{
- return get_address(address_name) - Memory::get().at_exe(0);
+ return fmt::format("{:x}", get_virtual_function_address(offset, index));
};
/// Log to spelunky.log
@@ -2096,6 +2100,53 @@ end
/// Get engine target frametime when game is unfocused (1/framerate, default 1/33).
lua["get_frametime_unfocused"] = get_frametime_inactive;
+ /// Destroys all layers and all entities in the level. Usually a bad idea, unless you also call create_level and spawn the player back in.
+ lua["destroy_level"] = destroy_level;
+
+ /// Destroys a layer and all entities in it.
+ lua["destroy_layer"] = destroy_layer;
+
+ /// Initializes an empty front and back layer that don't currently exist. Does nothing(?) if layers already exist.
+ lua["create_level"] = create_level;
+
+ /// Initializes an empty layer that doesn't currently exist.
+ lua["create_layer"] = create_layer;
+
+ /// Setting to false disables all player logic in SCREEN.LEVEL, mainly the death screen from popping up if all players are dead or missing, but also shop camera zoom and some other small things.
+ lua["set_level_logic_enabled"] = set_death_enabled;
+
+ /// Converts INPUTS to (x, y, BUTTON)
+ lua["inputs_to_buttons"] = [](INPUTS inputs) -> std::tuple
+ {
+ float x = 0;
+ float y = 0;
+ if (inputs & 0x100)
+ x = -1;
+ else if (inputs & 0x200)
+ x = 1;
+ if (inputs & 0x400)
+ y = 1;
+ else if (inputs & 0x800)
+ y = -1;
+ BUTTON buttons = (BUTTON)(inputs & 0x3f);
+ return std::make_tuple(x, y, buttons);
+ };
+
+ /// Converts (x, y, BUTTON) to INPUTS
+ lua["buttons_to_inputs"] = [](float x, float y, BUTTON buttons) -> INPUTS
+ {
+ INPUTS inputs = buttons;
+ if (x < 0)
+ inputs |= 0x100;
+ else if (x > 0)
+ inputs |= 0x200;
+ if (y > 0)
+ inputs |= 0x400;
+ else if (y < 0)
+ inputs |= 0x800;
+ return inputs;
+ };
+
lua.create_named_table("INPUTS", "NONE", 0, "JUMP", 1, "WHIP", 2, "BOMB", 4, "ROPE", 8, "RUN", 16, "DOOR", 32, "MENU", 64, "JOURNAL", 128, "LEFT", 256, "RIGHT", 512, "UP", 1024, "DOWN", 2048);
lua.create_named_table(
@@ -2250,7 +2301,24 @@ end
"POST_UPDATE",
ON::POST_UPDATE,
"USER_DATA",
- ON::USER_DATA);
+ ON::USER_DATA,
+ "PRE_LEVEL_CREATION",
+ ON::PRE_LEVEL_CREATION,
+ "POST_LEVEL_CREATION",
+ ON::POST_LEVEL_CREATION,
+ "PRE_LAYER_CREATION",
+ ON::PRE_LAYER_CREATION,
+ "POST_LAYER_CREATION",
+ ON::POST_LAYER_CREATION,
+ "PRE_LEVEL_DESTRUCTION",
+ ON::PRE_LEVEL_DESTRUCTION,
+ "POST_LEVEL_DESTRUCTION",
+ ON::POST_LEVEL_DESTRUCTION,
+ "PRE_LAYER_DESTRUCTION",
+ ON::PRE_LAYER_DESTRUCTION,
+ "POST_LAYER_DESTRUCTION",
+ ON::POST_LAYER_DESTRUCTION);
+
/* ON
// LOGO
// Runs when entering the the mossmouth logo screen.
@@ -2327,7 +2395,7 @@ end
// Params: PreLoadLevelFilesContext load_level_ctx
// Runs right before level files would be loaded
// PRE_LEVEL_GENERATION
- // Runs before any level generation, no entities should exist at this point
+ // Runs before any level generation, no entities should exist at this point. Does not work in all level-like screens. Return true to stop normal level generation.
// POST_ROOM_GENERATION
// Params: PostRoomGenerationContext room_gen_ctx
// Runs right after all rooms are generated before entities are spawned
@@ -2462,6 +2530,26 @@ end
// USER_DATA
// Params: Entity ent
// Runs on all changes to Entity.user_data, including after loading saved user_data in the next level and transition. Also runs the first time user_data is set back to nil, but nil won't be saved to bother you on future levels.
+ // PRE_LEVEL_CREATION
+ // Runs right before the front layer is created. Runs in all screens that usually have entities, or when creating a layer manually.
+ // POST_LEVEL_CREATION
+ // Runs right after the back layer has been created and you can start spawning entities in it. Runs in all screens that usually have entities, or when creating a layer manually.
+ // PRE_LAYER_CREATION
+ // Params: LAYER layer
+ // Runs right before a layer is created. Runs in all screens that usually have entities, or when creating a layer manually.
+ // POST_LAYER_CREATION
+ // Params: LAYER layer
+ // Runs right after a layer has been created and you can start spawning entities in it. Runs in all screens that usually have entities, or when creating a layer manually.
+ // PRE_LEVEL_DESTRUCTION
+ // Runs right before the current level is unloaded and any entities destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+ // POST_LEVEL_DESTRUCTION
+ // Runs right after the current level has been unloaded and all entities destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+ // PRE_LAYER_DESTRUCTION
+ // Params: LAYER layer
+ // Runs right before a layer is unloaded and any entities there destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
+ // POST_LAYER_DESTRUCTION
+ // Params: LAYER layer
+ // Runs right after a layer has been unloaded and any entities there destroyed. Runs in pretty much all screens, even ones without entities. The screen has already changed at this point, meaning the screen being destoyed is in state.screen_last.
*/
lua.create_named_table(
diff --git a/src/game_api/script/usertypes/entity_lua.cpp b/src/game_api/script/usertypes/entity_lua.cpp
index daa7a3d4f..b693abe30 100644
--- a/src/game_api/script/usertypes/entity_lua.cpp
+++ b/src/game_api/script/usertypes/entity_lua.cpp
@@ -12,15 +12,17 @@
#include // for min, max, swap, pair
#include // for _Vector_iterator, vector, _Vector_...
-#include "color.hpp" // for Color, Color::a, Color::b, Color::g
-#include "custom_types.hpp" // for get_custom_types_map
-#include "entities_chars.hpp" // for Player
-#include "entity.hpp" // for Entity, EntityDB, Animation, Rect
-#include "items.hpp" // for Inventory
-#include "math.hpp" // for Quad, AABB
-#include "movable.hpp" // for Movable, Movable::falling_timer
-#include "render_api.hpp" // for RenderInfo, RenderInfo::flip_horiz...
-#include "script/lua_backend.hpp" // for LuaBackend
+#include "color.hpp" // for Color, Color::a, Color::b, Color::g
+#include "containers/game_allocator.hpp" // for game_allocator
+#include "custom_types.hpp" // for get_custom_types_map
+#include "entities_chars.hpp" // for Player
+#include "entity.hpp" // for Entity, EntityDB, Animation, Rect
+#include "items.hpp" // for Inventory
+#include "math.hpp" // for Quad, AABB
+#include "movable.hpp" // for Movable, Movable::falling_timer
+#include "render_api.hpp" // for RenderInfo, RenderInfo::flip_horiz...
+#include "script/lua_backend.hpp" // for LuaBackend
+#include "script/safe_cb.hpp" // for make_safe_cb
namespace NEntity
{
@@ -347,6 +349,15 @@ void register_usertypes(sol::state& lua)
movable_type["set_gravity"] = &Movable::set_gravity;
movable_type["reset_gravity"] = &Movable::reset_gravity;
movable_type["set_position"] = &Movable::set_position;
+ movable_type["process_input"] = &Movable::process_input;
+ movable_type["cutscene"] = sol::readonly(&Movable::cutscene_behavior);
+ movable_type["clear_cutscene"] = [](Movable& movable)
+ {
+ delete movable.cutscene_behavior;
+ movable.cutscene_behavior = nullptr;
+ };
+
+ lua.new_usertype("CutsceneBehavior", sol::no_constructor);
lua["Entity"]["as_entity"] = &Entity::as;
lua["Entity"]["as_movable"] = &Entity::as;
diff --git a/src/game_api/script/usertypes/vtables_lua.cpp b/src/game_api/script/usertypes/vtables_lua.cpp
index 21fdb59c6..5cb5f67f7 100644
--- a/src/game_api/script/usertypes/vtables_lua.cpp
+++ b/src/game_api/script/usertypes/vtables_lua.cpp
@@ -114,6 +114,9 @@ void register_usertypes(sol::state& lua)
{
Entity* ent = get_entity_ptr(uid);
entity_vtable.unhook(ent, callback_id);
+ movable_vtable.unhook(ent, callback_id);
+ floor_vtable.unhook(ent, callback_id);
+ door_vtable.unhook(ent, callback_id);
});
HookHandler::set_hook_dtor_impl(
@@ -150,6 +153,9 @@ void register_usertypes(sol::state& lua)
[](Entity* ent, std::uint32_t callback_id)
{
entity_vtable.unhook(ent, callback_id);
+ movable_vtable.unhook(ent, callback_id);
+ floor_vtable.unhook(ent, callback_id);
+ door_vtable.unhook(ent, callback_id);
});
}
}; // namespace NVTables
diff --git a/src/game_api/search.cpp b/src/game_api/search.cpp
index f8c8e0a02..074c11b27 100644
--- a/src/game_api/search.cpp
+++ b/src/game_api/search.cpp
@@ -2034,6 +2034,64 @@ std::unordered_map g_address_rules{
.offset(0x21)
.at_exe(),
},
+ {
+ "unload_layer"sv,
+ // bp on destroy entity, leave level, it's third in stack or something
+ PatternCommandBuffer{}
+ .find_inst("49 89 cc 8b 41 18 85 c0 74 0b 49 8b 74 24 08"_gh)
+ .at_exe()
+ .function_start(),
+ },
+ {
+ "init_layer"sv,
+ // called a lot in load_screen, for both layers in every screen that has layers
+ PatternCommandBuffer{}
+ .find_inst("48 8d 7e 40 c7 44 24 2c 00 01 00 00"_gh)
+ .at_exe()
+ .function_start(),
+ //.from_exe_base(0x228b58f0),
+ },
+ {
+ "dead_players"sv,
+ // I guess it writes 14 to screen_next before the death screen pops up. Apparently it's a SCREEN_LEVEL virtual too.
+ PatternCommandBuffer{}
+ .find_inst("4c 8b b8 e8 12 00 00 48 8b 80 f0 12 00 00"_gh)
+ .at_exe()
+ .function_start(),
+ //.from_exe_base(0x22c061d0),
+ },
+ {
+ "spawn_transition"sv,
+ // These functions are hooked separately cause hooking the vtable just didn't work right for POST_LEVEL_GENERATION
+ PatternCommandBuffer{}
+ .get_virtual_function_address(VTABLE_OFFSET::THEME_DWELLING, VIRT_FUNC::THEME_SPAWN_TRANSITION)
+ .at_exe(),
+ //.from_exe_base(0x22afe5c0),
+ },
+ {
+ "spawn_transition_cosmic"sv,
+ // These functions are hooked separately cause hooking the vtable just didn't work right for POST_LEVEL_GENERATION
+ PatternCommandBuffer{}
+ .get_virtual_function_address(VTABLE_OFFSET::THEME_COSMICOCEAN, VIRT_FUNC::THEME_SPAWN_TRANSITION)
+ .at_exe(),
+ //.from_exe_base(0x22b373b0),
+ },
+ {
+ "spawn_transition_duat"sv,
+ // These functions are hooked separately cause hooking the vtable just didn't work right for POST_LEVEL_GENERATION
+ PatternCommandBuffer{}
+ .get_virtual_function_address(VTABLE_OFFSET::THEME_CITY_OF_GOLD, VIRT_FUNC::THEME_SPAWN_TRANSITION)
+ .at_exe(),
+ //.from_exe_base(0x22b34940),
+ },
+ {
+ "spawn_transition_olmecship"sv,
+ // These functions are hooked separately cause hooking the vtable just didn't work right for POST_LEVEL_GENERATION
+ PatternCommandBuffer{}
+ .get_virtual_function_address(VTABLE_OFFSET::THEME_BASECAMP, VIRT_FUNC::THEME_SPAWN_TRANSITION)
+ .at_exe(),
+ //.from_exe_base(0x22b2d350),
+ },
};
std::unordered_map g_cached_addresses;
diff --git a/src/game_api/spawn_api.cpp b/src/game_api/spawn_api.cpp
index 428f1d554..7cf829e97 100644
--- a/src/game_api/spawn_api.cpp
+++ b/src/game_api/spawn_api.cpp
@@ -662,18 +662,40 @@ void init_spawn_hooks()
}
}
-void spawn_player(int8_t player_slot, float x, float y)
+int32_t spawn_player(int8_t player_slot, std::optional x, std::optional y, std::optional layer)
{
if (player_slot < 1 || player_slot > 4)
- return;
+ return -1;
+ auto state = State::get().ptr();
+ auto& slot = state->items->player_select_slots[player_slot - 1];
+ if (slot.character < to_id("ENT_TYPE_CHAR_ANA_SPELUNKY") || slot.character > to_id("ENT_TYPE_CHAR_CLASSIC_GUY"))
+ return -1;
+ if (state->items->player_count < player_slot)
+ state->items->player_count = player_slot;
+ slot.activated = true;
push_spawn_type_flags(SPAWN_TYPE_SCRIPT);
OnScopeExit pop{[]
{ pop_spawn_type_flags(SPAWN_TYPE_SCRIPT); }};
- using spawn_player_fun = void(Items*, uint8_t ps, float pos_x, float pos_y);
+ auto old_x = state->level_gen->spawn_x;
+ auto old_y = state->level_gen->spawn_y;
+ state->level_gen->spawn_x = x.value_or(old_x);
+ state->level_gen->spawn_y = y.value_or(old_y);
+ using spawn_player_fun = void(Items*, uint8_t ps);
static auto spawn_player = (spawn_player_fun*)get_address("spawn_player");
- spawn_player(get_state_ptr()->items, player_slot - 1, x, y);
+ // move the back layer to front layer offset if spawning in back layer
+ if (layer.has_value() && layer.value() == LAYER::BACK)
+ std::swap(State::get().ptr()->layers[0], State::get().ptr()->layers[1]);
+ spawn_player(get_state_ptr()->items, player_slot - 1);
+ if (layer.has_value() && layer.value() == LAYER::BACK)
+ std::swap(State::get().ptr()->layers[0], State::get().ptr()->layers[1]);
+ state->level_gen->spawn_x = old_x;
+ state->level_gen->spawn_y = old_y;
+ auto player = state->items->player(player_slot - 1);
+ if (player)
+ return player->uid;
+ return -1;
}
int32_t spawn_companion(ENT_TYPE companion_type, float x, float y, LAYER layer)
diff --git a/src/game_api/spawn_api.hpp b/src/game_api/spawn_api.hpp
index 2fed2ba2c..b618399b0 100644
--- a/src/game_api/spawn_api.hpp
+++ b/src/game_api/spawn_api.hpp
@@ -49,7 +49,7 @@ void pop_spawn_type_flags(SPAWN_TYPE flags);
void init_spawn_hooks();
-void spawn_player(int8_t player_slot, float x, float y);
+int32_t spawn_player(int8_t player_slot, std::optional x, std::optional y, std::optional layer);
int32_t spawn_companion(ENT_TYPE companion_type, float x, float y, LAYER layer);
int32_t spawn_shopkeeper(float x, float y, LAYER layer, ROOM_TEMPLATE room_template = 65);
int32_t spawn_roomowner(ENT_TYPE owner_type, float x, float y, LAYER layer, int16_t room_template = -1);
diff --git a/src/game_api/virtual_table.hpp b/src/game_api/virtual_table.hpp
index 6d611fae1..8a4a3c728 100644
--- a/src/game_api/virtual_table.hpp
+++ b/src/game_api/virtual_table.hpp
@@ -25,6 +25,7 @@ enum class VIRT_FUNC
ENTITY_COLLISION2 = 26,
MOVABLE_DAMAGE = 48,
LOGIC_PERFORM = 1,
+ THEME_SPAWN_TRANSITION = 21,
};
enum class VTABLE_OFFSET
diff --git a/src/injected/ui.cpp b/src/injected/ui.cpp
index b5735dc31..f5cf8ef56 100644
--- a/src/injected/ui.cpp
+++ b/src/injected/ui.cpp
@@ -213,6 +213,7 @@ std::map default_keys{
{"speedhack_turbo", VK_PRIOR},
{"speedhack_slow", VK_NEXT},
{"toggle_uncapped_fps", OL_KEY_CTRL | OL_KEY_SHIFT | 'U'},
+ {"respawn", OL_KEY_CTRL | 'R'},
//{ "", 0x },
};
@@ -267,7 +268,7 @@ std::vector g_selected_ids;
bool set_focus_entity = false, set_focus_world = false, set_focus_zoom = false, set_focus_finder = false, set_focus_uid = false, scroll_to_entity = false, scroll_top = false, click_teleport = false,
throw_held = false, paused = false, show_app_metrics = false, lock_entity = false, lock_player = false,
freeze_last = false, freeze_level = false, freeze_total = false, hide_ui = false,
- enable_noclip = false, load_script_dir = true, load_packs_dir = false, enable_camp_camera = true, enable_camera_bounds = true, freeze_quest_yang = false, freeze_quest_sisters = false, freeze_quest_horsing = false, freeze_quest_sparrow = false, freeze_quest_tusk = false, freeze_quest_beg = false, run_finder = false, in_menu = false, zooming = false, g_inv = false, edit_last_id = false, edit_achievements = false, peek_layer = false, pause_updates = true;
+ enable_noclip = false, load_script_dir = true, load_packs_dir = false, enable_camp_camera = true, enable_camera_bounds = true, freeze_quest_yang = false, freeze_quest_sisters = false, freeze_quest_horsing = false, freeze_quest_sparrow = false, freeze_quest_tusk = false, freeze_quest_beg = false, run_finder = false, in_menu = false, zooming = false, g_inv = false, edit_last_id = false, edit_achievements = false, peek_layer = false, pause_updates = true, death_disable = false;
std::optional quest_yang_state, quest_sisters_state, quest_horsing_state, quest_sparrow_state, quest_tusk_state, quest_beg_state;
Entity* g_entity = 0;
Entity* g_held_entity = 0;
@@ -1234,6 +1235,7 @@ void smart_delete(Entity* ent, bool unsafe = false)
{
static auto first_door = to_id("ENT_TYPE_FLOOR_DOOR_ENTRANCE");
static auto logical_door = to_id("ENT_TYPE_LOGICAL_DOOR");
+ UI::safe_destroy(ent, unsafe);
if ((ent->type->id >= first_door && ent->type->id <= first_door + 15) || ent->type->id == logical_door)
{
auto pos = ent->position();
@@ -1245,14 +1247,9 @@ void smart_delete(Entity* ent, bool unsafe = false)
auto pos = ent->position();
auto layer = (LAYER)ent->layer;
ENT_TYPE type = ent->type->id;
- Callback cb = {g_state->time_total + 1, [pos, layer, type]
- {
- fix_decorations_at(std::round(pos.first), std::round(pos.second), layer);
- UI::cleanup_at(std::round(pos.first), std::round(pos.second), layer, type);
- }};
- callbacks.push_back(cb);
+ fix_decorations_at(std::round(pos.first), std::round(pos.second), layer);
+ UI::cleanup_at(std::round(pos.first), std::round(pos.second), layer, type);
}
- UI::safe_destroy(ent, unsafe);
}
void reset_windows()
@@ -1334,7 +1331,7 @@ int32_t spawn_entityitem(EntityItem to_spawn, bool s, bool set_last = true)
if (to_spawn.name.find("ENT_TYPE_CHAR") != std::string::npos)
{
int spawned = UI::spawn_companion(to_spawn.id, cpos.first, cpos.second, LAYER::PLAYER, g_vx, g_vy);
- if (!lock_entity && set_last)
+ if (!lock_entity && set_last && options["draw_hitboxes"])
g_last_id = spawned;
return spawned;
}
@@ -1348,7 +1345,7 @@ int32_t spawn_entityitem(EntityItem to_spawn, bool s, bool set_last = true)
static const auto ana_spelunky = to_id("ENT_TYPE_CHAR_ANA_SPELUNKY");
auto spawned = UI::spawn_playerghost(ana_spelunky + (rand() % 19), cpos.first, cpos.second, LAYER::PLAYER, g_vx, g_vy);
- if (!lock_entity && set_last)
+ if (!lock_entity && set_last && options["draw_hitboxes"])
g_last_id = spawned;
return spawned;
}
@@ -1422,7 +1419,7 @@ int32_t spawn_entityitem(EntityItem to_spawn, bool s, bool set_last = true)
ent->door_type = to_id("ENT_TYPE_FLOOR_DOOR_LAYER");
ent->platform_type = to_id("ENT_TYPE_FLOOR_DOOR_PLATFORM");
}
- if (!lock_entity && set_last)
+ if (!lock_entity && set_last && options["draw_hitboxes"])
g_last_id = spawned;
return spawned;
}
@@ -1890,8 +1887,6 @@ void quick_start(uint8_t screen, uint8_t world, uint8_t level, uint8_t theme)
g_state->level_next = level;
g_state->theme_next = theme;
g_state->quest_flags = g_state->quest_flags | 1;
- g_state->fadein = 1;
- g_state->fadeout = 1;
g_state->loading = 1;
if (g_game_manager->main_menu_music)
@@ -2296,6 +2291,44 @@ void warp_next_level(size_t num)
}
}
+void respawn()
+{
+ if (g_state->screen != 11 && g_state->screen != 12)
+ {
+ if (g_state->screen > 11)
+ {
+ quick_start(12, g_state->world_start, g_state->level_start, g_state->theme_start);
+ }
+ else
+ {
+ quick_start(12, 1, 1, 1);
+ }
+ return;
+ }
+ for (int8_t i = 0; i < g_state->items->player_count; ++i)
+ {
+ auto found = false;
+ for (auto p : UI::get_players())
+ {
+ if (p->inventory_ptr->player_slot == i)
+ {
+ found = true;
+ if (p->health == 0 || test_flag(p->flags, 29))
+ {
+ p->health = 4;
+ p->flags = clr_flag(p->flags, 29);
+ p->set_behavior(1);
+ }
+ }
+ }
+ if (!found)
+ {
+ g_state->items->player_inventories[i].health = 4;
+ UI::spawn_player(i);
+ }
+ }
+}
+
bool pressed(std::string keyname, WPARAM wParam)
{
if (keys.find(keyname) == keys.end() || (keys[keyname] & 0xff) == 0)
@@ -2859,6 +2892,10 @@ bool process_keys(UINT nCode, WPARAM wParam, [[maybe_unused]] LPARAM lParam)
g_engine_fps = 0;
update_frametimes();
}
+ else if (pressed("respawn", wParam))
+ {
+ respawn();
+ }
else if (pressed("toggle_godmode", wParam))
{
options["god_mode"] = !options["god_mode"];
@@ -3519,6 +3556,66 @@ const char* theme_name(int theme)
void render_narnia()
{
+ if (submenu("Other game screens"))
+ {
+ int screen = -1;
+ ImGui::PushID("WarpSpecial");
+ for (unsigned int i = 0; i < 21; ++i)
+ {
+ if ((i >= 5 && i <= 10))
+ continue;
+ if (options["menu_ui"])
+ {
+ if (ImGui::MenuItem(screen_names[i]))
+ screen = i;
+ }
+ else
+ {
+ if (i % 2)
+ ImGui::SameLine(ImGui::GetContentRegionAvail().x * 0.5f);
+ if (ImGui::Button(screen_names[i], ImVec2(ImGui::GetContentRegionMax().x * 0.5f, 0)))
+ screen = i;
+ }
+ }
+ endmenu();
+ if (screen != -1)
+ {
+ if (screen == 14)
+ {
+ if (g_state->screen == 11 or g_state->screen == 12)
+ UI::load_death_screen();
+ }
+ else if (g_state->screen != 12 && screen >= 11)
+ {
+ quick_start((uint8_t)screen, 1, 1, 1);
+ }
+ else
+ {
+ g_state->screen_next = screen;
+ g_state->loading = 1;
+ }
+ if (screen >= 16 && screen <= 18)
+ {
+ g_state->win_state = 1;
+ if (!g_state->end_spaceship_character)
+ g_state->end_spaceship_character = to_id("ENT_TYPE_CHAR_EGGPLANT_CHILD");
+ }
+ if (screen == 19)
+ {
+ g_state->world_next = 8;
+ g_state->level_next = 99;
+ g_state->theme_next = 10;
+ if (!g_state->level_gen->theme_cosmicocean->sub_theme)
+ g_state->level_gen->theme_cosmicocean->sub_theme = g_state->level_gen->theme_dwelling;
+ g_state->current_theme = g_state->level_gen->theme_cosmicocean;
+ g_state->win_state = 3;
+ if (g_state->level_count < 1)
+ g_state->level_count = 1;
+ }
+ }
+ ImGui::PopID();
+ }
+
ImGui::Text("Next level");
ImGui::SameLine(100.0f);
@@ -4731,21 +4828,26 @@ void render_clickhandler()
}
}
+ static auto front_col = ImColor(0, 255, 51, 200);
+ static auto back_col = ImColor(255, 160, 31, 200);
+ static auto front_fill = ImColor(front_col);
+ front_fill.Value.w = 0.25f;
+ static auto back_fill = ImColor(back_col);
+ back_fill.Value.w = 0.25f;
if (update_entity())
{
- render_hitbox(g_entity, true, ImColor(0, 255, 0, 200));
+ auto this_layer = (peek_layer ? g_state->camera_layer ^ 1 : g_state->camera_layer) == g_entity->layer;
+ render_hitbox(g_entity, true, this_layer ? front_col : back_col);
}
- static auto front_col = ImColor(0, 255, 51, 100);
- static auto back_col = ImColor(255, 160, 31, 100);
for (auto entity : g_selected_ids)
{
auto ent = get_entity_ptr(entity);
if (ent)
{
if (ent->layer == (peek_layer ? g_state->camera_layer ^ 1 : g_state->camera_layer))
- render_hitbox(ent, false, front_col, true);
+ render_hitbox(ent, false, front_fill, true);
else
- render_hitbox(ent, false, back_col, true);
+ render_hitbox(ent, false, back_fill, true);
}
}
@@ -5465,6 +5567,11 @@ void render_options()
UI::godmode_companions(options["god_mode_companions"]);
}
tooltip("Make the hired hands completely deathproof.");
+ if (ImGui::Checkbox("Disable death screen##NoDeath", &death_disable))
+ {
+ UI::death_enabled(!death_disable);
+ }
+ tooltip("Disable the death screen from popping up for any reason.");
if (ImGui::Checkbox("Noclip##Noclip", &options["noclip"]))
{
toggle_noclip();
@@ -7929,6 +8036,8 @@ void render_game_props()
}
if (submenu("Players"))
{
+ if (ImGui::MenuItem("Respawn dead players"))
+ respawn();
ImGui::TextWrapped("New players spawned here can't be controlled, but can be used to test some things that require multiple players.");
if (ImGui::SliderScalar("Number of players##SetNumPlayers", ImGuiDataType_U8, &g_state->items->player_count, &u8_one, &u8_four, "%d", ImGuiSliderFlags_AlwaysClamp))
{
@@ -7962,7 +8071,7 @@ void render_game_props()
{
g_state->items->player_inventories[i].health = 4;
auto uid = g_state->next_entity_uid;
- UI::spawn_player(i, spawn_x, spawn_y);
+ UI::spawn_player(i);
auto player = get_entity_ptr(uid)->as();
player->set_position(spawn_x, spawn_y);
}
@@ -8011,6 +8120,85 @@ void render_game_props()
}
endmenu();
}
+ if (submenu("Level generation flags"))
+ {
+ auto flags = (int)g_state->level_gen->flags;
+ auto flags2 = (int)g_state->level_gen->flags2;
+ auto flags3 = (int)g_state->level_gen->flags3;
+ ImGui::SeparatorText("Flags 1");
+ for (int i = 0; i < 8; i++)
+ {
+ ImGui::CheckboxFlags(levelgen_flags[i], &flags, (int)std::pow(2, i));
+ }
+ ImGui::SeparatorText("Flags 2");
+ for (int i = 0; i < 8; i++)
+ {
+ ImGui::CheckboxFlags(levelgen_flags2[i], &flags2, (int)std::pow(2, i));
+ }
+ ImGui::SeparatorText("Flags 3");
+ for (int i = 0; i < 8; i++)
+ {
+ ImGui::CheckboxFlags(levelgen_flags3[i], &flags3, (int)std::pow(2, i));
+ }
+ g_state->level_gen->flags = (uint8_t)flags;
+ g_state->level_gen->flags2 = (uint8_t)flags2;
+ g_state->level_gen->flags3 = (uint8_t)flags3;
+ if (g_state->current_theme)
+ {
+ ImGui::SeparatorText("Theme flags");
+ ImGui::Checkbox("Allow beehives##ThemeBeeHive", &g_state->current_theme->allow_beehive);
+ ImGui::Checkbox("Allow leprechauns##ThemeLeprechaun", &g_state->current_theme->allow_leprechaun);
+ }
+ endmenu();
+ }
+ if (submenu("Procedural chances"))
+ {
+ static auto hide_zero = true;
+ ImGui::Checkbox("Hide 0% chances", &hide_zero);
+ static auto render_procedural_chance = [](uint32_t id, LevelChanceDef& def)
+ {
+ int inverse_chance = g_state->level_gen->get_procedural_spawn_chance(id);
+ std::string name = std::string(g_state->level_gen->get_procedural_spawn_chance_name(id).value_or(fmt::format("{}", id)));
+ if (def.chances.empty() || (hide_zero && inverse_chance == 0))
+ return;
+ float chance = inverse_chance > 0 ? 100.f / static_cast(inverse_chance) : 0;
+ std::string all = fmt::format("{}", def.chances[0]);
+ for (auto i = 1; i < def.chances.size(); ++i)
+ all += "," + fmt::format("{}", def.chances[i]);
+ std::string str = fmt::format("{:.3f}% ({})", chance, all);
+ ImGui::Text("%s", name.c_str());
+ auto w = ImGui::GetItemRectSize().x;
+ ImGui::SameLine(std::max(0.5f * ImGui::GetContentRegionMax().x, w), 4.f);
+ ImGui::Text("%s", str.c_str());
+ };
+
+ static auto render_chance = [](int inverse_chance, const char* name)
+ {
+ if (hide_zero && inverse_chance == 0)
+ return;
+ float chance = inverse_chance > 0 ? 100.f / static_cast(inverse_chance) : 0;
+ std::string str = fmt::format("{:.3f}%", chance);
+ ImGui::Text("%s", name);
+ auto w = ImGui::GetItemRectSize().x;
+ ImGui::SameLine(std::max(0.5f * ImGui::GetContentRegionMax().x, w), 4.f);
+ ImGui::Text("%s", str.c_str());
+ };
+
+ ImGui::SeparatorText("Monster chances");
+ for (auto [id, def] : g_state->level_gen->data->level_monster_chances)
+ render_procedural_chance(id, def);
+
+ ImGui::SeparatorText("Trap chances");
+ for (auto [id, def] : g_state->level_gen->data->level_trap_chances)
+ render_procedural_chance(id, def);
+
+ ImGui::SeparatorText("Level chances");
+ if (g_state->current_theme)
+ render_chance(g_state->current_theme->get_shop_chance(), "shop");
+ for (auto i = 0; i < 15; ++i)
+ render_chance(g_state->level_gen->data->level_config[i], level_chances[i]);
+ endmenu();
+ }
if (submenu("AI targets"))
{
for (size_t x = 0; x < 8; ++x)
diff --git a/src/injected/ui_util.cpp b/src/injected/ui_util.cpp
index 3fcc543c7..69877d960 100644
--- a/src/injected/ui_util.cpp
+++ b/src/injected/ui_util.cpp
@@ -35,6 +35,10 @@ void UI::godmode_companions(bool g)
{
State::get().godmode_companions(g);
}
+void UI::death_enabled(bool g)
+{
+ set_death_enabled(g);
+}
std::pair UI::click_position(float x, float y)
{
return State::click_position(x, y);
@@ -699,10 +703,11 @@ void UI::safe_destroy(Entity* ent, bool unsafe, bool recurse)
const auto [x, y] = UI::get_position(ent);
const auto sf = ent->type->search_flags;
destroy_entity_items(ent);
- if (sf & 0x100 && test_flag(ent->flags, 3)) // solid floor
+ if (sf & 0x100)
{
- ent->destroy();
- update_liquid_collision_at(x, y, false);
+ if (test_flag(ent->flags, 3)) // solid floor
+ update_liquid_collision_at(x, y, false);
+ destroy_grid(ent->uid);
}
else if (ent->is_liquid())
{
@@ -749,12 +754,17 @@ int32_t UI::spawn_playerghost(ENT_TYPE char_type, float x, float y, LAYER layer,
return uid;
}
-void UI::spawn_player(uint8_t player_slot, float x, float y)
+void UI::spawn_player(uint8_t player_slot, std::optional x, std::optional y, std::optional layer)
{
- ::spawn_player(player_slot + 1, x, y);
+ ::spawn_player(player_slot + 1, x, y, layer);
}
std::pair UI::spawn_position()
{
return {State::get().ptr()->level_gen->spawn_x, State::get().ptr()->level_gen->spawn_y};
}
+
+void UI::load_death_screen()
+{
+ call_death_screen();
+}
diff --git a/src/injected/ui_util.hpp b/src/injected/ui_util.hpp
index d5960d5d1..224cee8f2 100644
--- a/src/injected/ui_util.hpp
+++ b/src/injected/ui_util.hpp
@@ -37,6 +37,7 @@ class UI
public:
static void godmode(bool g);
static void godmode_companions(bool g);
+ static void death_enabled(bool g);
static std::pair click_position(float x, float y);
static void zoom(float level);
static uint32_t get_frame_count();
@@ -84,6 +85,7 @@ class UI
static float get_spark_distance(SparkTrap* ent);
static void save_progress();
static int32_t spawn_playerghost(ENT_TYPE char_type, float x, float y, LAYER layer, float vx, float vy);
- static void spawn_player(uint8_t player_slot, float x, float y);
+ static void spawn_player(uint8_t player_slot, std::optional x = std::nullopt, std::optional y = std::nullopt, std::optional layer = std::nullopt);
static std::pair spawn_position();
+ static void load_death_screen();
};