Files
Noita-GPU-Vulkan/src/entity/runtime.h
T
Gaël 00a9c1c26c Lot of bugfixes
* Adding clipper to generate the terrain's negative powder-contour offset.
* Fixing RDB issues (fracture, active trigger, performances)
* Fixing blood emission
* Fixing the freezing formula on liquids
* Fixing Steve spawning due to a missed test.
* Reaction table properly build
2026-10-21 20:46:13 +02:00

298 lines
24 KiB
C++

#pragma once
// CPU-side game entities with Noita's component model: what Lua scripts see through EntityGetFirstComponent,
// ComponentGetValue2, EntityLoad, EntityKill, ... (lua/entity_api.h). Positions of entities that also live on the GPU are
// synchronised by the host; item pickup, chest opening and loot spawning run here, through the original scripts.
#include <functional>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <tuple>
#include <unordered_map>
#include <vector>
#include "entity/entity.h"
#include "entity/game_services.h"
#include "lua/lua_host.h"
namespace ng {
struct RtComponent {
int id = 0, entity = 0;
std::string type;
std::map<std::string, std::string> attrs;
bool enabled = true;
uint64_t value_revision = 0;
int gpu_lifetime = 0;
bool gpu_lifetime_pending = false; // comp() materializes this GPU mirror before exposing the component
std::set<std::string> tags;
};
struct RtEntity {
int id = 0;
std::string file, name;
std::set<std::string> tags;
double x = 0, y = 0, rot = 0, sx = 1, sy = 1;
bool alive = true;
std::vector<int> comps;
uint64_t component_revision = 0;
int single_lua_component = 0, lua_component_count = 0, collision_component_count = 0;
std::map<std::string, std::vector<int>, std::less<>> comps_by_type;
int parent = 0;
int born = 0;
int died = -1; // the frame of kill(); the record and its components are erased a few frames later (tick)
bool parked = false; // in a part of the world far from the player (main.cpp parkEntity): kept as it is but not alive (no scripts, no queries, no slot simulated) until it wakes
bool follow = false; double lx = 0, ly = 0;
bool inline_child = false; // made from an inline <Entity> of its parent's file (no GPU slot of its own yet) // a child added to a parent keeps its offset from it (the parent's position is synced from the GPU)
std::vector<SpawnComp> extra; // components a world-generation script added (perks)
bool cpu_motion = false; // moved by the runtime itself (VelocityComponent + SimplePhysicsComponent + DieIfSpeedBelowComponent, as the engine does): entities with scripts that keep running
int gpu = -1; // GPU entity slot, -1 = none (effects, markers, scripts-only entities)
};
struct GameMessage {
std::string title, text;
bool important = false;
};
class EntityRuntime : public IEntityWorld {
public:
EntityRuntime(const Wak& wak, LuaHost& host);
// ---- the engine services behind the rest of the Lua API (src/entity/runtime_natives.cpp; the host fills the hooks) ----
GameServices svc;
int max_entity_id() const { return next_ent_ - 1; }
const std::map<int, RtEntity>& entities() const { return ents_; }
struct Timeout { int frame = 0, entity = 0, comp = 0; std::string file, fn; };
std::vector<Timeout> timeouts; // SetTimeOut: run at that frame (tick)
std::set<int> orbs_run, orbs_all; // OrbComponent orb_id of the orbs picked this run / ever (the host may load the latter)
std::map<std::string, std::vector<float>> postfx; // GameSetPostFxParameter: name -> x, y, z, w (the renderer reads them)
std::map<std::string, std::string> postfx_textures;
void run_callback(int entity, int comp, const std::string& file, const std::string& fn, const std::vector<LVal>& args) { run_script_callback(entity, comp, file, fn, args); }
// Creates an entity. forced_id > 0 assigns that id (initial entities use id = GPU slot + 1).
// notify = false: the new entity never gets a GPU slot (cards inside wands).
int spawn(const std::string& path, double x, double y, int forced_id = 0, bool notify = true, const std::vector<SpawnComp>* extra = nullptr);
RtEntity* ent(int id);
RtComponent* comp(int id);
// ---- host side ----
// Return true only when a compiled GPU backend has accepted this exact call.
std::function<bool(int entity, int component, const std::string& file, const std::string& function, const std::vector<LVal>& args)> on_gpu_script;
void set_frame(int f) { frame_ = f; }
std::vector<int>* spawn_log = nullptr; // when set, every entity created by spawn() (including the ones LoadEntitiesComponents load) is appended
std::function<int(int x0, int y0, int x1, int y1)> cells_in_rect; // the cells (not air, not gas) in the rectangle x0..x1, y0..y1 inclusive: PressurePlateComponent (00c01860)
std::function<void(int id, double fade)> on_sprite_fade; // LifetimeComponent fade_sprites (00b9a010): every frame the sprites' alpha is 1 - fade, fade = age / lifetime
std::function<void(int id)> on_spawned; // after a script-created entity exists (host may give it a GPU slot)
std::function<void(int id)> on_killed;
std::function<void(int effect_entity, int comp)> on_game_effect_end; // a GameEffectComponent whose frames run out (00b015d0: the counter reaches 0), before its entity goes
std::function<void(int id)> on_physics_init; // PhysicsBody2InitFromComponents: the host makes the bodies again from the components
std::function<void(double x, double y)> on_set_fire; // an ElectricityComponent with hack_is_set_fire (fire.xml): the host lights the cells there
std::function<void(int id, int comp)> on_component_added; // a script added a component (EntityAddComponent): perk_spawn gives a loaded perk.xml its sprite
std::function<void(int id)> on_components_enabled; // a component of the entity was switched on or off
std::function<std::string(int item)> display_name; // the name the game shows for an item (spells: the translated action name, wands: their own name); empty = the ItemComponent's
std::function<bool(int item)> allow_pick; // Message_ItemAllowedToPickUp: the inventory has a free place for the item (and is closed); false = it stays where it is
std::function<void(int id)> on_hidden; // picked up into the inventory: still alive, no longer in the world
std::function<void(int id, const std::string& type, const std::string& key)> on_value_changed; // component value written by a script
std::vector<GameMessage> take_messages();
const std::vector<std::string>& script_errors() const { return errors_; }
void clear_script_errors() { errors_.clear(); }
// Gameplay: called once per frame with the player's position. interact = the E key went down this frame.
// Returns the ids of items picked up (their pickup scripts have run).
std::vector<int> pickup_step(double player_x, double player_y, bool interact);
// Applies lifetimes (LifetimeComponent) and similar per-frame upkeep.
void tick();
void load_entities(int id, int lc, bool notify); // a LoadEntitiesComponent loads its entities (at spawn, or when its timeout_frames timer comes)
long script_runs() const { return script_runs_; } // periodic script executions so far (debug)
bool native_projectile_scripts = true;
long native_script_runs() const { return native_script_runs_; }
// The area whose entities run their scripts (the loaded world around the camera): LuaComponents with execute_every_n_frame, CollisionTriggerComponents.
void set_active_area(double cx, double cy, double hw, double hh) { act_cx_ = cx; act_cy_ = cy; act_hw_ = hw; act_hh_ = hh; scripts_on_ = true; }
// Runs the script_death scripts of an entity (death(damage types, message, killer, drop_items)): the gold drop, loot, ... The caller kills the entity afterwards.
void run_death_scripts(int id, int damage_types, int killer);
// A message of the engine to an entity (Message_Kick, Message_PortalTeleportUsed, Message_WandFired...): every enabled LuaComponent of the entity that has the script_* field set
// (`field`) runs its function `func` with the arguments. Returns how many scripts ran.
bool has_event_script(int id, const std::string& field); // the entity or one of its children has an enabled LuaComponent with that script_* field
int send_event(int id, const std::string& field, const std::string& func, const std::vector<LVal>& args);
// Message_Kick (0083d970): the entities with a script_kick within `radius` of the point get kick( who_kicked )
void kick_entities(int kicker, double x, double y, double radius);
// InteractableComponent (00b4c810, defaults 00b49b70: radius 10): when the player presses the interact key within `radius` of an interactable the first one gets Message_Interaction:
// script_interacting -> interacting( entity_who_interacted, entity_interacted, interactable_name ) (the name is the component's ui_text)
void interact_step(int who, double x, double y, bool pressed);
// script_physics_body_modified of the LuaComponents: physics_body_modified( is_destroyed ) (a lantern drops fire when its body is damaged)
void run_body_modified(int id, bool is_destroyed);
std::vector<int> game_effect_components(int entity, const std::string& effect) override;
int load_game_effect(int entity, const std::string& effect, bool always_new, int& effect_entity) override;
int load_game_effect_file(int entity, const std::string& file) override;
struct GameEffectRef { int owner = 0, comp = 0; std::string effect; bool disable_movement = false; int caster_herd = 0; }; // caster_herd: herd + 1 of the one that loaded the effect (mCasterHerdId), 0 = unknown
std::vector<GameEffectRef> all_game_effects(); // every enabled GameEffectComponent with the entity it affects (the parent of its entity, or the entity itself)
bool has_game_effect(int entity, const std::string& effect) { return !game_effect_components(entity, effect).empty(); }
// script_damage_received of the LuaComponents: damage_received( damage, message, entity_thats_responsible, is_fatal, projectile_thats_responsible ). The DamageModelComponent still
// holds the hp from before the hit while the script runs (the scripts compute `health - damage`); `hp_after` is stored when it returns.
void run_damage_received(int id, double damage, const std::string& message, int responsible, bool is_fatal, double hp_before, double hp_after, int projectile = 0);
bool quiet_spawn = false; // spawn() runs no execute_on_added scripts (a stand-in entity: the projectile of a GPU hit handed to script_damage_received)
bool has_damage_script(int id);
// script_damage_about_to_be_received (Message_DamageAboutToBeApplied at the start of TakeDamage_Impl 00ab9a90, sent to the entity and every descendant by 00d17ad0;
// LuaComponent handler 00ba80d0): each script gets ( damage, x, y, entity_thats_responsible, critical_hit_chance ) and its two results replace them. Returns the damage.
double run_damage_about_to_be_received(int id, double damage, double x, double y, int responsible, int& crit);
std::set<int> damage_about_entities(); // the entities that have such a script on themselves or a descendant (one pass over the components)
// MaterialAreaCheckerComponent: every update_every_x_frame frames the host tells whether one of the two materials is in the area
// (x0, y0, x1, y1 in world cells): -1 = the area is not loaded (nothing happens), 0 = no, 1 = yes. A checker that looks for failure calls
// script_material_area_checker_failed of the entity's LuaComponent when the answer is 0, the other one the _success script when it is 1.
// query: the cells of the box that are m1 or m2 (*total: all its cells), -1 when the box is not loaded
void material_area_step(const std::function<int(double x0, double y0, double x1, double y1, const std::string& m1, const std::string& m2, int* total)>& query,
const std::function<bool(double x0, double y0, double x1, double y1)>& ready = {});
// TeleportComponent (with an enabled HitboxComponent): true when the player point is inside the hitbox of an enabled teleporter; gives the destination
// (target.x / target.y, absolute or relative to the teleporter) and the entity.
bool teleport_step(double player_x, double player_y, double& target_x, double& target_y, int* teleporter = nullptr);
// Kills every entity of the world except the player and what the player carries (the area was left).
void clear_world_entities();
std::vector<int> with_file(const std::string& part);
std::vector<int> with_component(std::string_view type); // living entities with a component of the type
// Incremental indexes for fields changed through set_value (null value: nonempty).
std::vector<int> with_component_field(const std::string& type, const std::string& key, const std::optional<std::string>& value = std::nullopt);
int player() const { return player_id_; }
void set_player(int id) { player_id_ = id; }
std::vector<int> inventory; // picked up items without a pickup script (wands, potions, spells)
int money();
void spend_money(int amount); // WalletComponent: money -= amount, money_spent += amount (a purchase)
int pickup_candidate(double px, double py);
void pick(int item, int who); // the item goes into the inventory of `who` (its script_item_picked_up first) // the item E would pick up now (0: none), the rules of pickup_step
// Text for the nearest item that needs the E key ("E Open chest"), empty when none is in reach.
std::string interact_hint(double player_x, double player_y); // player's WalletComponent
// ---- IEntityWorld ----
int load(const std::string& path, double x, double y) override { return spawn(path, x, y); }
int load_with(const std::string& path, double x, double y, const std::vector<SpawnComp>& extra) override { return spawn(path, x, y, 0, true, &extra); }
void kill(int id) override;
void physics_init(int id) override { if (on_physics_init) on_physics_init(id); }
bool alive(int id) override;
bool get_transform(int id, double& x, double& y, double& rot, double& sx, double& sy) override;
void set_transform(int id, double x, double y, double rot, double sx, double sy) override;
std::string get_name(int id) override;
void set_name(int id, const std::string& name) override;
std::string get_filename(int id) override;
bool has_tag(int id, const std::string& tag) override;
void add_tag(int id, const std::string& tag) override;
void remove_tag(int id, const std::string& tag) override;
std::string get_tags(int id) override;
std::vector<int> with_tag(const std::string& tag) override;
std::vector<int> in_radius(double x, double y, double r, const std::string& tag) override;
void script_query(bool on) override { script_query_ = on; }
bool script_query_ = false; // (before_tag_query runs only for a script's query: the host's own enumerations must not make a twin of every GPU projectile)
int parent(int id) override;
std::vector<int> children(int id) override;
void add_child(int parent, int child) override;
void assign_card_slot(int wand, int card); // a spell card that becomes a child of a wand takes the first free slot of the wand
int add_gun_action(int wand, const std::string& action_id, bool permanent) override;
std::vector<int> components(int entity, const std::string& type, const std::string& tag, bool include_disabled) override;
std::vector<int> components(int entity, const char* type, const std::string& tag, bool include_disabled) { return components_view(entity, type, tag, include_disabled); }
int add_component(int entity, const std::string& type, const std::map<std::string, std::string>& attrs) override;
void remove_component(int comp) override;
void set_component_enabled(int comp, bool enabled) override;
bool component_enabled(int comp) override;
// Item emitter context tags apply to the item itself: a card inside the
// held wand is stored, not held. Untagged/non-item effects keep their state.
bool item_emitter_enabled(int comp, int held_item);
std::string component_type(int comp) override;
int component_entity(int comp) override;
bool comp_has_tag(int comp, const std::string& tag) override;
LVal get_value(int comp, const std::string& key, bool typed) override;
void set_value(int comp, const std::string& key, const LVal& v) override;
int updated_entity() override { return updated_entity_; }
int updated_component() override { return updated_comp_; }
int frame() override { return frame_; }
std::function<void(int shooter, double x, double y, double tx, double ty, int entity, bool send_message)> on_shoot; // GameShootProjectile: the host turns the entity into a GPU projectile
void shoot_projectile(int shooter, double x, double y, double tx, double ty, int entity, bool send_message = true) override { if (on_shoot) on_shoot(shooter, x, y, tx, ty, entity, send_message); }
std::function<void(const std::string& bank, const std::string& event, double x, double y)> on_play_sound; // the host's sound (src/audio)
std::function<void(int entity, const std::string& name)> on_entity_sound;
std::function<void(int entity, const std::string& tag, double intensity, double intensity2)> on_entity_sound_loop;
void play_sound(const std::string& bank, const std::string& event, double x, double y) override { if (on_play_sound) on_play_sound(bank, event, x, y); }
std::function<void(double, double, double, double)> on_remove_joints; // PhysicsRemoveJoints: the host's bodies
std::function<void(double)> on_screenshake; // GameScreenshake
void physics_remove_joints(double x0, double y0, double x1, double y1) override { if (on_remove_joints) on_remove_joints(x0, y0, x1, y1); }
void screenshake(double amount, double, double) override { if (on_screenshake) on_screenshake(amount); }
std::function<void(int, int)> on_convert_material; // ConvertMaterialEverywhere
std::function<bool(const std::string& image, int& w, int& h)> image_size; // the size of an image file (the centered physics image shapes)
void convert_material_everywhere(int from, int to) override { if (on_convert_material) on_convert_material(from, to); }
void entity_play_sound(int entity, const std::string& name) override { if (on_entity_sound) on_entity_sound(entity, name); }
void entity_play_sound_loop(int entity, const std::string& tag, double intensity, double intensity2) override { if (on_entity_sound_loop) on_entity_sound_loop(entity, tag, intensity, intensity2); }
std::function<void(int entity, double amount, const std::string& type, const std::string& description, const std::string& ragdoll_fx, double ix, double iy, int responsible, double wx, double wy, double knockback)> on_inflict_damage; // the host applies it to the GPU entity
void inflict_damage(int entity, double amount, const std::string& type, const std::string& description, const std::string& ragdoll_fx, double ix, double iy, int responsible, double wx, double wy, double knockback) override { if (on_inflict_damage) on_inflict_damage(entity, amount, type, description, ragdoll_fx, ix, iy, responsible, wx, wy, knockback); }
std::function<std::string(double, double)> biome_fn; // the host knows the biome map
std::string biome_name(double x, double y) override { return biome_fn ? biome_fn(x, y) : std::string("_EMPTY_"); }
int world_state_entity() override { return world_state_; }
int create_entity(const std::string& name) override;
void regen_item_actions(int player) override;
void remove_from_parent(int id) override { if (RtEntity* e = ent(id)) { e->parent = 0; e->follow = false; } }
std::function<void(double&, double&, double&, double&)> camera_fn; // the host knows the camera (x, y, width, height in world cells)
bool camera_bounds(double& x, double& y, double& w, double& h) override { if (!camera_fn) return false; camera_fn(x, y, w, h); return true; }
void comp_add_tag(int comp, const std::string& tag) override;
int herd_relation(int a, int b) override;
int wand_capacity(int wand) override;
void print(const std::string& title, const std::string& text, bool important) override;
void physics_force(int entity, double fx, double fy) override { if (on_physics_force) on_physics_force(entity, fx, fy); }
std::function<void(int id, double fx, double fy)> on_physics_force;
std::vector<BodyInfo> bodies_in_area(double x0, double y0, double x1, double y1) override { return bodies_in_area_fn ? bodies_in_area_fn(x0, y0, x1, y1) : std::vector<BodyInfo>(); }
void physics_area_force(int id, double fx, double fy) override { if (on_physics_area_force) on_physics_area_force(id, fx, fy); }
std::function<std::vector<BodyInfo>(double, double, double, double)> bodies_in_area_fn;
std::function<void(int id, double fx, double fy)> on_physics_area_force; // PhysicsApplyForce: the host pushes the rigid body of the entity
// a tag query of a script (EntityGetWithTag, EntityGetInRadiusWithTag, EntityGetInRadius...): the host makes the entities the query must see that exist only on the GPU (the plain
// projectiles: lazy twins); radius < 0 = the whole world. Empty tag = every entity
std::function<void(const std::string& tag, double x, double y, double radius)> before_tag_query;
void load_to_entity(const std::string& path, int entity) override;
std::string file_tags(const std::string& file) { const EntityDef* d = db_.get(file); return d ? d->tags : std::string(); }
private:
int add_comp_internal(int entity, const std::string& type, std::map<std::string, std::string> attrs);
void erase_comp_internal(int component);
std::vector<int> components_view(int entity, std::string_view type, const std::string& tag, bool include_disabled);
bool run_script_callback(int entity, int comp, const std::string& file, const std::string& function, const std::vector<LVal>& args);
bool run_native_guard(int entity, const std::string& file);
void update_component_fields(const RtComponent& component, int delta, std::string_view key = {});
const Wak& wak_;
LuaHost& host_;
EntityDb db_;
std::map<int, RtEntity> ents_;
std::unordered_map<int, RtComponent> comps_;
std::map<std::string, std::map<int, int>, std::less<>> entities_by_component_;
using FieldQuery = std::tuple<std::string, std::string, std::optional<std::string>>;
std::map<FieldQuery, std::map<int, int>> component_fields_;
struct LuaSchedule {
uint64_t revision = uint64_t(-1);
std::string file;
int every = 1, times = 0, done = 0, next = -1;
bool call_init = false, inited = false, remove = false;
};
std::unordered_map<int, LuaSchedule> lua_schedules_;
struct NativeGuardState {
uint64_t revision = uint64_t(-1);
int angle_component = 0;
const std::string *angle = nullptr, *rotation = nullptr, *owner = nullptr;
std::string rotation_text, owner_text;
double rotation_value = 0, owner_value = 0;
bool has_storage = false;
};
std::unordered_map<int, NativeGuardState> native_guard_states_;
double act_cx_ = 0, act_cy_ = 0, act_hw_ = 0, act_hh_ = 0;
bool scripts_on_ = false;
long script_runs_ = 0;
long native_script_runs_ = 0;
uint64_t native_source_revision_ = uint64_t(-1), native_api_revision_ = uint64_t(-1);
bool native_guard_source_ = false, native_guard_api_ = false;
std::string native_guard_env_;
static constexpr int kDynamicIdBase = 1 << 16; // ids below are reserved for forced ids (the initial world entities: GPU slot + 1, the player 1)
int next_ent_ = kDynamicIdBase, next_comp_ = 1, frame_ = 0, player_id_ = 0, world_state_ = 0;
int updated_entity_ = 0, updated_comp_ = 0;
uint64_t component_revision_ = 0;
std::vector<GameMessage> messages_;
std::vector<std::string> errors_;
std::vector<int> pending_scripts_; // entities whose on-added scripts still have to run
};
} // namespace ng