* 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
298 lines
24 KiB
C++
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
|