diff --git a/shaders/proj_spawn.comp b/shaders/proj_spawn.comp index 729dd7d..2c11ff2 100644 --- a/shaders/proj_spawn.comp +++ b/shaders/proj_spawn.comp @@ -14,7 +14,7 @@ void main() { for (int r = int(gl_LocalInvocationID.x); r < reqCount; r += 64) { vec4 r0 = hcast[(list * 256 + r) * 2], r1 = hcast[(list * 256 + r) * 2 + 1]; int node = int(r1.x); - uint seed = uint(r1.z) * 2654435761u + uint(r) * 40503u + uint(pc.frame); + uint seed = uint(abs(r1.z)) * 2654435761u + uint(r) * 40503u + uint(pc.frame); if (node < 0) { boom_fx(-node - 1, r0.xy, seed, -1.0); continue; } vec4 n0 = hnodes[node * 4], n1 = hnodes[node * 4 + 1], n2 = hnodes[node * 4 + 2], n3 = hnodes[node * 4 + 3]; int first = int(n0.x), count = int(n0.y); @@ -69,9 +69,9 @@ void main() { } p.d = ivec4(type, first + i, ((int(r1.y) & 3) << 8) | ((shotBits & 1) << 10) | (min(int(n3.y) & 8191, 4095) << 12) | (((int(n3.y) >> 13) & 15) << 24) | (n2.z > 0.0 ? (1 << 28) : 0), int(serial + 1u)); // bits 24-27: lightning_count, bit 28: the wand added bounces (00c0b440 then sets bounce_at_any_angle) // team in bits 8-9, the critical chance (percent) from bit 12 // team in bits 8-9 (1 the player's wand, 2 enemies), fired triggers in the low bits fprojs[slot] = p; - if ((fti(tb + 11).y & 1) != 0) { // a projectile with components the GPU does not simulate: the host makes its runtime twin (event kind -4, two entries) + if ((fti(tb + 11).y & 1) != 0 || r1.z < 0.0) { // negative seed: bind the script's existing entity as the twin uint xi = uint(atomicAdd(xhead.x, 2)); - if (xi + 1u < 512u) { xe[xi] = vec4(r0.xy, -4.0, float(slot)); xe[xi + 1u] = vec4(float(serial + 1u), float(type), float(shooterSlot), float(int(r1.y) & 3)); } + if (xi + 1u < 512u) { xe[xi] = vec4(r0.xy, -4.0, float(slot)); xe[xi + 1u] = vec4(float(serial + 1u), float(type), float(shooterSlot), r1.z < 0.0 ? r1.z : float(int(r1.y) & 3)); } } if ((int(r1.w) & 1) != 0) { // muzzle flash of the first projectiles of a cast ivec4 t6 = fti(tb + 6); diff --git a/shaders/proj_step.comp b/shaders/proj_step.comp index 7b1aa04..0351812 100644 --- a/shaders/proj_step.comp +++ b/shaders/proj_step.comp @@ -59,6 +59,7 @@ int shot_flags(int typeFlags, int dz) { return ((dz >> 10) & 1) != 0 ? (typeFlag bool victim_at(vec2 pos, int shooter, int herd, int flags, int age, out int hit) { hit = -1; for (int k = 0; k < ctrl.inp.w; ++k) { + if (ents[k].ex19.w == -float(gl_GlobalInvocationID.x + 1u)) continue; // its own damage proxy const int kind = ents[k].st.x; if (kind == 0 && (ents[k].st.w & 64) != 0 && ents[k].ex19.w > 0.5) { // a worm's segment (WormComponent hitbox_radius around every part): the hit goes to the worm const int root = int(ents[k].ex19.w) - 1; diff --git a/src/app/inventory.h b/src/app/inventory.h index 1ad93f3..5ed1800 100644 --- a/src/app/inventory.h +++ b/src/app/inventory.h @@ -153,6 +153,7 @@ public: void remove(int item) { const Loc l = where(item); take(item); + if (item && gun_.equipped() == item) gun_.equip(0); (void)l; } diff --git a/src/app/main.cpp b/src/app/main.cpp index 122b02c..23b4573 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -80,6 +81,7 @@ void fly_waypoint(FlyPath& fp, float px, float py, float gx, float gy, int frame #include "app/inventory.h" #include "entity/status.h" #include "gun/gpu_fx.h" +#include "gun/projectile_damage.h" #include "gun/projectiles.h" #include "physics/gpu_bodies.h" #include "phys/phys_system.h" @@ -1763,7 +1765,7 @@ int main(int argc, char** argv) { // ---- entity runtime <-> GPU: slots, hooks, uploads of entities spawned by scripts ---- std::vector slotToId(kMaxEnts, 0); struct CollFx { double splash = 0; int mat = -1, cells = 0; }; // solid_on_collision_splash_power, solid_on_collision_material, pixels of the body - struct ScriptShot { std::string file; int shooter; double x, y, tx, ty; bool send = true; }; + struct ScriptShot { std::string file; int shooter; double x, y, tx, ty; bool send = true; int entity = 0; }; std::vector pendingShots; // GameShootProjectile calls of this tick std::map scriptShotNode; // file -> persistent node of the GPU projectile system // the game effects (GameEffectComponent children) of creatures and the player, as the GPU sees them @@ -1888,8 +1890,21 @@ int main(int argc, char** argv) { std::vector cutRules; bool cutGrow = false; std::vector> pendingEdits; // (x, y, radius, max durability, mode, material, probability, 0) - struct ProjTwin { int slot = -1, serial = 0, id = 0; std::string sig; bool dirty = false; }; // + sig: the twin's components without the live fields when the projectile's type was last set; dirty: a script changed a field of the type since // a GPU projectile with a runtime entity of its own (CellEater, MagicConvertMaterial, scripts...): ring slot, cast serial (d.w), entity id + struct ProjTwin { int slot = -1, serial = 0, id = 0; std::string sig; bool dirty = false; int trailFrame = -1; double trailX = 0, trailY = 0; bool synced = false; std::array lastGpu{}; int lastShooter = 0; uint64_t syncRevision = uint64_t(-1), hostRevision = uint64_t(-1), shieldLayoutRevision = uint64_t(-1), shieldValueRevision = uint64_t(-1); std::vector projectileComponents; bool velocityRotation = true, hasHostSystems = true, pendingRemoval = false, shieldNoOp = false; int shieldComponent = 0; double angularVelocity = 0; }; // GPU slot, cast serial, runtime entity and cached component layout / synchronized fields std::vector projTwins; + std::set deferredProjectileSlots; + bool realizingProjectileSlots = false; + size_t twinRevision = 0, indexedTwinRevision = size_t(-1); + std::unordered_map twinIndex; + auto findTwin = [&](int id) -> ProjTwin* { + if (indexedTwinRevision != twinRevision) { + twinIndex.clear(); + for (size_t i = 0; i < projTwins.size(); ++i) twinIndex.emplace(projTwins[i].id, i); + indexedTwinRevision = twinRevision; + } + const auto it = twinIndex.find(id); + return it == twinIndex.end() || projTwins[it->second].pendingRemoval ? nullptr : &projTwins[it->second]; + }; bool gpuLuaRecovering = false; struct PendingGpuLua { int entity, component; std::string file; }; std::vector pendingGpuLua; @@ -2207,6 +2222,7 @@ int main(int argc, char** argv) { // radius, explosion damage, age, bounces; e shooter slot + 1, herd) are written into its record; every other field (on_death_explode, collide_with_world, penetrate_world, // damage_by_type, bounce_*, knockback_force, sprites...) belongs to the type: the projectile gets the type of a definition built from the twin's components (def_from). auto twinLiveField = [](const std::string& type, const std::string& key) -> int { // the FProj float the field lives in (index in the 24 floats of the record), -1 = a field of the type, -2 = not the GPU's + if (key.rfind("_ng_", 0) == 0) return -2; // per-instance host clocks do not change GPU types or their emitters if (key.size() > 1 && key[0] == 'm' && std::isupper((unsigned char)key[1]) && key != "mWhoShot" && key != "mShooterHerdId" && key != "mVelocity.x" && key != "mVelocity.y") return -2; // the engine's own state if (type == "ProjectileComponent") { if (key == "damage") return 4; @@ -2247,27 +2263,44 @@ int main(int argc, char** argv) { } return s; }; - auto twinFromGpu = [&](int id, const float* f) { // the live fields of the GPU projectile into the twin (the scripts read them: SPELLS_TO_POWER sums the damage) - const int age = (int)f[10]; - for (int pc : rt->components(id, "ProjectileComponent", "", true)) { - auto& at = rt->comp(pc)->attrs; - at["damage"] = std::to_string(f[4]); - at["lifetime"] = std::to_string(f[5] >= 1e8f ? -1 : std::max(0, (int)f[5] - age)); - if (!at.count("mStartingLifetime") || at["mStartingLifetime"].empty()) at["mStartingLifetime"] = std::to_string(f[5] >= 1e8f ? -1 : (int)f[5]); // (set at the launch: the lifetime it started with) // the frames left (the engine counts lifetime down; mStartingLifetime keeps the start) - at["config_explosion.explosion_radius"] = std::to_string(f[8]); - at["config_explosion.damage"] = std::to_string(f[9]); - at["bounces_left"] = std::to_string((int)f[11]); - const int ss = (int)f[18]; - at["mWhoShot"] = std::to_string(ss == 1 ? rt->player() : (ss > 1 && ss <= entN ? slotToId[ss - 1] : 0)); - at["mShooterHerdId"] = std::to_string((int)f[19]); + auto twinFromGpu = [&](int id, const float* f, ProjTwin* twin = nullptr) { // the live fields of the GPU projectile into the twin (the scripts read them: SPELLS_TO_POWER sums the damage) + ng::RtEntity* te = rt->ent(id); + if (twin && te && twin->syncRevision != te->component_revision) twin->synced = false; + std::vector localComponents; + if (!twin || !twin->synced) { + auto& cached = twin ? twin->projectileComponents : localComponents; + cached.clear(); + for (int cid : rt->components(id, "ProjectileComponent", "", true)) cached.push_back(rt->comp(cid)); } - for (int vc : rt->components(id, "VelocityComponent", "", true)) { auto& at = rt->comp(vc)->attrs; at["mVelocity.x"] = std::to_string(f[2] * 60.0f); at["mVelocity.y"] = std::to_string(f[3] * 60.0f); at["gravity_y"] = std::to_string(f[6] * 3600.0f); at["air_friction"] = std::to_string(f[7] * 60.0f); } - if (ng::RtEntity* te = rt->ent(id)) { // the rotation (00c073b0): along the velocity (velocity_sets_rotation, default on), else turned by angular_velocity per second - bool vsr = true; double av = 0; - for (int pc : rt->components(id, "ProjectileComponent", "", true)) { auto& at = rt->comp(pc)->attrs; vsr = !at.count("velocity_sets_rotation") || at["velocity_sets_rotation"] != "0"; av = at.count("angular_velocity") ? std::atof(at["angular_velocity"].c_str()) : 0.0; } + const auto& projectileComponents = twin ? twin->projectileComponents : localComponents; + const int age = (int)f[10]; + const auto changed = [&](int field) { return !twin || !twin->synced || twin->lastGpu[field] != f[field]; }; + const int ss = (int)f[18], shooter = ss == 1 ? rt->player() : (ss > 1 && ss <= entN ? slotToId[ss - 1] : 0); + auto syncFloat = [](auto& attrs, const char* key, float value) { + const auto it = attrs.find(key); + if (it == attrs.end() || it->second.empty() || std::strtof(it->second.c_str(), nullptr) != value) attrs[key] = std::to_string(value); + }; + for (auto* component : projectileComponents) { + auto& at = component->attrs; + if (changed(4)) syncFloat(at, "damage", f[4]); + component->gpu_lifetime = f[5] >= 1e8f ? -1 : std::max(0, (int)f[5] - age); + component->gpu_lifetime_pending = true; + if ((!twin || !twin->synced) && (!at.count("mStartingLifetime") || at["mStartingLifetime"].empty())) at["mStartingLifetime"] = std::to_string(f[5] >= 1e8f ? -1 : (int)f[5]); + if (changed(8)) syncFloat(at, "config_explosion.explosion_radius", f[8]); + if (changed(9)) syncFloat(at, "config_explosion.damage", f[9]); + if (changed(11)) at["bounces_left"] = std::to_string((int)f[11]); + if (changed(18) || twin->lastShooter != shooter) at["mWhoShot"] = std::to_string(shooter); + if (changed(19)) at["mShooterHerdId"] = std::to_string((int)f[19]); + } + if (changed(2) || changed(3) || changed(6) || changed(7)) for (int vc : rt->components(id, "VelocityComponent", "", true)) { auto& at = rt->comp(vc)->attrs; syncFloat(at, "mVelocity.x", f[2] * 60.0f); syncFloat(at, "mVelocity.y", f[3] * 60.0f); syncFloat(at, "gravity_y", f[6] * 3600.0f); syncFloat(at, "air_friction", f[7] * 60.0f); } + if (te) { // rotation parameters change only when scripts edit the type or component layout + bool vsr = twin ? twin->velocityRotation : true; double av = twin ? twin->angularVelocity : 0; + if (!twin || !twin->synced) for (auto* component : projectileComponents) { auto& at = component->attrs; vsr = !at.count("velocity_sets_rotation") || at["velocity_sets_rotation"] != "0"; av = at.count("angular_velocity") ? std::atof(at["angular_velocity"].c_str()) : 0.0; } if (vsr) { if (f[2] != 0.0f || f[3] != 0.0f) te->rot = std::atan2((double)f[3], (double)f[2]); } else te->rot += av / 60.0; + if (twin) { twin->velocityRotation = vsr; twin->angularVelocity = av; twin->syncRevision = te->component_revision; } } + if (twin) { std::copy(f, f + 24, twin->lastGpu.begin()); twin->lastShooter = shooter; twin->synced = true; } }; auto spawnTwin = [&](const ng::ProjDef* pd, double x, double y) { // the projectile's entity with the shot's extra entities as its children (the engine loads the files of c.extra_entities into the projectile entity) const int t = rt->spawn(pd->file, x, y, 0, false); @@ -2280,6 +2313,19 @@ int main(int argc, char** argv) { } return t; }; + auto addProjectileDamageProxy = [&](int id, int projectileSlot) { + auto* e = rt->ent(id); + const auto damage = rt->components(id, "DamageModelComponent", "", false); + if (!e || damage.empty() || e->gpu >= 0 || entBuilder.layer().ents.size() >= kMaxEnts) return; + const auto* dm = rt->comp(damage.front()); + const ng::EntityState state = ng::projectile_damage_proxy({dm->type, dm->attrs}, e->x, e->y, projectileSlot); + const int slot = (int)entBuilder.layer().ents.size(); + entBuilder.layer().ents.push_back(state); + e->gpu = slot; slotToId[slot] = id; entReadPtr[slot] = state; + deathDone[slot] = 0; propDone[slot] = 0; + entN = (int)entBuilder.layer().ents.size(); + queueUp(g.ents, (VkDeviceSize)slot * sizeof(ng::EntityState), &state, sizeof state); + }; auto twinToGpu = [&](int slot, int field, const ng::RtComponent* c) { // a live field a script wrote: into the GPU record auto num = [&](const char* k, double dv) { auto it = c->attrs.find(k); return it == c->attrs.end() || it->second.empty() ? dv : std::atof(it->second.c_str()); }; const float* f = reinterpret_cast(fprojReadPtr + (size_t)slot * 96); @@ -2471,13 +2517,14 @@ int main(int argc, char** argv) { } ProjTwin tw; tw.slot = k; tw.serial = d[3]; tw.id = twin; tw.sig = twinSig(twin); projTwins.push_back(tw); + ++twinRevision; twinSerial[k] = d[3]; if (std::getenv("NG_SCRIPTLOG")) std::printf("frame %d: lazy twin %d of projectile slot %d serial %d (%s) for tag '%s'%c", rt->frame(), twin, k, d[3], pd->file.c_str(), tag.c_str(), 10); } busy = false; }; rt->on_components_enabled = [&, prevEnabled2 = rt->on_components_enabled](int id) { // a script switched a component of a twin on or off: its type changes - for (ProjTwin& tw : projTwins) if (tw.id == id) { tw.dirty = true; break; } + for (ProjTwin& tw : projTwins) if (tw.id == id) { tw.dirty = true; tw.synced = false; break; } if (prevEnabled2) prevEnabled2(id); }; rt->on_killed = [&, prevKilledX = rt->on_killed](int id) { // ExplosionComponent ON_DEATH: the entity explodes as it dies @@ -2657,18 +2704,26 @@ int main(int argc, char** argv) { patchEntity(re->gpu, offsetof(ng::EntityState, st) + 12, &flags, 4); }; rt->on_value_changed = [&](int id, const std::string& type, const std::string& key) { + if (type == "VariableStorageComponent" || key.rfind("_ng_", 0) == 0) return; ng::RtEntity* re = rt->ent(id); if (type == "Transform") { // a script moved a twin (chain_bolt.lua jumps to the next target): the GPU projectile goes there - for (ProjTwin& tw : projTwins) if (tw.id == id && re) { + if (ProjTwin* found = findTwin(id); found && re) { + ProjTwin& tw = *found; const float* f0 = fprojReadPtr ? reinterpret_cast(fprojReadPtr + (size_t)tw.slot * 96) : nullptr; if (f0) if (const ng::ProjDef* pd = gfx.type_def(reinterpret_cast(f0 + 12)[0])) { // the jump is part of the path: a trail emitter (is_trail, trail_gap) draws // its particles along it as along any motion (00bc75e0: one per trail_gap of the distance) - chain bolt's purple line between its targets - const double ox = f0[0], oy = f0[1], dist = std::hypot(re->x - ox, re->y - oy); + const int trailFrame = rt->frame(); + const double ox = tw.trailFrame == trailFrame ? tw.trailX : f0[0], oy = tw.trailFrame == trailFrame ? tw.trailY : f0[1]; + const double dist = std::hypot(re->x - ox, re->y - oy); + tw.trailFrame = trailFrame; tw.trailX = re->x; tw.trailY = re->y; if (dist > 1.0 && dist < 512.0) for (const ng::EmitterDef& em : pd->fx.emitters) { if (!em.trail || em.trail_gap <= 0.0f || em.real) continue; - const int n = std::min(256, (int)(dist / em.trail_gap)); + // Share the 1024 host requests between live trails. Orbit scripts call both + // SetTransform and ApplyTransform; emitting twice starved new fireballs first. + const int share = std::max(1, std::min(12, 768 / std::max(1, (int)projTwins.size()))); + const int n = std::min(share, (int)(dist / em.trail_gap)); for (int k = 0; k < n; ++k) { - const double t = frand(); + const double t = (k + frand()) / n; const double vx = em.xv0 + (em.xv1 - em.xv0) * frand(), vy = em.yv0 + (em.yv1 - em.yv0) * frand(); emitPart(ox + (re->x - ox) * t, oy + (re->y - oy) * t, vx, vy, em.life0 + (em.life1 - em.life0) * frand(), em.gravity, em.color, 4, 0, em.collide, em.force, em.alpha, em.glow, em.fade); } @@ -2679,14 +2734,16 @@ int main(int argc, char** argv) { // at it (proj_step.comp draw_sprite; -scale in e.x, which only the thrown bodies use) const float ns = -(float)std::max(0.01, std::fabs(re->sx)); queueUp(g.fxState, 16 + (VkDeviceSize)tw.slot * 96 + 64, &ns, 4); } - break; + return; } } if (type == "DamageModelComponent" && key == "kill_now") { // DamageModelComponent kill_now on a twin (vacuum_powder.lua): the entity dies, its projectile with it - for (ProjTwin& tw : projTwins) if (tw.id == id) { killQueue.push_back(id); return; } + if (findTwin(id)) { killQueue.push_back(id); return; } } - for (ProjTwin& tw : projTwins) if (tw.id == id) { // a script changed a projectile's entity (black_hole_gravity.lua the velocity, SPELLS_TO_POWER the collisions and lifetime...): the GPU projectile follows + if (ProjTwin* found = findTwin(id)) { // a script changed a projectile's entity: the GPU projectile follows + ProjTwin& tw = *found; const int fld = twinLiveField(type, key); + if (fld != -2) tw.synced = false; if (fld == -1) tw.dirty = true; // a field of the type: a new type after the scripts of this frame else if (fld >= 0) { const auto cs = rt->components(id, type, "", true); if (!cs.empty()) twinToGpu(tw.slot, fld, rt->comp(cs.front())); } return; @@ -2773,8 +2830,11 @@ int main(int argc, char** argv) { // an entity without a ProjectileComponent stays the entity the script loaded (wand_pickup.lua / orb_pickup.lua shoot the image emitter wand_effect.xml: as a GPU // projectile it sprayed sparks from one point; the host's emitters draw its image) if (rt->components(entityId, "ProjectileComponent", "", true).empty()) return; - pendingShots.push_back({pe->file, shooter, x, y, tx, ty, send}); - rt->kill(entityId); + deferredProjectileSlots.erase(entityId); + pendingShots.push_back({pe->file, shooter, x, y, tx, ty, send, entityId}); + // GameShootProjectile keeps the EntityLoad ID: orbit scripts attach and move it after this call. + if (pe->gpu >= 0) { rigid.remove_slot(pe->gpu); hideSlot(pe->gpu); slotToId[pe->gpu] = 0; } + if (auto* retained = rt->ent(entityId)) retained->gpu = -1; }; if (areaStore) rt->biome_fn = [&](double x, double y) -> std::string { // BiomeMapGetName: the biome of the tile (the map origin is tile (35, 14) at world 0,0) const ng::BiomeDef* bd = areaStore->biome_map().biome_at((int)std::floor(x / 512.0) + 35, (int)std::floor(y / 512.0) + 14); @@ -2791,6 +2851,12 @@ int main(int argc, char** argv) { if (std::getenv("NG_KILLTRACE")) { ng::RtEntity* dbg = rt->ent(id); std::fprintf(stderr, "on_spawned %d %s%c", id, dbg ? dbg->file.c_str() : "?", 10); } ng::RtEntity* re = rt->ent(id); if (!re) return; + // EntityLoad followed by GameShootProjectile needs only the projectile slot. + // Wait until script shots are collected before allocating an ordinary sprite/body slot. + if (!realizingProjectileSlots && re->gpu < 0 && !rt->components(id, "ProjectileComponent", "", true).empty()) { + deferredProjectileSlots.insert(id); + return; + } for (int sc : rt->components(id, "SpriteComponent", "", true)) { auto& sa = rt->comp(sc)->attrs; if (sa["additive"] == "1" && sa["kill_entity_after_finished"] == "1") { pendingFlares.push_back(id); break; } @@ -5531,6 +5597,13 @@ int main(int argc, char** argv) { } }; auto twinSystems = [&](ProjTwin& tw, ng::RtEntity* te) { // the components of a projectile twin the GPU does not simulate, one frame + if (tw.hostRevision != te->component_revision) { + static const std::set hostTypes = {"ExplosionComponent", "MaterialSuckerComponent", "BlackHoleComponent", "MaterialSeaSpawnerComponent", "LightningComponent", "LaserEmitterComponent", "AreaDamageComponent", "GameAreaEffectComponent", "ArcComponent", "CellEaterComponent", "MagicConvertMaterialComponent"}; + tw.hasHostSystems = false; + for (const auto& [type, ids] : te->comps_by_type) if (hostTypes.count(type)) { tw.hasHostSystems = true; break; } + tw.hostRevision = te->component_revision; + } + if (!tw.hasHostSystems) return; for (int xc : rt->components(tw.id, "ExplosionComponent", "", true)) { // ExplosionComponent ON_TIMER: timeout_frames + random(0, timeout_frames_random) after it was made, the explosion (00ae78e0) auto& at = rt->comp(xc)->attrs; if (at["trigger"] != "ON_TIMER") continue; @@ -7242,17 +7315,25 @@ int main(int argc, char** argv) { std::printf("GPU Lua complete frame %u slot %d serial %u fault %u gravity %.7g position %.3f %.3f%c", q[1], k, q[0], q[3], p[6] * 3600.0f, p[0], p[1], 10); } if (rt && psim) { // the twins of the GPU projectiles: their position follows the projectile (the scripts read it), a dead projectile ends its twin; then the components the GPU does not simulate - for (size_t ti = 0; ti < projTwins.size();) { + PERFMARK("projectile twin sync"); + bool removeTwins = false; + for (size_t ti = 0; ti < projTwins.size(); ++ti) { ProjTwin& tw = projTwins[ti]; const float* f = reinterpret_cast(fprojReadPtr + (size_t)tw.slot * 96); const int32_t* d = reinterpret_cast(f + 12); ng::RtEntity* te = rt->ent(tw.id); - if (!te || !te->alive || d[3] != tw.serial) { if (te && te->alive) { twinSystems(tw, te); rt->kill(tw.id); } projTwins.erase(projTwins.begin() + ti); continue; } // (the components run on the death frame too: the engine updates them before the entity goes) + if (!te || !te->alive || d[3] != tw.serial) { if (te && te->alive) { twinSystems(tw, te); rt->kill(tw.id); } tw.pendingRemoval = true; removeTwins = true; continue; } // components run on the death frame too te->x = f[0]; te->y = f[1]; - twinFromGpu(tw.id, f); + if (te->gpu > 0) { + entReadPtr[te->gpu].pos[0] = f[0]; entReadPtr[te->gpu].pos[1] = f[1]; + patchEntity(te->gpu, offsetof(ng::EntityState, pos), f, 8); + } + twinFromGpu(tw.id, f, &tw); + PERFMARK("projectile host components"); twinSystems(tw, te); - ++ti; + PERFMARK("projectile twin sync"); } + if (removeTwins) { std::erase_if(projTwins, [](const ProjTwin& tw) { return tw.pendingRemoval; }); ++twinRevision; } } if (rt && cellsReadPtr && cellsViewX == viewX && cellsViewY == viewY) { PERFMARK("loose ground"); @@ -9334,9 +9415,8 @@ int main(int argc, char** argv) { } } if (rt) { // the AreaDamageComponents of the other entities (not projectiles) in the loaded area - std::set twinIds; for (const ProjTwin& tw : projTwins) twinIds.insert(tw.id); for (int aid : rt->with_component("AreaDamageComponent")) { - if (twinIds.count(aid)) continue; + if (findTwin(aid)) continue; ng::RtEntity* ae = rt->ent(aid); if (!ae || !ae->alive || std::fabs(ae->x - (viewX + kGridW * 0.5)) > kGridW || std::fabs(ae->y - (viewY + kGridH * 0.5)) > kGridH) continue; areaDamageOf(aid, ae); @@ -9824,10 +9904,9 @@ int main(int argc, char** argv) { emitFrom(rt->player(), rt->player()); } { // a projectile entity a script loaded (EntityLoad, not shot: lantern_death.xml's sparks) has no GPU projectile: its ProjectileComponent lifetime ends it here - std::set twinIds; for (const ProjTwin& tw : projTwins) twinIds.insert(tw.id); for (int eid : rt->with_component("ProjectileComponent")) { const ng::RtEntity* pe = rt->ent(eid); - if (!pe || !pe->alive || twinIds.count(eid)) continue; + if (!pe || !pe->alive || findTwin(eid)) continue; const bool body = pe->gpu > 0 && pe->gpu < entN && entReadPtr && entReadPtr[pe->gpu].st[0] == 4; // (a thrown physics projectile a script loaded: tnt.xml, a slot of its own) if (pe->gpu >= 0 && !body) continue; const auto pcs = rt->components(eid, "ProjectileComponent", "", true); @@ -9851,6 +9930,7 @@ int main(int argc, char** argv) { } } { // the emitters of the entities without a GPU slot (spawned by scripts: workshop_areadamage.xml's red sparks...) near the camera; children of an entity follow it above + const auto imageOwners = rt->with_component_field("ParticleEmitterComponent", "image_animation_file"); for (int eid : rt->with_component("ParticleEmitterComponent")) { const ng::RtEntity* ee = rt->ent(eid); if (!ee || !ee->alive || eid == rt->player() || seenEff.count(eid)) continue; // (every entity in the world, those with a GPU slot too: portals, creatures, props, items) @@ -9859,7 +9939,8 @@ int main(int argc, char** argv) { if (rt->alive(ee->parent)) emitFrom(ee->parent, eid); continue; } - bool twin = false; for (const ProjTwin& tw : projTwins) if (tw.id == eid) { twin = true; break; } + const bool twin = findTwin(eid) != nullptr; + if (twin && !std::binary_search(imageOwners.begin(), imageOwners.end(), eid)) continue; emitFrom(eid, eid, twin); // (a twin: only its image_animation_file emitters - SUMMON_WANDGHOST's wand_effect.xml; effects.cpp leaves them to the host) } } @@ -9868,13 +9949,46 @@ int main(int argc, char** argv) { for (int kid : killQueue) rt->kill(kid); killQueue.clear(); for (const DeferredBoom& b : hostExplodeDeferred) hostExplode(b.cfg, b.x, b.y, b.who); + PERFMARK("energy shields"); if (rt && psim) for (int sid : rt->with_component("EnergyShieldComponent")) { // 00adefd0: the shield recharges (recharge_speed per second up to max_energy) and works from energy_required_to_shield; // a projectile of another herd (with damage) that crossed into the ring of radius inside the sector is reflected about the ring (random turn +-0.04 rad), put on the // ring, taken over by the shield's owner (shooter, herd), and costs damage_multiplier x its damage of energy + const auto* shieldEntity = rt->ent(sid); + const bool noBody = shieldEntity && !shieldEntity->parent && shieldEntity->gpu < 0 && sid != rt->player(); + if (auto* twin = findTwin(sid)) { + if (twin->shieldLayoutRevision != shieldEntity->component_revision) { + const auto shields = rt->components(sid, "EnergyShieldComponent", "", true); + twin->shieldComponent = shields.size() == 1 ? shields.front() : 0; + twin->shieldLayoutRevision = shieldEntity->component_revision; + twin->shieldValueRevision = uint64_t(-1); + } + if (noBody && twin->shieldComponent) { + const auto* shield = rt->comp(twin->shieldComponent); + if (shield && !shield->enabled) continue; + if (shield && twin->shieldValueRevision != shield->value_revision) { + const auto number = [&](const char* key, double fallback) { const auto at = shield->attrs.find(key); return at == shield->attrs.end() || at->second.empty() ? fallback : std::atof(at->second.c_str()); }; + twin->shieldNoOp = number("energy", 0) == number("max_energy", 1) && number("recharge_speed", 1) >= 0; + twin->shieldValueRevision = shield->value_revision; + } + if (shield && twin->shieldNoOp) continue; + } + // Host recharge/reflection can change energy without a script setter. + twin->shieldValueRevision = uint64_t(-1); + } for (int ec : rt->components(sid, "EnergyShieldComponent", "", false)) { if (!rt->component_enabled(ec)) continue; auto& at = rt->comp(ec)->attrs; - auto num = [&](const char* k, double dv) { auto it = at.find(k); return it == at.end() || it->second.empty() ? dv : std::atof(it->second.c_str()); }; + auto num = [&](const char* k, double dv) { + const auto it = at.find(k); + if (it == at.end() || it->second.empty()) return dv; + double value; + const auto parsed = std::from_chars(it->second.data(), it->second.data() + it->second.size(), value); + return parsed.ec == std::errc() && parsed.ptr == it->second.data() + it->second.size() ? value : std::atof(it->second.c_str()); + }; + if (noBody) { + const auto energy = at.find("energy"), maximum = at.find("max_energy"); + if (energy != at.end() && maximum != at.end() && !energy->second.empty() && energy->second == maximum->second && num("recharge_speed", 1) >= 0) continue; + } double energy = std::min(num("max_energy", 1), num("energy", 0) + num("recharge_speed", 1) / 60.0); const bool active = energy >= num("energy_required_to_shield", 0.2); int root = sid; while (rt->parent(root)) root = rt->parent(root); @@ -9913,10 +10027,11 @@ int main(int argc, char** argv) { } at["mPrevPosition.x"] = std::to_string(cx); at["mPrevPosition.y"] = std::to_string(cy); } - at["energy"] = std::to_string(energy); + if (energy != num("energy", 0)) at["energy"] = std::to_string(energy); } } hostExplodeDeferred.clear(); + PERFMARK("explosion flares"); // the explosion flares (explosion_flare*.xml: SpriteComponent additive, alpha 0.2, a soft disc scaled 5-10 times by the Transform, played once): an additive // radial light of the sprite's colour over its animation (FAnim slots 256..511 belong to the host) for (int fid : pendingFlares) { @@ -9950,6 +10065,7 @@ int main(int argc, char** argv) { } } pendingFlares.clear(); + PERFMARK("fog sources"); { // the fog of war holes (SpriteComponent fog_of_war_hole: "the alpha channel of this texture punctures a hole in the fog of war"): the explosion flares' // holes (scale 15, alpha 0.55), torches, wraiths, the sun items: the alpha of their sprite (its frame of the moment) x the component's alpha, scaled; render.comp: fog x (1 - it) const size_t hb = 1024 + 9 * db.materials().size() + 132 + db.materials().size() + 2 + 8192; @@ -9974,7 +10090,7 @@ int main(int argc, char** argv) { }; std::vector holes(1 + 64 * 6, 0.0f); int nh = 0; - for (int hid : rt->with_component("SpriteComponent")) { + for (int hid : rt->with_component_field("SpriteComponent", "fog_of_war_hole", "1")) { if (nh >= 64) break; const ng::RtEntity* he = rt->ent(hid); if (!he || !he->alive) continue; @@ -10016,6 +10132,7 @@ int main(int argc, char** argv) { queueUp(g.tables, (VkDeviceSize)sb * 4, src.data(), (1 + (size_t)ns * 3) * 4); } if (std::getenv("NG_SCRIPTLOG") && !rt->script_errors().empty()) { for (const std::string& er : rt->script_errors()) std::printf("frame %d: script error: %s%c", frame, er.substr(0, 300).c_str(), 10); rt->clear_script_errors(); } + PERFMARK("projectile retypes"); if (rt && psim) for (ProjTwin& tw : projTwins) if (tw.dirty) { // the twins whose type fields the scripts changed this frame: the projectile takes the type of the changed copy tw.dirty = false; const std::string sig = twinSig(tw.id); @@ -10076,14 +10193,35 @@ int main(int argc, char** argv) { if (pid2) rt->on_shoot(rt->player(), sx0, sy0, sx0 + tdx, sy0 + tdy, pid2, true); } for (const ScriptShot& ss : pendingShots) { // the shots of scripts: speed from the aim vector (the scripts pass the target as position + velocity, in pixels per second), the shooter is credited with the hits - auto it = scriptShotNode.find(ss.file); - if (it == scriptShotNode.end() && psim) it = scriptShotNode.emplace(ss.file, gfx.persistent_node(*psim, ss.file)).first; - if (it == scriptShotNode.end() || it->second < 0) continue; + if (ss.entity && !rt->alive(ss.entity)) continue; + std::string file = ss.file; + if (ss.entity && psim) { + ng::EntityDef ed; ed.path = ss.file; + for (const auto& tag : rt->ent(ss.entity)->tags) ed.tags += (ed.tags.empty() ? "" : ",") + tag; + for (int cid : rt->ent(ss.entity)->comps) if (const auto* c = rt->comp(cid); c && rt->component_enabled(cid)) { + ed.comps.push_back({c->type, c->attrs}); + } + file = ng::ProjectileSim::script_definition_key(ed); + psim->def_from(file, ed); + } + auto it = scriptShotNode.find(file); + if (it == scriptShotNode.end() && psim) it = scriptShotNode.emplace(file, gfx.persistent_node(*psim, file)).first; + if (it == scriptShotNode.end() || it->second < 0) { if (ss.entity) rt->kill(ss.entity); continue; } int slot = -1; if (ss.shooter == rt->player()) slot = 0; else for (int k = 1; k < entN && slot < 0; ++k) if (slotToId[k] == ss.shooter) slot = k; if (std::getenv("NG_SCRIPTLOG")) std::printf("frame %d: script shot %s by entity %d (slot %d) node %d herd %d%c", frame, ss.file.c_str(), ss.shooter, slot, it->second, slot >= 0 ? entReadPtr[slot].ex8[1] : -1, 10); - gfx.shoot(frame, ss.x, ss.y, (ss.tx - ss.x) / 60.0, (ss.ty - ss.y) / 60.0, it->second, ss.shooter == rt->player() ? 1 : 2, slot + 1, slot >= 0 ? entReadPtr[slot].ex8[1] : 0, !ss.send); // (send_message false: as if the script_shot scripts had run) + if (!gfx.shoot(frame, ss.x, ss.y, (ss.tx - ss.x) / 60.0, (ss.ty - ss.y) / 60.0, it->second, ss.shooter == rt->player() ? 1 : 2, slot + 1, slot >= 0 ? entReadPtr[slot].ex8[1] : 0, !ss.send, ss.entity) && ss.entity) rt->kill(ss.entity); + } + if (!deferredProjectileSlots.empty()) { + const auto deferred = std::move(deferredProjectileSlots); + deferredProjectileSlots.clear(); + realizingProjectileSlots = true; + const bool wasRespawning = respawning; + respawning = true; // the initial notification already played the spawn sound + for (int id : deferred) if (const auto* e = rt->ent(id); e && e->alive && e->gpu < 0) rt->on_spawned(id); + respawning = wasRespawning; + realizingProjectileSlots = false; } if (const char* mb = std::getenv("NG_MATBOX")) if (cellsReadPtr) { // test NG_MATBOX=x0:y0:x1:y1:every - the materials of a box of world cells (all but air) int x0 = 0, y0 = 0, x1 = 0, y1 = 0, ev = 30; @@ -10215,7 +10353,10 @@ int main(int argc, char** argv) { if (activeItem < 0 && gun->equipped()) for (int k = 0; k < ng::Inventory::kWands; ++k) if (inv.wands[k] == gun->equipped()) activeWand = k; // the wand in the hand stays in the hand when it is moved to another slot for (int& w : inv.items) if (w && (!rt->ent(w) || !rt->ent(w)->alive)) w = 0; for (int& w : inv.full) if (w && (!rt->ent(w) || !rt->ent(w)->alive)) w = 0; - if (!inv.wands[activeWand]) for (int k = 0; k < ng::Inventory::kWands; ++k) if (inv.wands[k]) { activeWand = k; gun->equip(inv.wands[k]); break; } + if (!inv.wands[activeWand]) { + for (int k = 0; k < ng::Inventory::kWands; ++k) if (inv.wands[k]) { activeWand = k; break; } + } + if (gun->equipped() != inv.wands[activeWand]) gun->equip(inv.wands[activeWand]); const bool selNew = g_in.select_new; g_in.select_new = false; const int wheelSteps = g_in.wheel; g_in.wheel = 0; const int selBefore = activeItem >= 0 ? 4 + activeItem : activeWand; @@ -10590,7 +10731,7 @@ int main(int argc, char** argv) { } } } - if (wantFire && playMode && aimXc >= 0 && gun->equipped() && activeItem < 0) { + if (wantFire && playMode && aimXc >= 0 && gun->equipped() && gun->equipped() == inv.wands[activeWand] && activeItem < 0) { ng::ShotResult shot; if (gun->fire(frame, shot)) { ++shotsFired; @@ -11244,7 +11385,7 @@ int main(int argc, char** argv) { const int uiL = std::max(1, (int)std::lround(curW * 2.8 / 2000.0)), tsL = std::max(1, uiL * 2 / 3); PERFMARK("text sprites, panels"); { // the text sprites of the world (SpriteComponent is_text_sprite: the shop prices in font_pixel_white), at the entity minus the sprite offset, one font pixel per world pixel - for (int tid : rt->with_component("SpriteComponent")) { + for (int tid : rt->with_component_field("SpriteComponent", "is_text_sprite", "1")) { const ng::RtEntity* te = rt->ent(tid); if (!te || !te->alive || te->parent) continue; for (int cc : rt->components(tid, "ItemCostComponent", "", true)) { // ItemCostSystem (00b8f9f0): a changed cost is written into the text of the sprite tagged shop_cost @@ -11686,7 +11827,7 @@ int main(int argc, char** argv) { lastFrameT = nowT; if (frame > 5 && std::getenv("NG_FRAMESTAT") && dt > 40.0) { std::printf("slow frame %d: %.1f ms (region step %.1f ms, spawns %.1f ms, gpu %.1f ms)", frame, dt, lastPollMs, lastSpawnMs, lastGpuRaw); if (false) { auto d = [&](int x, int y) { return std::chrono::duration(cpuT[y] - cpuT[x]).count(); }; std::printf(" (input+camera %.1f, game logic %.1f, HUD %.1f)", d(0, 1), d(1, 2), d(2, 3)); } std::putchar(10); } frameMs = frameMs == 0 ? dt : frameMs * 0.9 + dt * 0.1; - if (std::getenv("NG_PERFLOG") && frame % 60 == 0) { int lp = 0; if (fprojReadPtr) for (int k = 0; k < ng::GpuFx::kMaxProj; ++k) lp += reinterpret_cast(fprojReadPtr + (size_t)k * 96 + 48)[3] != 0; std::printf("perf frame %d: %.1f ms per frame, gpu %.1f ms, entities %d, runtime %d, projectiles %d%c", frame, frameMs, gpuMs, entN, rt ? (int)rt->with_tag("").size() : 0, lp, 10); } // test: the frame time over a run + if (std::getenv("NG_PERFLOG") && frame % 60 == 0) { int lp = 0; if (fprojReadPtr) for (int k = 0; k < ng::GpuFx::kMaxProj; ++k) lp += reinterpret_cast(fprojReadPtr + (size_t)k * 96 + 48)[3] != 0; std::printf("perf frame %d: %.1f ms per frame, gpu %.1f ms, entities %d, runtime %d, projectiles %d, native scripts %ld%c", frame, frameMs, gpuMs, entN, rt ? (int)rt->with_tag("").size() : 0, lp, rt ? rt->native_script_runs() : 0, 10); } // test: the frame time over a run } hs.gpu_ms = gpuMs; hs.frame_ms = frameMs; hs.sim_speed = simSpeed; hs.show_timing = !worldMode; hs.fps = lastFps; hs.entities = entN; hs.stored_tiles = (int)store.tiles(); hs.frame = frame; hs.seed_text = seedText.c_str(); @@ -12153,8 +12294,13 @@ int main(int argc, char** argv) { if (std::getenv("NG_SCRIPTLOG")) std::printf("frame %d: cast creature %s -> entity %d at (%.0f, %.0f)%c", frame, pd->file.c_str(), cid, e[0], e[1], 10); continue; } - const int twin = spawnTwin(pd, e[0], e[1]); - if (!twin) continue; + const int retained = e2[3] < 0.0f ? -(int)e2[3] : 0; + const int twin = retained ? (rt->alive(retained) ? retained : 0) : spawnTwin(pd, e[0], e[1]); + if (!twin) { + // Its parent may have died while the spawn event was in flight. + if (retained) { const int32_t* state = reinterpret_cast(fprojReadPtr + (size_t)e[3] * 96 + 48); if (state[3] == (int)e2[0]) { const int32_t flags = state[2] | (1 << 29); queueUp(g.fxState, 16 + (VkDeviceSize)e[3] * 96 + 56, &flags, 4); } } + continue; + } const int sslot = (int)e2[2]; const int shooterId = sslot == 1 ? rt->player() : (sslot > 1 && sslot <= entN ? slotToId[sslot - 1] : 0); for (int pcid : rt->components(twin, "ProjectileComponent", "", true)) rt->comp(pcid)->attrs["mWhoShot"] = std::to_string(shooterId); @@ -12169,6 +12315,8 @@ int main(int argc, char** argv) { } castTwins[shooterId].push_back(twin); // (the projectiles of one cast: their arcs are linked below) // (the type's fields as the file has them: what the on-added scripts change makes a new type) projTwins.push_back(tw); + ++twinRevision; + addProjectileDamageProxy(twin, tw.slot); if (std::getenv("NG_SCRIPTLOG")) std::printf("frame %d: twin %d of projectile slot %d serial %d (%s)%c", frame, twin, tw.slot, tw.serial, pd->file.c_str(), 10); if (ng::RtEntity* te0 = rt->ent(twin)) twinSystems(projTwins.back(), te0); // the spawn frame counts (the engine updates the components the frame the projectile is made) continue; @@ -12214,7 +12362,7 @@ int main(int argc, char** argv) { if (e[2] == -5.0f && k + 1 < std::min(head, 512)) { // the projectile of a twin died const float* e2 = xe + 4 * (k + 1); ++k; - for (size_t ti = 0; ti < projTwins.size(); ++ti) if (projTwins[ti].slot == (int)e[3] && projTwins[ti].serial == (int)e2[0]) { if (ng::RtEntity* td = rt->ent(projTwins[ti].id)) { td->x = e[0]; td->y = e[1]; } rt->kill(projTwins[ti].id); projTwins.erase(projTwins.begin() + ti); break; } + for (size_t ti = 0; ti < projTwins.size(); ++ti) if (projTwins[ti].slot == (int)e[3] && projTwins[ti].serial == (int)e2[0]) { if (ng::RtEntity* td = rt->ent(projTwins[ti].id)) { td->x = e[0]; td->y = e[1]; } rt->kill(projTwins[ti].id); projTwins.erase(projTwins.begin() + ti); ++twinRevision; break; } continue; } if (e[2] == -3.0f && k + 1 < std::min(head, 512)) { // Message_Shot: a shot of a shooter with script_shot scripts (two entries: position / type, then angle, speed, base angle, shooter slot + 1 | team << 20) @@ -12909,12 +13057,32 @@ int main(int argc, char** argv) { uint8_t* mp = nullptr; VK(vkMapMemory(g.dev, g.stagingFrame.mem, 0, VK_WHOLE_SIZE, 0, (void**)&mp)); size_t used = 0; + std::vector copies; + copies.reserve(queuedUploads.size()); + const Buffer* batchDestination = nullptr; + VkDeviceSize previousEnd = 0; + std::set writtenBuffers; + auto flushCopies = [&] { + if (copies.empty()) return; + if (writtenBuffers.count(batchDestination)) { + const VkMemoryBarrier ordered{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT}; + vkCmdPipelineBarrier(g.cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &ordered, 0, nullptr, 0, nullptr); + } + vkCmdCopyBuffer(g.cmd, g.stagingFrame.buf, batchDestination->buf, (uint32_t)copies.size(), copies.data()); + writtenBuffers.insert(batchDestination); + copies.clear(); + }; for (const QueuedUpload& q : queuedUploads) { std::memcpy(mp + used, q.data.data(), q.data.size()); - const VkBufferCopy bc{used, q.off, q.data.size()}; - vkCmdCopyBuffer(g.cmd, g.stagingFrame.buf, q.dst->buf, 1, &bc); // (in order: a later piece over the same bytes wins, as before) + // Consecutive, non-overlapping regions can share one Vulkan command. + // A destination change or backwards patch ends the batch, retaining order. + if (q.dst != batchDestination || q.off < previousEnd) flushCopies(); + batchDestination = q.dst; + copies.push_back({used, q.off, q.data.size()}); + previousEnd = q.off + q.data.size(); used += q.data.size(); } + flushCopies(); vkUnmapMemory(g.dev, g.stagingFrame.mem); queuedUploads.clear(); const VkMemoryBarrier ub{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT}; diff --git a/src/app/perf.h b/src/app/perf.h index 2212b37..6539fa8 100644 --- a/src/app/perf.h +++ b/src/app/perf.h @@ -14,7 +14,7 @@ namespace ng { class PerfRecorder { public: - static constexpr int kMaxSections = 32, kGpu = 20, kCounts = 12; + static constexpr int kMaxSections = 64, kGpu = 20, kCounts = 12; struct Frame { int frame = 0; float total = 0, wait = 0, gpu = 0; // whole frame (wall time from one frame to the next), waiting for the GPU / the swap chain, the GPU's own time diff --git a/src/core/wak.cpp b/src/core/wak.cpp index 5a382cb..ffe5150 100644 --- a/src/core/wak.cpp +++ b/src/core/wak.cpp @@ -74,7 +74,7 @@ namespace { std::string lower_path(std::string_view p) { std::string l(p); for (char& c : l) c = (char)std::tolower((unsigned char)c); return l; } } -void Wak::set_override(std::string_view path, std::vector bytes) const { overrides_[lower_path(path)] = std::move(bytes); } +void Wak::set_override(std::string_view path, std::vector bytes) const { overrides_[lower_path(path)] = std::move(bytes); ++override_revision_; } bool Wak::overridden(std::string_view path) const { return overrides_.count(lower_path(path)) != 0; } // Loose files beside the archive: the mods (mods//...) and the files of data/ the archive does not hold (the install ships data/fonts/font_damage.xml, diff --git a/src/core/wak.h b/src/core/wak.h index 1a83029..844c9fb 100644 --- a/src/core/wak.h +++ b/src/core/wak.h @@ -30,6 +30,7 @@ public: // Mods: ModTextFileSetContent / ModImageMakeEditable replace a file for every later read; paths under mods/ are loose files below the game folder. void set_override(std::string_view path, std::vector bytes) const; bool overridden(std::string_view path) const; + uint64_t override_revision() const { return override_revision_; } void set_loose_root(const std::filesystem::path& root) const { loose_root_ = root; } const std::filesystem::path& loose_root() const { return loose_root_; } bool exists(std::string_view path) const; // in the archive, overridden, or a loose mods/ file @@ -39,6 +40,7 @@ private: std::vector entries_; std::unordered_map index_; mutable std::unordered_map> overrides_; // by lower-case path + mutable uint64_t override_revision_ = 0; mutable std::filesystem::path loose_root_; bool read_loose(std::string_view path, std::vector& out) const; std::unordered_map lower_; // the same paths in lower case: the files are looked up without regard to case, as on the game's file system (boss_limbs.xml asks for limb1_A.png, the archive has limb1_a.png) diff --git a/src/entity/runtime.cpp b/src/entity/runtime.cpp index 9896b99..8960ce2 100644 --- a/src/entity/runtime.cpp +++ b/src/entity/runtime.cpp @@ -3,6 +3,8 @@ #include "entity/herds.h" #include +#include +#include #include #include #include @@ -39,6 +41,8 @@ bool is_bool_key(const std::string& k) { bool parse_number(const std::string& s, double& out) { if (s.empty()) return false; + const auto parsed = std::from_chars(s.data(), s.data() + s.size(), out); + if (parsed.ec == std::errc() && parsed.ptr == s.data() + s.size()) return true; char* end = nullptr; out = std::strtod(s.c_str(), &end); return end && *end == 0; @@ -46,8 +50,11 @@ bool parse_number(const std::string& s, double& out) { std::string number_string(double v) { char b[64]; - if (std::floor(v) == v && std::fabs(v) < 1e15) std::snprintf(b, sizeof b, "%lld", (long long)v); - else std::snprintf(b, sizeof b, "%.9g", v); + const auto text = std::floor(v) == v && std::fabs(v) < 1e15 + ? std::to_chars(b, b + sizeof b, (long long)v) + : std::to_chars(b, b + sizeof b, v, std::chars_format::general, 9); + if (text.ec == std::errc()) return std::string(b, text.ptr); + std::snprintf(b, sizeof b, "%.9g", v); return b; } @@ -116,7 +123,10 @@ RtEntity* EntityRuntime::ent(int id) { RtComponent* EntityRuntime::comp(int id) { auto it = comps_.find(id); - return it == comps_.end() ? nullptr : &it->second; + if (it == comps_.end()) return nullptr; + RtComponent& c = it->second; + if (c.gpu_lifetime_pending) { c.attrs["lifetime"] = std::to_string(c.gpu_lifetime); c.gpu_lifetime_pending = false; } + return &c; } int EntityRuntime::add_comp_internal(int entity, const std::string& type, std::map attrs) { @@ -133,9 +143,35 @@ int EntityRuntime::add_comp_internal(int entity, const std::string& type, std::m c.attrs = std::move(attrs); comps_[c.id] = c; e->comps.push_back(c.id); + e->component_revision = ++component_revision_; + e->comps_by_type[type].push_back(c.id); + if (type == "LuaComponent") { ++e->lua_component_count; e->single_lua_component = e->lua_component_count == 1 ? c.id : 0; } + else if (type == "CollisionTriggerComponent") ++e->collision_component_count; + ++entities_by_component_[type][entity]; + update_component_fields(c, 1); return c.id; } +void EntityRuntime::erase_comp_internal(int cid) { + RtComponent* c = comp(cid); + if (!c) return; + update_component_fields(*c, -1); + auto& owners = entities_by_component_[c->type]; + auto owner = owners.find(c->entity); + if (owner != owners.end() && --owner->second == 0) owners.erase(owner); + if (RtEntity* e = ent(c->entity)) { + e->component_revision = ++component_revision_; + auto& typed = e->comps_by_type[c->type]; + typed.erase(std::remove(typed.begin(), typed.end(), cid), typed.end()); + if (c->type == "LuaComponent") { e->lua_component_count = (int)typed.size(); e->single_lua_component = typed.size() == 1 ? typed.front() : 0; } + else if (c->type == "CollisionTriggerComponent") e->collision_component_count = (int)typed.size(); + if (typed.empty()) e->comps_by_type.erase(c->type); + e->comps.erase(std::remove(e->comps.begin(), e->comps.end(), cid), e->comps.end()); + } + comps_.erase(cid); + lua_schedules_.erase(cid); +} + void EntityRuntime::regen_item_actions(int player) { for (auto& [id, e] : ents_) { if (!e.alive || components(id, "ItemActionComponent", "", true).empty()) continue; @@ -191,6 +227,8 @@ int EntityRuntime::spawn(const std::string& path, double x, double y, int forced e.name = def->name; for (auto& t : split(def->tags, ',')) e.tags.insert(t); } + if (RtEntity* previous = ent(id)) for (int cid : std::vector(previous->comps)) erase_comp_internal(cid); + native_guard_states_.erase(id); ents_[id] = e; if (spawn_log) spawn_log->push_back(id); bool has_item = false; @@ -364,14 +402,95 @@ static std::string lua_vm_key(RtComponent* c, int comp_id, const std::string& fi return "f" + file; } +bool EntityRuntime::run_native_guard(int entity, const std::string& file) { + static const std::string guard = "data/scripts/projectiles/magic_shield_part.lua"; + static const std::string utilities = "data/scripts/lib/utilities.lua"; + if (!native_projectile_scripts || file != guard || !host_.has_cached_file(guard)) return false; + // The first invocation runs Lua (and dofile_once) normally. Only the exact + // reviewed source and unchanged API bindings can use this implementation. + if (native_source_revision_ != wak_.override_revision()) { + auto matches = [&](const std::string& path, uint64_t expected) { + std::vector bytes; + if (!wak_.read(path, bytes)) return false; + uint64_t hash = 14695981039346656037ull; + for (uint8_t b : bytes) if (b != 13) hash = (hash ^ b) * 1099511628211ull; + return hash == expected; + }; + native_guard_source_ = matches(guard, 10701123369528421423ull) && matches(utilities, 16046592235890916284ull); + native_source_revision_ = wak_.override_revision(); + } + if (!native_guard_source_) return false; + if (native_api_revision_ != host_.execution_revision() || native_guard_env_ != host_.env) { + native_guard_api_ = host_.uses_original_api({"dofile_once", "GetUpdatedEntityID", "EntityGetComponent", "ipairs", "ComponentGetValue", "ComponentGetValue2", "ComponentSetValue2", "EntityGetTransform", "EntitySetTransform", "EntityKill", "math.cos", "math.sin"}); + for (const auto* path : {&guard, &utilities}) { + const auto appended = LuaHost::mod_appends().find(*path); + if (appended != LuaHost::mod_appends().end() && !appended->second.empty()) native_guard_api_ = false; + } + native_api_revision_ = host_.execution_revision(); + native_guard_env_ = host_.env; + } + if (!native_guard_api_) return false; + auto& state = native_guard_states_[entity]; + const auto* guardEntity = ent(entity); + if (!guardEntity) return false; + if (state.revision != guardEntity->component_revision) { + state = NativeGuardState{}; + const auto vars = components(entity, "VariableStorageComponent", "", false); + state.has_storage = !vars.empty(); + for (int cid : vars) { + const auto& attrs = comp(cid)->attrs; + const auto name = attrs.find("name"); + if (name == attrs.end()) continue; + const bool isAngle = name->second == "angle"; + const auto value = attrs.find(isAngle ? "value_float" : "value_int"); + if (value == attrs.end()) return false; + if (isAngle) { state.angle = &value->second; state.angle_component = cid; } + else if (name->second == "rot") state.rotation = &value->second; + else if (name->second == "owner") state.owner = &value->second; + } + state.revision = guardEntity->component_revision; + } + double angle = 0; + if (state.angle && (!parse_number(*state.angle, angle) || !std::isfinite(angle))) return false; + auto cachedNumber = [&](const std::string* source, std::string& text, double& value) { + if (!source) { value = 0; return true; } + if (text == *source && !text.empty()) return true; + if (!parse_number(*source, value) || !std::isfinite(value)) return false; + text = *source; + return true; + }; + if (!cachedNumber(state.rotation, state.rotation_text, state.rotation_value) || !cachedNumber(state.owner, state.owner_text, state.owner_value)) return false; + const double rotation = state.rotation_value, owner = state.owner_value; + if (owner < -2147483648.0 || owner > 2147483647.0) return false; + if (state.has_storage) { + angle += rotation * 0.02; + set_value(state.angle_component, "value_float", LVal::number(angle)); + } + angle += rotation; + if (owner != 0) { + double x, y, r, sx, sy; + if (get_transform((int)owner, x, y, r, sx, sy)) { + const RtEntity* e = ent(entity); + set_transform(entity, x + std::cos(angle) * 16, y - std::sin(angle) * 16, e->rot, e->sx, e->sy); + } else kill(entity); + } + ++native_script_runs_; + return true; +} + bool EntityRuntime::run_script_callback(int entity, int comp_id, const std::string& file, const std::string& function, const std::vector& args) { if (on_gpu_script && on_gpu_script(entity, comp_id, file, function, args)) return true; const int saved_e = updated_entity_, saved_c = updated_comp_; const std::string saved_env = host_.env; updated_entity_ = entity; updated_comp_ = comp_id; - host_.env = lua_vm_key(comp(comp_id), comp_id, file); + const auto* context = comp(comp_id); + const auto vm = context ? context->attrs.find("vm_type") : std::map::const_iterator{}; + if (!context || context->type != "LuaComponent") host_.env.clear(); + else if (vm == context->attrs.end() || (vm->second != "ONE_PER_COMPONENT_INSTANCE" && vm->second != "CREATE_NEW_EVERY_EXECUTION")) { + host_.env.assign(1, 'f'); host_.env.append(file); + } else host_.env = lua_vm_key(comp(comp_id), comp_id, file); std::string err; - bool ok = host_.run_file(file, &err); + bool ok = (function.empty() && args.empty() && run_native_guard(entity, file)) || host_.run_file(file, &err); if (ok && !function.empty()) ok = host_.call_function(function, args, &err); if (host_.env.size() > 1 && host_.env[0] == 'n') host_.drop_env(host_.env); host_.env = saved_env; @@ -381,15 +500,39 @@ bool EntityRuntime::run_script_callback(int entity, int comp_id, const std::stri return ok; } -std::vector EntityRuntime::with_component(const std::string& type) { +std::vector EntityRuntime::with_component(std::string_view type) { std::vector r; - for (auto& [id, e] : ents_) { - if (!e.alive) continue; - for (int cid : e.comps) { RtComponent* c = comp(cid); if (c && c->type == type) { r.push_back(id); break; } } - } + const auto typed = entities_by_component_.find(type); + if (typed != entities_by_component_.end()) for (const auto& [id, count] : typed->second) + if (RtEntity* e = ent(id); e && e->alive) r.push_back(id); return r; } +void EntityRuntime::update_component_fields(const RtComponent& c, int delta, std::string_view changedKey) { + for (auto& [query, owners] : component_fields_) { + const auto& [type, key, value] = query; + if (c.type != type || (!changedKey.empty() && key != changedKey)) continue; + const auto at = c.attrs.find(key); + if (at == c.attrs.end() || (value ? at->second != *value : at->second.empty())) continue; + if (delta > 0) ++owners[c.entity]; + else if (const auto owner = owners.find(c.entity); owner != owners.end() && --owner->second == 0) owners.erase(owner); + } +} + +std::vector EntityRuntime::with_component_field(const std::string& type, const std::string& key, const std::optional& value) { + auto [index, inserted] = component_fields_.try_emplace(FieldQuery{type, key, value}); + auto& owners = index->second; + if (inserted) for (const auto& [id, count] : entities_by_component_[type]) for (int cid : components(id, type, "", true)) { + const auto& attrs = comp(cid)->attrs; + const auto at = attrs.find(key); + if (at != attrs.end() && (value ? at->second == *value : !at->second.empty())) ++owners[id]; + } + std::vector result; + result.reserve(owners.size()); + for (const auto& [id, count] : owners) if (const auto* e = ent(id); e && e->alive) result.push_back(id); + return result; +} + void EntityRuntime::run_death_scripts(int id, int damage_types, int killer) { std::vector> scripts; for (int cid : ents_[id].comps) { @@ -496,8 +639,8 @@ double EntityRuntime::run_damage_about_to_be_received(int id, double damage, dou std::set EntityRuntime::damage_about_entities() { std::set r; - for (auto& [cid, c] : comps_) { - if (c.type != "LuaComponent" || !c.enabled) continue; + for (int id : with_component_field("LuaComponent", "script_damage_about_to_be_received")) for (int cid : components(id, "LuaComponent", "", false)) { + const auto& c = *comp(cid); auto it = c.attrs.find("script_damage_about_to_be_received"); if (it == c.attrs.end() || it->second.empty()) continue; for (int e = c.entity, guard = 0; e && guard < 64; ++guard) { RtEntity* re = ent(e); if (!re || !re->alive) break; r.insert(e); e = re->parent; } @@ -599,9 +742,9 @@ int EntityRuntime::load_game_effect(int entity, const std::string& effect, bool std::vector EntityRuntime::all_game_effects() { std::vector r; - for (auto& [eid, e] : ents_) { - if (!e.alive) continue; - for (int cid : e.comps) { + for (int eid : with_component("GameEffectComponent")) { + const RtEntity& e = *ent(eid); + for (int cid : components(eid, "GameEffectComponent", "", false)) { RtComponent* c = comp(cid); if (!c || c->type != "GameEffectComponent" || !c->enabled) continue; GameEffectRef g; @@ -688,13 +831,19 @@ std::vector EntityRuntime::children(int id) { } std::vector EntityRuntime::components(int entity, const std::string& type, const std::string& tag, bool include_disabled) { + return components_view(entity, type, tag, include_disabled); +} + +std::vector EntityRuntime::components_view(int entity, std::string_view type, const std::string& tag, bool include_disabled) { std::vector r; RtEntity* e = ent(entity); if (!e) return r; - for (int cid : e->comps) { + const auto typed = e->comps_by_type.find(type); + if (!type.empty() && typed == e->comps_by_type.end()) return r; + const auto& candidates = type.empty() ? e->comps : typed->second; + for (int cid : candidates) { RtComponent* c = comp(cid); if (!c) continue; - if (!type.empty() && c->type != type) continue; if (!include_disabled && !c->enabled) continue; if (!tag.empty() && !c->tags.count(tag)) continue; r.push_back(cid); @@ -719,14 +868,14 @@ void EntityRuntime::remove_component(int cid) { if (!c) return; } if (c->type == "LuaComponent" && c->attrs["vm_type"] == "ONE_PER_COMPONENT_INSTANCE") host_.drop_env("c" + std::to_string(cid)); - if (RtEntity* e = ent(c->entity)) e->comps.erase(std::remove(e->comps.begin(), e->comps.end(), cid), e->comps.end()); - comps_.erase(cid); + erase_comp_internal(cid); } void EntityRuntime::set_component_enabled(int cid, bool en) { if (RtComponent* c = comp(cid)) { const bool changed = c->enabled != en; c->enabled = en; + if (changed) if (auto* e = ent(c->entity)) e->component_revision = ++component_revision_; if (changed && en && c->type == "LifetimeComponent") c->attrs["_ng_born"] = std::to_string(frame_); // its count starts when it is switched on (the tutorial stones' emitters: disabled until the player reaches the trigger) if (changed && c->type == "LuaComponent" && !c->attrs["script_enabled_changed"].empty()) // the component's own switch: script_enabled_changed( entity_id, is_enabled ) run_script_callback(c->entity, cid, c->attrs["script_enabled_changed"], "enabled_changed", {LVal::number(c->entity), LVal::boolean(en)}); @@ -789,12 +938,17 @@ LVal EntityRuntime::get_value(int cid, const std::string& key, bool typed) { void EntityRuntime::set_value(int cid, const std::string& key, const LVal& v) { RtComponent* c = comp(cid); if (!c) return; + if (v.t == LVal::T::Nil) return; + update_component_fields(*c, -1, key); switch (v.t) { case LVal::T::Bool: c->attrs[key] = v.b ? "1" : "0"; break; case LVal::T::Num: c->attrs[key] = number_string(v.n); break; case LVal::T::Str: c->attrs[key] = v.s; break; case LVal::T::Nil: return; } + update_component_fields(*c, 1, key); + ++c->value_revision; + if (c->type == "VariableStorageComponent" && key == "name") if (auto* e = ent(c->entity)) e->component_revision = ++component_revision_; if (on_value_changed) on_value_changed(c->entity, c->type, key); } @@ -826,10 +980,12 @@ int EntityRuntime::pickup_candidate(double px, double py) { const double cy = py - 4.0; int best = 0; double best_d = 1e300; - for (auto& [id, e] : ents_) { - if (!e.alive || id == player_id_ || e.parent) continue; + for (int id : with_component("ItemComponent")) { + const RtEntity& e = *ent(id); + if (id == player_id_ || e.parent) continue; int item_comp = 0; - for (int cid : e.comps) { RtComponent* c = comp(cid); if (c && c->type == "ItemComponent" && c->enabled) { item_comp = cid; break; } } + const auto items = components(id, "ItemComponent", "", false); + if (!items.empty()) item_comp = items.front(); if (!item_comp) continue; RtComponent* ic = comp(item_comp); if (ic->attrs["is_pickable"] == "0" || ic->attrs["auto_pickup"] == "1") continue; @@ -878,10 +1034,12 @@ std::vector EntityRuntime::pickup_step(double px, double py, bool interact) int best = 0; double best_d = 1e300; std::vector auto_items; - for (auto& [id, e] : ents_) { - if (!e.alive || id == player_id_) continue; + for (int id : with_component("ItemComponent")) { + const RtEntity& e = *ent(id); + if (id == player_id_) continue; int item_comp = 0; - for (int cid : e.comps) { RtComponent* c = comp(cid); if (c && c->type == "ItemComponent" && c->enabled) { item_comp = cid; break; } } + const auto items = components(id, "ItemComponent", "", false); + if (!items.empty()) item_comp = items.front(); if (!item_comp) continue; RtComponent* ic = comp(item_comp); if (ic->attrs["is_pickable"] == "0") continue; @@ -899,9 +1057,10 @@ std::vector EntityRuntime::pickup_step(double px, double py, bool interact) std::string EntityRuntime::interact_hint(double px, double py) { std::string best; double best_d = 22.0; - for (auto& [id, e] : ents_) { - if (!e.alive || id == player_id_) continue; - for (int cid : e.comps) { + for (int id : with_component("ItemComponent")) { + const RtEntity& e = *ent(id); + if (id == player_id_) continue; + for (int cid : components(id, "ItemComponent", "", false)) { RtComponent* c = comp(cid); if (!c || c->type != "ItemComponent" || !c->enabled || c->attrs["is_pickable"] == "0" || c->attrs["auto_pickup"] == "1") continue; const double d = std::hypot(e.x - px, (e.y - 3.0) - (py - 4.0)); @@ -923,9 +1082,9 @@ void EntityRuntime::material_area_step(const std::function 0 struct Job { int entity, comp; bool failure; double x0, y0, x1, y1; std::string m1, m2; int count_min; }; std::vector jobs; - for (auto& [id, e] : ents_) { - if (!e.alive) continue; - for (int cid : e.comps) { + for (int id : with_component("MaterialAreaCheckerComponent")) { + const RtEntity& e = *ent(id); + for (int cid : components(id, "MaterialAreaCheckerComponent", "", false)) { RtComponent* c = comp(cid); if (!c || c->type != "MaterialAreaCheckerComponent" || !c->enabled) continue; int every = (int)std::atof(c->attrs["update_every_x_frame"].c_str()); @@ -959,8 +1118,9 @@ void EntityRuntime::material_area_step(const std::function(now - runtimeMark).count(); runtimeMark = now; } }; { // LoadEntitiesComponent timers (timeout_frames): the load when mTimerTriggerFrame comes std::vector> due; - for (auto& [id, e] : ents_) { - if (!e.alive) continue; - for (int cid : e.comps) { + for (int id : with_component("LoadEntitiesComponent")) { + for (int cid : components(id, "LoadEntitiesComponent", "", false)) { RtComponent* c = comp(cid); if (!c || c->type != "LoadEntitiesComponent" || !c->enabled) continue; const auto it = c->attrs.find("mTimerTriggerFrame"); @@ -1043,15 +1207,16 @@ void EntityRuntime::tick() { std::vector gone; for (auto& [id, e] : ents_) if (!e.alive && e.died >= 0 && frame_ - e.died >= 3 && id != player_id_ && id != world_state_) gone.push_back(id); for (int id : gone) { - for (int cid : ents_[id].comps) comps_.erase(cid); + for (int cid : std::vector(ents_[id].comps)) erase_comp_internal(cid); ents_.erase(id); + native_guard_states_.erase(id); } } std::vector expire; std::vector> endedEffects; // (effect entity, GameEffectComponent) whose frames ran out this update - for (auto& [id, e] : ents_) { - if (!e.alive) continue; - for (int cid : e.comps) { + for (int id : with_component("LifetimeComponent")) { + const RtEntity& e = *ent(id); + for (int cid : components(id, "LifetimeComponent", "", false)) { RtComponent* c = comp(cid); if (!c || c->type != "LifetimeComponent" || !c->enabled) continue; const double life = std::atof(c->attrs["lifetime"].c_str()); @@ -1084,9 +1249,9 @@ void EntityRuntime::tick() { } return animFrames[key] = len; }; - for (auto& [id, e] : ents_) { - if (!e.alive) continue; - for (int cid : e.comps) { + for (int id : with_component_field("SpriteComponent", "kill_entity_after_finished", "1")) { + const RtEntity& e = *ent(id); + for (int cid : components(id, "SpriteComponent", "", false)) { RtComponent* c = comp(cid); if (!c || c->type != "SpriteComponent" || !c->enabled) continue; auto k = c->attrs.find("kill_entity_after_finished"); @@ -1119,9 +1284,8 @@ void EntityRuntime::tick() { } for (int id : dead) kill(id); } - for (auto& [eid, e] : ents_) { // the duration of the game effects (frames; -1 and 0 never run out): the effect entity goes when it is over - if (!e.alive) continue; - for (int cid : e.comps) { + for (int eid : with_component("GameEffectComponent")) { // effect durations: -1 and 0 never run out + for (int cid : components(eid, "GameEffectComponent", "", false)) { RtComponent* c = comp(cid); if (!c || c->type != "GameEffectComponent" || !c->enabled) continue; const int fr = std::atoi(c->attrs["frames"].c_str()); @@ -1160,6 +1324,7 @@ void EntityRuntime::tick() { } } } + runtimeStamp(0); if (!scripts_on_) return; // ---- the scripts of the entities in the loaded area ---- std::vector ids; @@ -1168,9 +1333,23 @@ void EntityRuntime::tick() { for (int id : ids) { RtEntity* e = ent(id); if (!e || !e->alive) continue; - const std::vector comps = e->comps; + if (!e->lua_component_count && !e->collision_component_count) continue; + // Only these two systems execute in this pass. Preserve component order + // and snapshot semantics without visiting sprites, emitters and storage. + const bool hasCollisions = e->collision_component_count != 0; + const int single = e->single_lua_component; + std::vector snapshot; + std::span comps; + if (single && !hasCollisions) comps = std::span(&single, 1); + else { + snapshot = components(id, "LuaComponent", "", true); + const auto collisions = components(id, "CollisionTriggerComponent", "", true); + snapshot.insert(snapshot.end(), collisions.begin(), collisions.end()); + std::sort(snapshot.begin(), snapshot.end()); + comps = snapshot; + } std::vector hit_scripts; // script_collision_trigger_hit of the entity's LuaComponents - for (int cid : comps) { + if (hasCollisions) for (int cid : comps) { RtComponent* c = comp(cid); if (c && c->type == "LuaComponent" && c->enabled && !c->attrs["script_collision_trigger_hit"].empty()) hit_scripts.push_back(c->attrs["script_collision_trigger_hit"]); } @@ -1178,20 +1357,31 @@ void EntityRuntime::tick() { RtComponent* c = comp(cid); if (!c || !c->enabled) continue; if (c->type == "LuaComponent") { // execute_every_n_frame (-1: only when added or on events), execute_times (< 1: no limit; mTimesExecuted counts), remove_after_executed - const std::string file = c->attrs["script_source_file"]; + auto& schedule = lua_schedules_[cid]; + if (schedule.revision != c->value_revision) { + schedule.file = c->attrs["script_source_file"]; + schedule.every = std::atoi(c->attrs["execute_every_n_frame"].c_str()); + schedule.times = std::atoi(c->attrs["execute_times"].c_str()); + schedule.done = std::atoi(c->attrs["mTimesExecuted"].c_str()); + const auto next = c->attrs.find("mNextExecutionTime"); + schedule.next = next == c->attrs.end() || next->second.empty() ? -1 : std::atoi(next->second.c_str()); + schedule.call_init = c->attrs["call_init_function"] == "1"; + schedule.inited = c->attrs["_inited"] == "1"; + schedule.remove = c->attrs["remove_after_executed"] == "1"; + schedule.revision = c->value_revision; + } + const std::string file = schedule.file; if (file.empty()) continue; - const int every = std::atoi(c->attrs["execute_every_n_frame"].c_str()); - const int times = std::atoi(c->attrs["execute_times"].c_str()); // < 1: no limit - const int done = std::atoi(c->attrs["mTimesExecuted"].c_str()); + const int every = schedule.every, times = schedule.times, done = schedule.done; if (every <= 0 || (times > 0 && done >= times)) continue; if ((frame_ + id) % every != 0) continue; - { auto nt = c->attrs.find("mNextExecutionTime"); if (nt != c->attrs.end() && !nt->second.empty() && frame_ < std::atoi(nt->second.c_str())) continue; } // a script that was told to wait (Component_SetTimeOut of the props) + if (schedule.next >= 0 && frame_ < schedule.next) continue; ++script_runs_; - const bool init = c->attrs["call_init_function"] == "1" && c->attrs["_inited"] != "1"; + const bool init = schedule.call_init && !schedule.inited; static const bool sprof = std::getenv("NG_SCRIPTPROF") != nullptr; // (test: the time of the per-frame scripts by file, the top ones every 300 frames) static std::map> sprofT; const auto st0 = sprof ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - if (init) { c->attrs["_inited"] = "1"; run_script_callback(id, cid, file, "init", {LVal::number(id)}); } + if (init) { c->attrs["_inited"] = "1"; schedule.inited = true; run_script_callback(id, cid, file, "init", {LVal::number(id)}); } else run_script_callback(id, cid, file, "", {}); if (sprof) { auto& pt = sprofT[file]; pt.first += std::chrono::duration(std::chrono::steady_clock::now() - st0).count(); ++pt.second; @@ -1209,6 +1399,7 @@ void EntityRuntime::tick() { if (!alive(id)) break; if (RtComponent* c2 = comp(cid)) { c2->attrs["mTimesExecuted"] = std::to_string(done + 1); + lua_schedules_[cid].done = done + 1; if (c2->attrs["remove_after_executed"] == "1" && (times < 1 || done + 1 >= times)) remove_component(cid); // (spells_to_power.lua: every frame, removed after its first run) } } else if (c->type == "CollisionTriggerComponent") { // 00a87e00 (defaults 00a83cf0: width, height, radius 32, timer_for_destruction 0, destroy_this_entity_when_triggered 1, remove_component_when_triggered 0) @@ -1245,6 +1436,7 @@ void EntityRuntime::tick() { } } } + runtimeStamp(1); // LuaComponents with enable_coroutines (00ba7e20): their VM's wake_up_waiting_threads( 1 ) every frame (data/scripts/lib/coroutines.lua: wait( frames ) in async // functions). One shared Lua state here, so the coroutines of every component share one clock: called once per frame. // Each VM has its own clock and its own waiting threads (the script's dofile of coroutines.lua made them), woken with its entity as the updated one. @@ -1253,7 +1445,12 @@ void EntityRuntime::tick() { const std::string saved_env = host_.env; std::set woken; std::vector> vms; - for (auto& [cid, c] : comps_) if (c.type == "LuaComponent" && c.attrs["enable_coroutines"] == "1" && alive(c.entity)) vms.push_back({cid, c.entity}); + for (int eid : with_component_field("LuaComponent", "enable_coroutines", "1")) for (int cid : components(eid, "LuaComponent", "", true)) { + const auto& at = comp(cid)->attrs; + const auto enabled = at.find("enable_coroutines"); + if (enabled != at.end() && enabled->second == "1") vms.push_back({cid, eid}); + } + std::sort(vms.begin(), vms.end()); // component insertion order, independent of hash table storage for (auto& [cid, eid] : vms) { RtComponent* c = comp(cid); if (!c || !alive(eid)) continue; @@ -1269,6 +1466,8 @@ void EntityRuntime::tick() { host_.env = saved_env; updated_entity_ = saved_e; updated_comp_ = saved_c; } + runtimeStamp(2); + if (runtimeProfile && ++runtimeFrames == 300) { std::printf("runtime frame %d: upkeep %.3f, scheduled scripts %.3f, coroutines %.3f ms/frame%c", frame_, runtimeTimes[0] / runtimeFrames, runtimeTimes[1] / runtimeFrames, runtimeTimes[2] / runtimeFrames, 10); for (double& t : runtimeTimes) t = 0; runtimeFrames = 0; } } } // namespace ng diff --git a/src/entity/runtime.h b/src/entity/runtime.h index 4e80ed5..c810d70 100644 --- a/src/entity/runtime.h +++ b/src/entity/runtime.h @@ -4,8 +4,12 @@ // 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" @@ -19,6 +23,9 @@ struct RtComponent { 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; }; @@ -29,6 +36,9 @@ struct RtEntity { 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) @@ -95,6 +105,8 @@ public: 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. @@ -137,7 +149,9 @@ public: // 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(const std::string& type); // living entities with a component of the type // living entities whose file name contains the text + 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; } @@ -174,6 +188,7 @@ public: 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; @@ -229,19 +244,47 @@ public: 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::map comps_; + 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 diff --git a/src/gun/gpu_fx.cpp b/src/gun/gpu_fx.cpp index 6fcf103..8de2a7d 100644 --- a/src/gun/gpu_fx.cpp +++ b/src/gun/gpu_fx.cpp @@ -94,11 +94,13 @@ int GpuFx::type_for(const ProjDef& d, ProjectileSim* sim) { if (it != typeOf_.end()) return it->second; if (types_ >= kMaxTypes) return 0; const int loadFirst = 0, loadCount = d.load_files.empty() ? 0 : 1; // load_this_entity is spawned by the host (an event of the explosion); the type only says whether there is any - const int first_emit = emitters_; + std::vector packedEmitters; + packedEmitters.reserve(d.fx.emitters.size() * kEmitStride * 4); for (const EmitterDef& e : d.fx.emitters) { - if (emitters_ >= kMaxEmitters) break; if (std::getenv("NG_EMITLOG")) std::printf("emitter %d of a type: colour %08x glow %d count %d-%d life %.0f-%.0f r %.0f-%.0f away %.0f attractor %.0f long %d%c", emitters_, e.color, (int)e.glow, e.count0, e.count1, e.life0, e.life1, e.rmin, e.rmax, e.away, e.attractor, (int)e.as_long, 10); - uint32_t* w = tables_ + (size_t)(kEmitBase + emitters_ * kEmitStride) * 4; + const size_t offset = packedEmitters.size(); + packedEmitters.resize(offset + kEmitStride * 4); + uint32_t* w = packedEmitters.data() + offset; Words e0{w}, e1{w + 4}, e2{w + 8}, e3{w + 12}; // e0.y: flags (bit 5: not glowing - blended), bits 8-19 / 20-31: area_circle_radius max / min in quarter pixels; e0.x bits 24-31: the alpha of a blended one const uint32_t qmax = (uint32_t)std::clamp((int)std::lround(e.rmax * 4.0f), 0, 4095), qmin = (uint32_t)std::clamp((int)std::lround(e.rmin * 4.0f), 0, 4095); @@ -106,9 +108,20 @@ int GpuFx::type_for(const ProjDef& d, ProjectileSim* sim) { e1.f(0, e.xv0); e1.f(1, e.xv1); e1.f(2, e.yv0); e1.f(3, e.yv1); e2.f(0, e.gravity); e2.i(1, (int)(std::min(e.interval0 + e.interval1, 255) | std::clamp((int)std::lround(e.attractor), 0, 255) << 8) | (std::clamp((int)std::lround(e.away), -32768, 32767) << 16)); e2.i(2, std::min(e.count0, 65535) | (std::min(e.delay, 32767) << 16)); e2.i(3, std::max(e.count0, e.count1)); // (z: count_min | delay_frames << 16) // (y: the mean interval in half frames (min + max) | attractor_force << 8 | velocity_always_away_from_center (px/s, signed) << 16) e3.f(0, e.jitter); e3.i(1, e.real && material_index ? material_index(e.material) : 0); e3.f(2, 0.5f * (e.ox1 - e.ox0)); e3.f(3, 0.5f * (e.oy1 - e.oy0)); - ++emitters_; } - const int n_emit = emitters_ - first_emit; + int first_emit = 0, n_emit = 0; + if (!packedEmitters.empty()) { + const auto cached = emitterOf_.find(packedEmitters); + if (cached != emitterOf_.end()) { + first_emit = cached->second.first; n_emit = cached->second.second; + } else { + first_emit = emitters_; + n_emit = std::min((int)d.fx.emitters.size(), kMaxEmitters - emitters_); + std::memcpy(tables_ + (size_t)(kEmitBase + first_emit * kEmitStride) * 4, packedEmitters.data(), (size_t)n_emit * kEmitStride * 16); + emitters_ += n_emit; + emitterOf_.emplace(std::move(packedEmitters), std::make_pair(first_emit, n_emit)); + } + } if (std::getenv("NG_EMITLOG")) std::printf("type %d %s: emitters %d..+%d%c", types_, d.file.c_str(), first_emit, n_emit, 10); uint32_t* t = tables_ + (size_t)(types_ * kTypeStride) * 4; Words t0{t}, t1{t + 4}, t2{t + 8}, t3{t + 12}, t4{t + 16}, t5{t + 20}, t6{t + 24}, t7{t + 28}, t8{t + 32}, t9{t + 36}, t10{t + 40}, t11{t + 44}, t12{t + 48}, t13{t + 52}, t14{t + 56}, t15{t + 60}, t16{t + 64}, t17{t + 68}; @@ -173,7 +186,7 @@ int GpuFx::type_for(const ProjDef& d, ProjectileSim* sim) { } const int dpm = !d.death_particle.empty() && material_index ? material_index(d.death_particle) : -1; t17.f(0, d.gravity_x); t17.f(1, d.damage_scale_max_speed); t17.i(2, d.lifetime_randomness); t17.i(3, dpm > 0 ? (dpm + 1) | (std::min(d.death_particle_count, 32767) << 16) : 0); - if (std::getenv("NG_TYPELOG")) std::printf("type %d %s: t1.w %08x t16.x %08x bounces %d ocd %d%c", types_, d.file.c_str(), t[12 + 3], t[64], d.bounces_left, (int)d.on_collision_die, 10); + if (std::getenv("NG_TYPELOG")) std::printf("type %d %s: t1.w %08x t16.x %08x bounces %d ocd %d fire %.3f slice %.3f%c", types_, d.file.c_str(), t[7], t[64], d.bounces_left, (int)d.on_collision_die, d.dmg_type[2], d.dmg_type[4], 10); typeEffects_.resize(types_ + 1); typeEffects_[types_] = d.hit_effects; typeDef_.resize(types_ + 1); @@ -257,10 +270,11 @@ bool GpuFx::cast(ProjectileSim& sim, const ShotResult& shot, double x, double y, return request(frame, x, y, dx, dy, add_node(sim, shot.root), 1, shooter, herd); } -bool GpuFx::shoot(int frame, double x, double y, double vx, double vy, int node, int team, int shooter, int herd, bool scripts_done) { +bool GpuFx::shoot(int frame, double x, double y, double vx, double vy, int node, int team, int shooter, int herd, bool scripts_done, int entity) { if (!request(frame, x, y, vx, vy, node, 1, shooter, herd)) return false; float* r = hostF_ + kCastBase + (size_t)((((frame & 1) * 256 + hostI_[frame & 1] - 1) * 2)) * 4; r[5] = (float)(team + 4 + (herd << 3) + (scripts_done ? (1 << 30) : 0)); // bit 2: the speed is the length of the direction vector, bit 30: the script_shot scripts have run + if (entity > 0) r[6] = -(float)entity; // negative seed carries the existing runtime entity to the twin event return true; } diff --git a/src/gun/gpu_fx.h b/src/gun/gpu_fx.h index 3891162..be5a093 100644 --- a/src/gun/gpu_fx.h +++ b/src/gun/gpu_fx.h @@ -31,7 +31,7 @@ public: // Casts a shot from (x, y) along (dx, dy) on the frame `frame` (the request goes into the list that frame consumes). bool cast(ProjectileSim& sim, const ShotResult& shot, double x, double y, double dx, double dy, int frame, int shooter_slot_plus_1 = 1, int herd_plus_1 = 0); // the shooter is the slot + 1 of the entity that casts (the player: 1) // A shot of an entity file from a script (GameShootProjectile): `node` from persistent_node, velocity in cells per frame (it replaces the file's speed), team 1 the player's, 2 enemies. - bool shoot(int frame, double x, double y, double vx, double vy, int node, int team, int shooter_slot_plus_1, int herd_plus_1, bool scripts_done = false); // scripts_done: the script_shot scripts have already seen this shot + bool shoot(int frame, double x, double y, double vx, double vy, int node, int team, int shooter_slot_plus_1, int herd_plus_1, bool scripts_done = false, int entity = 0); // entity: retain the script's projectile identity in its twin event // Visual-only explosion (exploding props): the sprite, sparks and light flash of config_explosion attributes. void boom(const ExplosionFx& fx, double x, double y, int frame); // A node that casts one projectile of an entity file (no modifiers, no triggers) and stays valid: the ranged attack of a creature. @@ -67,6 +67,7 @@ private: std::vector typeDef_; // the definition behind every projectile type (stable: owned by the ProjectileSim map) std::vector> typeEffects_; // the hit effects of every projectile type std::map boomOf_; + std::map, std::pair> emitterOf_; // packed definitions -> shared table range std::map texOf_; }; diff --git a/src/gun/gun.cpp b/src/gun/gun.cpp index 9e9588c..61d1621 100644 --- a/src/gun/gun.cpp +++ b/src/gun/gun.cpp @@ -173,6 +173,7 @@ std::vector GunRuntime::cards(int wand) { } bool GunRuntime::equip(int wand) { + if (!wand) { wand_ = 0; return true; } if (!loaded_ || !rt_.alive(wand) || !ability(wand)) return false; auto num = [&](const char* k, double d) { return abil(wand, k, d); }; std::ostringstream lua; diff --git a/src/gun/projectile_damage.h b/src/gun/projectile_damage.h new file mode 100644 index 0000000..e7bd2d9 --- /dev/null +++ b/src/gun/projectile_damage.h @@ -0,0 +1,27 @@ +#pragma once +#include "entity/entity.h" +#include + +namespace ng { +// A flying projectile owns its motion and sprite; this invisible entity only receives damage. +inline EntityState projectile_damage_proxy(const EntityComponent& dm, double x, double y, int projectile_slot) { + EntityState e; + auto number = [&](const char* key, float fallback) { const auto it = dm.attrs.find(key); return it == dm.attrs.end() ? fallback : (float)std::atof(it->second.c_str()); }; + e.pos[0] = (float)x; e.pos[1] = (float)y; + e.st[0] = 4; e.st[3] = 32; // external motion, like a rigid prop + e.hp[0] = number("hp", 1); + e.x[3] = (int32_t)(e.hp[0] * 1000); + e.ex7[0] = 0; + e.ex19[3] = -(float)(projectile_slot + 1); // exclude the projectile's own direct hit + static const char* keys[] = {"projectile", "explosion", "fire", "melee", "electricity", "drill", "slice", "ice", "healing", "physics_hit", "radioactive", "poison", "overeating", "curse", "holy"}; + for (int i = 0; i < 15; ++i) { + const std::string key = std::string("damage_multipliers.") + keys[i]; + const float value = number(key.c_str(), 1); + if (i < 4) e.ex9[i] = value; + else if (i < 8) e.ex12[i - 4] = value; + else if (i < 12) e.ex13[i - 8] = value; + else e.ex14[i - 12] = value; + } + return e; +} +} diff --git a/src/gun/projectiles.cpp b/src/gun/projectiles.cpp index d4b67e2..bf29951 100644 --- a/src/gun/projectiles.cpp +++ b/src/gun/projectiles.cpp @@ -24,6 +24,20 @@ constexpr double kDeg = 3.14159265358979323846 / 180.0; } // namespace +std::string ProjectileSim::script_definition_key(const EntityDef& entity) { + std::string state = entity.tags; + for (const auto& c : entity.comps) { + state += c.type; + if (c.type == "VariableStorageComponent") continue; + for (const auto& [key, value] : c.attrs) { + // _ng_next and _ng_img_t are emitter clocks, not changes to its effect definition. + if (key.rfind("_ng_", 0) == 0 || key == "mWhoShot" || key == "mEntityThatShot" || key == "mShooterHerdId" || key.rfind("mVelocity", 0) == 0) continue; + state += key + "=" + value + ";"; + } + } + return entity.path + "#script" + std::to_string(std::hash()(state)); +} + const ProjDef* ProjectileSim::def(const std::string& file) { auto it = defs_.find(file); if (it != defs_.end()) return &it->second; @@ -41,6 +55,7 @@ void ProjectileSim::disable_gpu_lua() { // The entity has components the GPU does not simulate (CellEater, MagicConvertMaterial, area effects, scripts...): the host keeps a runtime twin of the projectile static bool host_runs(const EntityDef& e, bool gpu_script = false) { + if (!e.all("DamageModelComponent").empty()) return true; for (const char* rich : {"CellEaterComponent", "MagicConvertMaterialComponent", "GameAreaEffectComponent", "AreaDamageComponent", "LightningComponent", "LaserEmitterComponent", "MaterialSeaSpawnerComponent", "TeleportProjectileComponent", "EnergyShieldComponent", "ExplosionComponent", "LooseGroundComponent", "ArcComponent", "LightningComponent", "BlackHoleComponent", "MaterialSuckerComponent"}) if (!e.all(rich).empty()) return true; if (!gpu_script) for (const EntityComponent* lc : e.all("LuaComponent")) for (const auto& kv : lc->attrs) if (kv.first.rfind("script_", 0) == 0 && !kv.second.empty()) return true; @@ -94,7 +109,11 @@ const ProjDef* ProjectileSim::def_from(const std::string& file, const EntityDef& std::istringstream tags(entity.tags);std::string tag; while(std::getline(tags,tag,',')) { auto begin=tag.find_first_not_of(' '), end=tag.find_last_not_of(' '); - if(begin!=std::string::npos&&tag.substr(begin,end-begin+1)=="projectile")d.query_projectile=true; + if(begin!=std::string::npos) { + const auto token = tag.substr(begin,end-begin+1); + if(token=="projectile")d.query_projectile=true; + if(token=="orbit_projectile")d.orbiting=true; + } } unsigned velocity_count=0; for(const auto* component:entity.all("VelocityComponent"))if(component->get("_enabled","1")!="0")++velocity_count; @@ -160,6 +179,11 @@ const ProjDef* ProjectileSim::def_from(const std::string& file, const EntityDef& d.friendly_fire = attr(pc, "friendly_fire", 0) != 0; d.dont_hit_player = attr(pc, "dont_hit_player", 0) != 0; d.collide_with_shooter_frames = (int)attr(pc, "collide_with_shooter_frames", -1); // (-1: never hits its shooter) + if (d.orbiting) { + d.friendly_fire = true; + d.dont_hit_player = false; + d.collide_with_shooter_frames = 6; + } d.physics = !e->all("PhysicsBodyComponent").empty(); if (d.physics) d.on_collision_die = false; d.explode_on_death = attr(pc, "on_death_explode", 0) != 0 || attr(pc, "on_lifetime_out_explode", 0) != 0; diff --git a/src/gun/projectiles.h b/src/gun/projectiles.h index d99f8c7..bdd0ca9 100644 --- a/src/gun/projectiles.h +++ b/src/gun/projectiles.h @@ -26,6 +26,7 @@ struct ProjDef { bool on_collision_die = true; int bounces_left = 0; // ProjectileComponent bounces_left: grenades bounce this often before they explode bool friendly_fire = false; // ProjectileComponent.friendly_fire: hurts the herd of the shooter too + bool orbiting = false; // orbit_projectile: attached hazards can hit their caster after launch grace bool dont_hit_player = false; // dont_hit_player: never hurts the player, whatever the herds int collide_with_shooter_frames = 0; // the shooter itself is not hit during the first frames // HitEffectComponent (of the projectile and of its extra entities): what a hit does to the victim @@ -53,7 +54,7 @@ struct ProjDef { bool die_on_liquid = false; // die_on_liquid_collision float liquid_drag = 1.0f; // VelocityComponent liquid_drag (constructor default 1.0): the share of the speed lost per liquid cell around, per second bool displace_liquid = true; // ProjectileComponent displace_liquid: the liquid around is pushed aside (the constructor default is not recovered, the explicit values of the data are honoured) - bool physics = false; // a thrown physics body (bombs): bounces and rolls, explodes only when its fuse runs out + bool physics = false; // a thrown physics body (bombs): bounces and rolls; fuse expiry or damage can detonate it bool creature = false; // a cast creature (SUMMON_WANDGHOST's wand_ghost_player.xml: no ProjectileComponent, an AI): the host spawns it as a whole entity, the GPU projectile only carries it out of the wand float break_speed = 0; // a breakable thrown body (SUMMON_EGG's egg_monster.xml: PhysicsBodyCollisionDamage + DamageModel + ExplodeOnDamage): an impact faster than this (px/s) kills it and it explodes with the ExplodeOnDamage config bool explode_on_death = false; @@ -147,6 +148,8 @@ public: const ProjDef* def(const std::string& file); // A definition built from an entity description that is not a file (the runtime copy of a projectile after the scripts changed it), stored under `key`. const ProjDef* def_from(const std::string& key, const EntityDef& entity, const std::string& extras = ""); // + the extra entities of the shot (emitters, light, hit effects, homing) + // Stable identity for a script's projectile definition; host scheduling state is per instance. + static std::string script_definition_key(const EntityDef& entity); EntityDb& db() { return db_; } // The projectile with the extra entities a shot carries (c.extra_entities, comma separated): their particle emitters and light are added. const ProjDef* def(const std::string& file, const std::string& extras); diff --git a/src/lua/lua_host.cpp b/src/lua/lua_host.cpp index f2f025f..7052de5 100644 --- a/src/lua/lua_host.cpp +++ b/src/lua/lua_host.cpp @@ -86,10 +86,19 @@ double num(lua_State* L, int i, double def = 0) { return lua_isnumber(L, i) ? lu std::string str(lua_State* L, int i) { return lua_isstring(L, i) ? lua_tostring(L, i) : ""; } // ---- file loading ---- +static size_t script_cache_revision = 0; bool load_from_wak(LuaHost& h, lua_State* L, const std::string& path, std::string* err) { static std::unordered_map> cache; // the scripts of the entities run again and again: read the archive once - if (LuaHost::scripts_changed) { cache.clear(); LuaHost::scripts_changed = false; } // (a mod replaced a file) + static std::unordered_map> compiled; + if (LuaHost::scripts_changed) { cache.clear(); compiled.clear(); ++script_cache_revision; LuaHost::scripts_changed = false; } // (a mod replaced a file) + if (const auto chunk = compiled.find(path); chunk != compiled.end()) { + // Load a fresh closure so each entity still receives its own environment. + // Parsing identical source for every per-frame projectile script is costly. + if (luaL_loadbuffer(L, (const char*)chunk->second.data(), chunk->second.size(), ("@" + path).c_str()) == 0) return true; + lua_pop(L, 1); + compiled.erase(chunk); + } auto it = cache.find(path); if (it == cache.end()) { std::vector raw; @@ -117,6 +126,14 @@ bool load_from_wak(LuaHost& h, lua_State* L, const std::string& path, std::strin lua_pop(L, 1); return false; } + std::vector bytecode; + const auto writer = [](lua_State*, const void* bytes, size_t size, void* output) -> int { + auto& out = *static_cast*>(output); + const auto* begin = static_cast(bytes); + out.insert(out.end(), begin, begin + size); + return 0; + }; + if (lua_dump(L, writer, &bytecode) == 0 && !bytecode.empty()) compiled.emplace(path, std::move(bytecode)); return true; } @@ -461,6 +478,21 @@ LuaHost::LuaHost(const Wak& wak, GameRandom& rng) : wak_(wak), rng_(rng) { L_ = luaL_newstate(); luaL_openlibs(L_); register_api(); + // Retain function identities for validated native script paths. Lua overrides + // (including overrides local to an entity's VM) must keep running through Lua. + auto capture = [&](const char* prefix) { + lua_pushnil(L_); + while (lua_next(L_, -2)) { + if (lua_type(L_, -2) == LUA_TSTRING && lua_isfunction(L_, -1)) { + const std::string key = std::string(prefix) + lua_tostring(L_, -2); + lua_pushvalue(L_, -1); + original_api_[key] = luaL_ref(L_, LUA_REGISTRYINDEX); + } + lua_pop(L_, 1); + } + }; + lua_pushvalue(L_, LUA_GLOBALSINDEX); capture(""); lua_pop(L_, 1); + lua_getglobal(L_, "math"); capture("math."); lua_pop(L_, 1); // Spawn functions the engine registers for every biome before its script runs (the script may override colours). // Colours as RGBA hex, the table common to all biomes of the reference data (noita-tools maps.json). static const struct { uint32_t rgba; const char* fn; } kDefaults[] = { @@ -554,7 +586,7 @@ void LuaHost::register_api() { register_host_natives(*this); } -void LuaHost::register_native(const char* name, NativeFn fn) { reg(L_, *this, name, fn); } +void LuaHost::register_native(const char* name, NativeFn fn) { ++execution_revision_; reg(L_, *this, name, fn); } // Pushes the current environment table (the globals when env is empty), made on first use: a table whose metatable sends missing names to the globals static void push_env_table(lua_State* L, const std::string& key) { @@ -580,7 +612,12 @@ static void get_fn(lua_State* L, const std::string& env, const std::string& name } void LuaHost::drop_env(const std::string& key) { + ++execution_revision_; if (key.empty()) return; + if (const auto files = compiled_files_.find(key); files != compiled_files_.end()) { + for (const auto& [file, ref] : files->second) luaL_unref(L_, LUA_REGISTRYINDEX, ref); + compiled_files_.erase(files); + } lua_getfield(L_, LUA_REGISTRYINDEX, "ng_envs"); if (lua_istable(L_, -1)) { lua_pushnil(L_); lua_setfield(L_, -2, key.c_str()); } lua_pop(L_, 1); @@ -593,7 +630,50 @@ bool LuaHost::has_function(const std::string& name, bool own) { return f; } +bool LuaHost::has_cached_file(const std::string& path) const { + if (scripts_changed || compiled_revision_ != script_cache_revision) return false; + const auto files = compiled_files_.find(env); + return files != compiled_files_.end() && files->second.count(path) != 0; +} + +bool LuaHost::uses_original_api(std::initializer_list names) { + const int top = lua_gettop(L_); + push_env_table(L_, env); + const int environment = lua_gettop(L_); + auto finish = [&](bool result) { lua_settop(L_, top); return result; }; + if (!env.empty()) { + if (!lua_getmetatable(L_, environment)) return finish(false); + lua_pushliteral(L_, "__index"); lua_rawget(L_, -2); + lua_pushvalue(L_, LUA_GLOBALSINDEX); + const bool standardLookup = lua_rawequal(L_, -1, -2) != 0; + lua_pop(L_, 3); + if (!standardLookup) return finish(false); + } + auto global = [&](const std::string& name) { + lua_pushlstring(L_, name.data(), name.size()); lua_rawget(L_, environment); + if (lua_isnil(L_, -1) && !env.empty()) { lua_pop(L_, 1); lua_pushlstring(L_, name.data(), name.size()); lua_rawget(L_, LUA_GLOBALSINDEX); } + }; + for (const char* name : names) { + const auto original = original_api_.find(name); + if (original == original_api_.end()) return finish(false); + const char* dot = std::strchr(name, '.'); + if (dot) { + global(std::string(name, dot)); + if (!lua_istable(L_, -1)) return finish(false); + lua_pushstring(L_, dot + 1); lua_rawget(L_, -2); lua_remove(L_, -2); + } else global(name); + lua_rawgeti(L_, LUA_REGISTRYINDEX, original->second); + const bool same = lua_rawequal(L_, -1, -2) != 0; + lua_pop(L_, 2); + if (!same) return finish(false); + } + global("NULL_ENTITY"); + const bool zero = lua_isnumber(L_, -1) && lua_tonumber(L_, -1) == 0; + return finish(zero); +} + bool LuaHost::call_function(const std::string& name, const std::vector& args, std::string* err) { + ++execution_revision_; get_fn(L_, env, name); if (!lua_isfunction(L_, -1)) { lua_pop(L_, 1); @@ -612,6 +692,7 @@ bool LuaHost::call_function(const std::string& name, const std::vector& ar } bool LuaHost::call_function_numbers(const std::string& name, const std::vector& args, int nresults, std::vector& results, std::string* err) { + ++execution_revision_; results.assign(nresults, 0.0); get_fn(L_, env, name); // (not a function: the pcall fails, as the engine's does) for (const LVal& a : args) { @@ -713,12 +794,35 @@ bool LuaHost::gen_set(int cid, const std::string& key, const std::string& value) bool LuaHost::load_once(const std::string& path) { return loaded_once_.insert(path).second; } bool LuaHost::run_file(const std::string& path, std::string* err) { - if (!load_from_wak(*this, L_, path, err)) return false; - if (!env.empty()) { push_env_table(L_, env); lua_setfenv(L_, -2); } + ++execution_revision_; + if (scripts_changed) { // refresh source/bytecode before consulting this state's closures + if (!load_from_wak(*this, L_, path, err)) return false; + lua_pop(L_, 1); + } + if (compiled_revision_ != script_cache_revision) { + for (const auto& [key, files] : compiled_files_) for (const auto& [file, ref] : files) luaL_unref(L_, LUA_REGISTRYINDEX, ref); + compiled_files_.clear(); + compiled_revision_ = script_cache_revision; + } + auto& files = compiled_files_[env]; + const auto cached = files.find(path); + if (cached != files.end()) lua_rawgeti(L_, LUA_REGISTRYINDEX, cached->second); + else { + if (!load_from_wak(*this, L_, path, err)) return false; + lua_pushvalue(L_, -1); + files.emplace(path, luaL_ref(L_, LUA_REGISTRYINDEX)); + } + // Separate closures for separate environments; dropping a VM releases its chunks too. + // Reusing a chunk also lets LuaJIT retain its hot counters and compiled traces. + push_env_table(L_, env); + lua_getfenv(L_, -2); + if (lua_rawequal(L_, -1, -2)) lua_pop(L_, 2); + else { lua_pop(L_, 1); lua_setfenv(L_, -2); } return pcall_traced(L_, 0, 0, err); } bool LuaHost::run_string(const std::string& code, const std::string& chunk_name, std::string* err) { + ++execution_revision_; if (luaL_loadbuffer(L_, code.data(), code.size(), chunk_name.c_str()) != 0) { if (err) *err = lua_tostring(L_, -1); lua_pop(L_, 1); @@ -728,6 +832,7 @@ bool LuaHost::run_string(const std::string& code, const std::string& chunk_name, } bool LuaHost::call_global(const std::string& name, const std::vector& args, std::string* err) { + ++execution_revision_; lua_getglobal(L_, name.c_str()); if (!lua_isfunction(L_, -1)) { lua_pop(L_, 1); diff --git a/src/lua/lua_host.h b/src/lua/lua_host.h index 0bf4405..dc8e915 100644 --- a/src/lua/lua_host.h +++ b/src/lua/lua_host.h @@ -61,6 +61,9 @@ public: // globals. run_file / call_function / has_function use it, a dofile'd chunk takes the environment of the one calling it, dofile_once counts per environment. std::string env; void drop_env(const std::string& key); + uint64_t execution_revision() const { return execution_revision_; } + bool has_cached_file(const std::string& path) const; + bool uses_original_api(std::initializer_list names); // The same with nresults results, each read with lua_tonumber (nil and non-numbers give 0, as the engine reads them). On an error they are all 0. bool call_function_numbers(const std::string& name, const std::vector& args, int nresults, std::vector& results, std::string* err = nullptr); // The game world behind the entity/component API (null: scripts only record events). @@ -184,6 +187,10 @@ private: std::vector events_; std::map missing_; std::set loaded_once_; + std::map> compiled_files_; // environment -> file -> registry reference + size_t compiled_revision_ = size_t(-1); + uint64_t execution_revision_ = 0; + std::map original_api_; std::vector actions_; size_t actions_len_ = (size_t)-1; bool tried_actions_ = false;