#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 #include #include #include #include #include #include #include #include #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 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 tags; }; struct RtEntity { int id = 0; std::string file, name; std::set tags; double x = 0, y = 0, rot = 0, sx = 1, sy = 1; bool alive = true; std::vector comps; uint64_t component_revision = 0; int single_lua_component = 0, lua_component_count = 0, collision_component_count = 0; std::map, 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 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 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& entities() const { return ents_; } struct Timeout { int frame = 0, entity = 0, comp = 0; std::string file, fn; }; std::vector timeouts; // SetTimeOut: run at that frame (tick) std::set orbs_run, orbs_all; // OrbComponent orb_id of the orbs picked this run / ever (the host may load the latter) std::map> postfx; // GameSetPostFxParameter: name -> x, y, z, w (the renderer reads them) std::map postfx_textures; void run_callback(int entity, int comp, const std::string& file, const std::string& fn, const std::vector& 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* 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& args)> on_gpu_script; void set_frame(int f) { frame_ = f; } std::vector* spawn_log = nullptr; // when set, every entity created by spawn() (including the ones LoadEntitiesComponents load) is appended std::function cells_in_rect; // the cells (not air, not gas) in the rectangle x0..x1, y0..y1 inclusive: PressurePlateComponent (00c01860) std::function on_sprite_fade; // LifetimeComponent fade_sprites (00b9a010): every frame the sprites' alpha is 1 - fade, fade = age / lifetime std::function on_spawned; // after a script-created entity exists (host may give it a GPU slot) std::function on_killed; std::function on_game_effect_end; // a GameEffectComponent whose frames run out (00b015d0: the counter reaches 0), before its entity goes std::function on_physics_init; // PhysicsBody2InitFromComponents: the host makes the bodies again from the components std::function on_set_fire; // an ElectricityComponent with hack_is_set_fire (fire.xml): the host lights the cells there std::function on_component_added; // a script added a component (EntityAddComponent): perk_spawn gives a loaded perk.xml its sprite std::function on_components_enabled; // a component of the entity was switched on or off std::function 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 allow_pick; // Message_ItemAllowedToPickUp: the inventory has a free place for the item (and is closed); false = it stays where it is std::function on_hidden; // picked up into the inventory: still alive, no longer in the world std::function on_value_changed; // component value written by a script std::vector take_messages(); const std::vector& 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 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& 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 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 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 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& query, const std::function& 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 with_file(const std::string& part); std::vector 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 with_component_field(const std::string& type, const std::string& key, const std::optional& value = std::nullopt); int player() const { return player_id_; } void set_player(int id) { player_id_ = id; } std::vector 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& 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 with_tag(const std::string& tag) override; std::vector 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 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 components(int entity, const std::string& type, const std::string& tag, bool include_disabled) override; std::vector 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& 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 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 on_play_sound; // the host's sound (src/audio) std::function on_entity_sound; std::function 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 on_remove_joints; // PhysicsRemoveJoints: the host's bodies std::function 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 on_convert_material; // ConvertMaterialEverywhere std::function 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 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 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 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 on_physics_force; std::vector 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(); } 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(double, double, double, double)> bodies_in_area_fn; std::function 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 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 attrs); void erase_comp_internal(int component); std::vector 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& 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 ents_; std::unordered_map comps_; std::map, std::less<>> entities_by_component_; using FieldQuery = std::tuple>; std::map> 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 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 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 messages_; std::vector errors_; std::vector pending_scripts_; // entities whose on-added scripts still have to run }; } // namespace ng