optimizations + bugfixes

* Bombs explode on damage/explosion
* Orbits spell fixed
* Performance: implemented shared component indexes,
compiled Lua caching, and updates only for changed projectile fields.
Also removed accumulating hidden entity slots.
This commit is contained in:
Gaël committed 2026-10-20 11:02:18 +02:00
1 parent 87bff164f1
commit a34f42d53e
17 files changed
+727 -131

No files matched your search

+3 -3
View File
@@ -14,7 +14,7 @@ void main() {
for (int r = int(gl_LocalInvocationID.x); r < reqCount; r += 64) { 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]; vec4 r0 = hcast[(list * 256 + r) * 2], r1 = hcast[(list * 256 + r) * 2 + 1];
int node = int(r1.x); 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; } 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]; 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); 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 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; 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)); 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 if ((int(r1.w) & 1) != 0) { // muzzle flash of the first projectiles of a cast
ivec4 t6 = fti(tb + 6); ivec4 t6 = fti(tb + 6);
+1
View File
@@ -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) { bool victim_at(vec2 pos, int shooter, int herd, int flags, int age, out int hit) {
hit = -1; hit = -1;
for (int k = 0; k < ctrl.inp.w; ++k) { 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; 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 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; const int root = int(ents[k].ex19.w) - 1;
+1
View File
@@ -153,6 +153,7 @@ public:
void remove(int item) { void remove(int item) {
const Loc l = where(item); const Loc l = where(item);
take(item); take(item);
if (item && gun_.equipped() == item) gun_.equip(0);
(void)l; (void)l;
} }
+222 -54
View File
@@ -18,6 +18,7 @@
#include <set> #include <set>
#include <unordered_map> #include <unordered_map>
#include <chrono> #include <chrono>
#include <charconv>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
@@ -80,6 +81,7 @@ void fly_waypoint(FlyPath& fp, float px, float py, float gx, float gy, int frame
#include "app/inventory.h" #include "app/inventory.h"
#include "entity/status.h" #include "entity/status.h"
#include "gun/gpu_fx.h" #include "gun/gpu_fx.h"
#include "gun/projectile_damage.h"
#include "gun/projectiles.h" #include "gun/projectiles.h"
#include "physics/gpu_bodies.h" #include "physics/gpu_bodies.h"
#include "phys/phys_system.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 ---- // ---- entity runtime <-> GPU: slots, hooks, uploads of entities spawned by scripts ----
std::vector<int> slotToId(kMaxEnts, 0); std::vector<int> 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 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<ScriptShot> pendingShots; // GameShootProjectile calls of this tick std::vector<ScriptShot> pendingShots; // GameShootProjectile calls of this tick
std::map<std::string, int> scriptShotNode; // file -> persistent node of the GPU projectile system std::map<std::string, int> scriptShotNode; // file -> persistent node of the GPU projectile system
// the game effects (GameEffectComponent children) of creatures and the player, as the GPU sees them // 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<CutRule> cutRules; std::vector<CutRule> cutRules;
bool cutGrow = false; bool cutGrow = false;
std::vector<std::array<float, 8>> pendingEdits; // (x, y, radius, max durability, mode, material, probability, 0) std::vector<std::array<float, 8>> 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<float, 24> lastGpu{}; int lastShooter = 0; uint64_t syncRevision = uint64_t(-1), hostRevision = uint64_t(-1), shieldLayoutRevision = uint64_t(-1), shieldValueRevision = uint64_t(-1); std::vector<ng::RtComponent*> 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<ProjTwin> projTwins; std::vector<ProjTwin> projTwins;
std::set<int> deferredProjectileSlots;
bool realizingProjectileSlots = false;
size_t twinRevision = 0, indexedTwinRevision = size_t(-1);
std::unordered_map<int, size_t> 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; bool gpuLuaRecovering = false;
struct PendingGpuLua { int entity, component; std::string file; }; struct PendingGpuLua { int entity, component; std::string file; };
std::vector<PendingGpuLua> pendingGpuLua; std::vector<PendingGpuLua> 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, // 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). // 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 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 (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 (type == "ProjectileComponent") {
if (key == "damage") return 4; if (key == "damage") return 4;
@@ -2247,27 +2263,44 @@ int main(int argc, char** argv) {
} }
return s; 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) 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)
const int age = (int)f[10]; ng::RtEntity* te = rt->ent(id);
for (int pc : rt->components(id, "ProjectileComponent", "", true)) { if (twin && te && twin->syncRevision != te->component_revision) twin->synced = false;
auto& at = rt->comp(pc)->attrs; std::vector<ng::RtComponent*> localComponents;
at["damage"] = std::to_string(f[4]); if (!twin || !twin->synced) {
at["lifetime"] = std::to_string(f[5] >= 1e8f ? -1 : std::max(0, (int)f[5] - age)); auto& cached = twin ? twin->projectileComponents : localComponents;
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) cached.clear();
at["config_explosion.explosion_radius"] = std::to_string(f[8]); for (int cid : rt->components(id, "ProjectileComponent", "", true)) cached.push_back(rt->comp(cid));
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]);
} }
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); } const auto& projectileComponents = twin ? twin->projectileComponents : localComponents;
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 const int age = (int)f[10];
bool vsr = true; double av = 0; const auto changed = [&](int field) { return !twin || !twin->synced || twin->lastGpu[field] != f[field]; };
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 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]); } 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; 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) 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); const int t = rt->spawn(pd->file, x, y, 0, false);
@@ -2280,6 +2313,19 @@ int main(int argc, char** argv) {
} }
return t; 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 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()); }; 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<const float*>(fprojReadPtr + (size_t)slot * 96); const float* f = reinterpret_cast<const float*>(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); ProjTwin tw; tw.slot = k; tw.serial = d[3]; tw.id = twin; tw.sig = twinSig(twin);
projTwins.push_back(tw); projTwins.push_back(tw);
++twinRevision;
twinSerial[k] = d[3]; 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); 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; 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 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); if (prevEnabled2) prevEnabled2(id);
}; };
rt->on_killed = [&, prevKilledX = rt->on_killed](int id) { // ExplosionComponent ON_DEATH: the entity explodes as it dies 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); patchEntity(re->gpu, offsetof(ng::EntityState, st) + 12, &flags, 4);
}; };
rt->on_value_changed = [&](int id, const std::string& type, const std::string& key) { 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); 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 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<const float*>(fprojReadPtr + (size_t)tw.slot * 96) : nullptr; const float* f0 = fprojReadPtr ? reinterpret_cast<const float*>(fprojReadPtr + (size_t)tw.slot * 96) : nullptr;
if (f0) if (const ng::ProjDef* pd = gfx.type_def(reinterpret_cast<const int32_t*>(f0 + 12)[0])) { // the jump is part of the path: a trail emitter (is_trail, trail_gap) draws if (f0) if (const ng::ProjDef* pd = gfx.type_def(reinterpret_cast<const int32_t*>(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 // 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 (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; 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) { 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(); 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); 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) // 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); 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 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); 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 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())); } else if (fld >= 0) { const auto cs = rt->components(id, type, "", true); if (!cs.empty()) twinToGpu(tw.slot, fld, rt->comp(cs.front())); }
return; 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 // 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) // projectile it sprayed sparks from one point; the host's emitters draw its image)
if (rt->components(entityId, "ProjectileComponent", "", true).empty()) return; if (rt->components(entityId, "ProjectileComponent", "", true).empty()) return;
pendingShots.push_back({pe->file, shooter, x, y, tx, ty, send}); deferredProjectileSlots.erase(entityId);
rt->kill(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) 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); 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); } 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); ng::RtEntity* re = rt->ent(id);
if (!re) return; 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)) { for (int sc : rt->components(id, "SpriteComponent", "", true)) {
auto& sa = rt->comp(sc)->attrs; auto& sa = rt->comp(sc)->attrs;
if (sa["additive"] == "1" && sa["kill_entity_after_finished"] == "1") { pendingFlares.push_back(id); break; } 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 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<std::string> 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) 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; auto& at = rt->comp(xc)->attrs;
if (at["trigger"] != "ON_TIMER") continue; 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); 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 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]; ProjTwin& tw = projTwins[ti];
const float* f = reinterpret_cast<const float*>(fprojReadPtr + (size_t)tw.slot * 96); const float* f = reinterpret_cast<const float*>(fprojReadPtr + (size_t)tw.slot * 96);
const int32_t* d = reinterpret_cast<const int32_t*>(f + 12); const int32_t* d = reinterpret_cast<const int32_t*>(f + 12);
ng::RtEntity* te = rt->ent(tw.id); 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]; 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); 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) { if (rt && cellsReadPtr && cellsViewX == viewX && cellsViewY == viewY) {
PERFMARK("loose ground"); 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 if (rt) { // the AreaDamageComponents of the other entities (not projectiles) in the loaded area
std::set<int> twinIds; for (const ProjTwin& tw : projTwins) twinIds.insert(tw.id);
for (int aid : rt->with_component("AreaDamageComponent")) { for (int aid : rt->with_component("AreaDamageComponent")) {
if (twinIds.count(aid)) continue; if (findTwin(aid)) continue;
ng::RtEntity* ae = rt->ent(aid); 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; 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); areaDamageOf(aid, ae);
@@ -9824,10 +9904,9 @@ int main(int argc, char** argv) {
emitFrom(rt->player(), rt->player()); 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 { // 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<int> twinIds; for (const ProjTwin& tw : projTwins) twinIds.insert(tw.id);
for (int eid : rt->with_component("ProjectileComponent")) { for (int eid : rt->with_component("ProjectileComponent")) {
const ng::RtEntity* pe = rt->ent(eid); 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) 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; if (pe->gpu >= 0 && !body) continue;
const auto pcs = rt->components(eid, "ProjectileComponent", "", true); 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 { // 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")) { for (int eid : rt->with_component("ParticleEmitterComponent")) {
const ng::RtEntity* ee = rt->ent(eid); 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) 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); if (rt->alive(ee->parent)) emitFrom(ee->parent, eid);
continue; 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) 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); for (int kid : killQueue) rt->kill(kid);
killQueue.clear(); killQueue.clear();
for (const DeferredBoom& b : hostExplodeDeferred) hostExplode(b.cfg, b.x, b.y, b.who); 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; 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 // 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 // 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)) { for (int ec : rt->components(sid, "EnergyShieldComponent", "", false)) {
if (!rt->component_enabled(ec)) continue; if (!rt->component_enabled(ec)) continue;
auto& at = rt->comp(ec)->attrs; 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); 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); const bool active = energy >= num("energy_required_to_shield", 0.2);
int root = sid; while (rt->parent(root)) root = rt->parent(root); 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["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(); 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 // 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) // radial light of the sprite's colour over its animation (FAnim slots 256..511 belong to the host)
for (int fid : pendingFlares) { for (int fid : pendingFlares) {
@@ -9950,6 +10065,7 @@ int main(int argc, char** argv) {
} }
} }
pendingFlares.clear(); 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' { // 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) // 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; 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<float> holes(1 + 64 * 6, 0.0f); std::vector<float> holes(1 + 64 * 6, 0.0f);
int nh = 0; 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; if (nh >= 64) break;
const ng::RtEntity* he = rt->ent(hid); const ng::RtEntity* he = rt->ent(hid);
if (!he || !he->alive) continue; 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); 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(); } 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 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; tw.dirty = false;
const std::string sig = twinSig(tw.id); 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); 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 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 (ss.entity && !rt->alive(ss.entity)) continue;
if (it == scriptShotNode.end() && psim) it = scriptShotNode.emplace(ss.file, gfx.persistent_node(*psim, ss.file)).first; std::string file = ss.file;
if (it == scriptShotNode.end() || it->second < 0) continue; 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; int slot = -1;
if (ss.shooter == rt->player()) slot = 0; if (ss.shooter == rt->player()) slot = 0;
else for (int k = 1; k < entN && slot < 0; ++k) if (slotToId[k] == ss.shooter) slot = k; 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); 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) 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; 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 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.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; 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 bool selNew = g_in.select_new; g_in.select_new = false;
const int wheelSteps = g_in.wheel; g_in.wheel = 0; const int wheelSteps = g_in.wheel; g_in.wheel = 0;
const int selBefore = activeItem >= 0 ? 4 + activeItem : activeWand; 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; ng::ShotResult shot;
if (gun->fire(frame, shot)) { if (gun->fire(frame, shot)) {
++shotsFired; ++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); 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"); 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 { // 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); const ng::RtEntity* te = rt->ent(tid);
if (!te || !te->alive || te->parent) continue; 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 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; 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<double, std::milli>(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); } 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<double, std::milli>(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; 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<const int32_t*>(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<const int32_t*>(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.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(); 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); 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; continue;
} }
const int twin = spawnTwin(pd, e[0], e[1]); const int retained = e2[3] < 0.0f ? -(int)e2[3] : 0;
if (!twin) continue; 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<const int32_t*>(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 sslot = (int)e2[2];
const int shooterId = sslot == 1 ? rt->player() : (sslot > 1 && sslot <= entN ? slotToId[sslot - 1] : 0); 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); 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) 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); 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 (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) 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; 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 if (e[2] == -5.0f && k + 1 < std::min(head, 512)) { // the projectile of a twin died
const float* e2 = xe + 4 * (k + 1); const float* e2 = xe + 4 * (k + 1);
++k; ++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; 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) 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; uint8_t* mp = nullptr;
VK(vkMapMemory(g.dev, g.stagingFrame.mem, 0, VK_WHOLE_SIZE, 0, (void**)&mp)); VK(vkMapMemory(g.dev, g.stagingFrame.mem, 0, VK_WHOLE_SIZE, 0, (void**)&mp));
size_t used = 0; size_t used = 0;
std::vector<VkBufferCopy> copies;
copies.reserve(queuedUploads.size());
const Buffer* batchDestination = nullptr;
VkDeviceSize previousEnd = 0;
std::set<const Buffer*> 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) { for (const QueuedUpload& q : queuedUploads) {
std::memcpy(mp + used, q.data.data(), q.data.size()); std::memcpy(mp + used, q.data.data(), q.data.size());
const VkBufferCopy bc{used, q.off, q.data.size()}; // Consecutive, non-overlapping regions can share one Vulkan command.
vkCmdCopyBuffer(g.cmd, g.stagingFrame.buf, q.dst->buf, 1, &bc); // (in order: a later piece over the same bytes wins, as before) // 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(); used += q.data.size();
} }
flushCopies();
vkUnmapMemory(g.dev, g.stagingFrame.mem); vkUnmapMemory(g.dev, g.stagingFrame.mem);
queuedUploads.clear(); 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}; 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};
+1 -1
View File
@@ -14,7 +14,7 @@ namespace ng {
class PerfRecorder { class PerfRecorder {
public: public:
static constexpr int kMaxSections = 32, kGpu = 20, kCounts = 12; static constexpr int kMaxSections = 64, kGpu = 20, kCounts = 12;
struct Frame { struct Frame {
int frame = 0; 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 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
+1 -1
View File
@@ -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; } 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<uint8_t> bytes) const { overrides_[lower_path(path)] = std::move(bytes); } void Wak::set_override(std::string_view path, std::vector<uint8_t> 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; } bool Wak::overridden(std::string_view path) const { return overrides_.count(lower_path(path)) != 0; }
// Loose files beside the archive: the mods (mods/<id>/...) and the files of data/ the archive does not hold (the install ships data/fonts/font_damage.xml, // Loose files beside the archive: the mods (mods/<id>/...) and the files of data/ the archive does not hold (the install ships data/fonts/font_damage.xml,
+2
View File
@@ -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. // 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<uint8_t> bytes) const; void set_override(std::string_view path, std::vector<uint8_t> bytes) const;
bool overridden(std::string_view path) 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; } void set_loose_root(const std::filesystem::path& root) const { loose_root_ = root; }
const std::filesystem::path& loose_root() const { return loose_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 bool exists(std::string_view path) const; // in the archive, overridden, or a loose mods/ file
@@ -39,6 +40,7 @@ private:
std::vector<WakEntry> entries_; std::vector<WakEntry> entries_;
std::unordered_map<std::string, size_t> index_; std::unordered_map<std::string, size_t> index_;
mutable std::unordered_map<std::string, std::vector<uint8_t>> overrides_; // by lower-case path mutable std::unordered_map<std::string, std::vector<uint8_t>> overrides_; // by lower-case path
mutable uint64_t override_revision_ = 0;
mutable std::filesystem::path loose_root_; mutable std::filesystem::path loose_root_;
bool read_loose(std::string_view path, std::vector<uint8_t>& out) const; bool read_loose(std::string_view path, std::vector<uint8_t>& out) const;
std::unordered_map<std::string, size_t> 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) std::unordered_map<std::string, size_t> 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)
+255 -56
View File
@@ -3,6 +3,8 @@
#include "entity/herds.h" #include "entity/herds.h"
#include <algorithm> #include <algorithm>
#include <charconv>
#include <span>
#include <cmath> #include <cmath>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
@@ -39,6 +41,8 @@ bool is_bool_key(const std::string& k) {
bool parse_number(const std::string& s, double& out) { bool parse_number(const std::string& s, double& out) {
if (s.empty()) return false; 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; char* end = nullptr;
out = std::strtod(s.c_str(), &end); out = std::strtod(s.c_str(), &end);
return end && *end == 0; return end && *end == 0;
@@ -46,8 +50,11 @@ bool parse_number(const std::string& s, double& out) {
std::string number_string(double v) { std::string number_string(double v) {
char b[64]; char b[64];
if (std::floor(v) == v && std::fabs(v) < 1e15) std::snprintf(b, sizeof b, "%lld", (long long)v); const auto text = std::floor(v) == v && std::fabs(v) < 1e15
else std::snprintf(b, sizeof b, "%.9g", v); ? 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; return b;
} }
@@ -116,7 +123,10 @@ RtEntity* EntityRuntime::ent(int id) {
RtComponent* EntityRuntime::comp(int id) { RtComponent* EntityRuntime::comp(int id) {
auto it = comps_.find(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<std::string, std::string> attrs) { int EntityRuntime::add_comp_internal(int entity, const std::string& type, std::map<std::string, std::string> attrs) {
@@ -133,9 +143,35 @@ int EntityRuntime::add_comp_internal(int entity, const std::string& type, std::m
c.attrs = std::move(attrs); c.attrs = std::move(attrs);
comps_[c.id] = c; comps_[c.id] = c;
e->comps.push_back(c.id); 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; 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) { void EntityRuntime::regen_item_actions(int player) {
for (auto& [id, e] : ents_) { for (auto& [id, e] : ents_) {
if (!e.alive || components(id, "ItemActionComponent", "", true).empty()) continue; 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; e.name = def->name;
for (auto& t : split(def->tags, ',')) e.tags.insert(t); for (auto& t : split(def->tags, ',')) e.tags.insert(t);
} }
if (RtEntity* previous = ent(id)) for (int cid : std::vector<int>(previous->comps)) erase_comp_internal(cid);
native_guard_states_.erase(id);
ents_[id] = e; ents_[id] = e;
if (spawn_log) spawn_log->push_back(id); if (spawn_log) spawn_log->push_back(id);
bool has_item = false; 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; 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<uint8_t> 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<LVal>& args) { bool EntityRuntime::run_script_callback(int entity, int comp_id, const std::string& file, const std::string& function, const std::vector<LVal>& args) {
if (on_gpu_script && on_gpu_script(entity, comp_id, file, function, args)) return true; 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 int saved_e = updated_entity_, saved_c = updated_comp_;
const std::string saved_env = host_.env; const std::string saved_env = host_.env;
updated_entity_ = entity; updated_comp_ = comp_id; 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<std::string, std::string>::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; 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 (ok && !function.empty()) ok = host_.call_function(function, args, &err);
if (host_.env.size() > 1 && host_.env[0] == 'n') host_.drop_env(host_.env); if (host_.env.size() > 1 && host_.env[0] == 'n') host_.drop_env(host_.env);
host_.env = saved_env; host_.env = saved_env;
@@ -381,15 +500,39 @@ bool EntityRuntime::run_script_callback(int entity, int comp_id, const std::stri
return ok; return ok;
} }
std::vector<int> EntityRuntime::with_component(const std::string& type) { std::vector<int> EntityRuntime::with_component(std::string_view type) {
std::vector<int> r; std::vector<int> r;
for (auto& [id, e] : ents_) { const auto typed = entities_by_component_.find(type);
if (!e.alive) continue; if (typed != entities_by_component_.end()) for (const auto& [id, count] : typed->second)
for (int cid : e.comps) { RtComponent* c = comp(cid); if (c && c->type == type) { r.push_back(id); break; } } if (RtEntity* e = ent(id); e && e->alive) r.push_back(id);
}
return r; 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<int> EntityRuntime::with_component_field(const std::string& type, const std::string& key, const std::optional<std::string>& 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<int> 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) { void EntityRuntime::run_death_scripts(int id, int damage_types, int killer) {
std::vector<std::pair<int, std::string>> scripts; std::vector<std::pair<int, std::string>> scripts;
for (int cid : ents_[id].comps) { 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<int> EntityRuntime::damage_about_entities() { std::set<int> EntityRuntime::damage_about_entities() {
std::set<int> r; std::set<int> r;
for (auto& [cid, c] : comps_) { for (int id : with_component_field("LuaComponent", "script_damage_about_to_be_received")) for (int cid : components(id, "LuaComponent", "", false)) {
if (c.type != "LuaComponent" || !c.enabled) continue; const auto& c = *comp(cid);
auto it = c.attrs.find("script_damage_about_to_be_received"); auto it = c.attrs.find("script_damage_about_to_be_received");
if (it == c.attrs.end() || it->second.empty()) continue; 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; } 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::GameEffectRef> EntityRuntime::all_game_effects() { std::vector<EntityRuntime::GameEffectRef> EntityRuntime::all_game_effects() {
std::vector<GameEffectRef> r; std::vector<GameEffectRef> r;
for (auto& [eid, e] : ents_) { for (int eid : with_component("GameEffectComponent")) {
if (!e.alive) continue; const RtEntity& e = *ent(eid);
for (int cid : e.comps) { for (int cid : components(eid, "GameEffectComponent", "", false)) {
RtComponent* c = comp(cid); RtComponent* c = comp(cid);
if (!c || c->type != "GameEffectComponent" || !c->enabled) continue; if (!c || c->type != "GameEffectComponent" || !c->enabled) continue;
GameEffectRef g; GameEffectRef g;
@@ -688,13 +831,19 @@ std::vector<int> EntityRuntime::children(int id) {
} }
std::vector<int> EntityRuntime::components(int entity, const std::string& type, const std::string& tag, bool include_disabled) { std::vector<int> 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<int> EntityRuntime::components_view(int entity, std::string_view type, const std::string& tag, bool include_disabled) {
std::vector<int> r; std::vector<int> r;
RtEntity* e = ent(entity); RtEntity* e = ent(entity);
if (!e) return r; 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); RtComponent* c = comp(cid);
if (!c) continue; if (!c) continue;
if (!type.empty() && c->type != type) continue;
if (!include_disabled && !c->enabled) continue; if (!include_disabled && !c->enabled) continue;
if (!tag.empty() && !c->tags.count(tag)) continue; if (!tag.empty() && !c->tags.count(tag)) continue;
r.push_back(cid); r.push_back(cid);
@@ -719,14 +868,14 @@ void EntityRuntime::remove_component(int cid) {
if (!c) return; if (!c) return;
} }
if (c->type == "LuaComponent" && c->attrs["vm_type"] == "ONE_PER_COMPONENT_INSTANCE") host_.drop_env("c" + std::to_string(cid)); 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()); erase_comp_internal(cid);
comps_.erase(cid);
} }
void EntityRuntime::set_component_enabled(int cid, bool en) { void EntityRuntime::set_component_enabled(int cid, bool en) {
if (RtComponent* c = comp(cid)) { if (RtComponent* c = comp(cid)) {
const bool changed = c->enabled != en; const bool changed = c->enabled != en;
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 && 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 ) 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)}); 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) { void EntityRuntime::set_value(int cid, const std::string& key, const LVal& v) {
RtComponent* c = comp(cid); RtComponent* c = comp(cid);
if (!c) return; if (!c) return;
if (v.t == LVal::T::Nil) return;
update_component_fields(*c, -1, key);
switch (v.t) { switch (v.t) {
case LVal::T::Bool: c->attrs[key] = v.b ? "1" : "0"; break; 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::Num: c->attrs[key] = number_string(v.n); break;
case LVal::T::Str: c->attrs[key] = v.s; break; case LVal::T::Str: c->attrs[key] = v.s; break;
case LVal::T::Nil: return; 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); 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; const double cy = py - 4.0;
int best = 0; int best = 0;
double best_d = 1e300; double best_d = 1e300;
for (auto& [id, e] : ents_) { for (int id : with_component("ItemComponent")) {
if (!e.alive || id == player_id_ || e.parent) continue; const RtEntity& e = *ent(id);
if (id == player_id_ || e.parent) continue;
int item_comp = 0; 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; if (!item_comp) continue;
RtComponent* ic = comp(item_comp); RtComponent* ic = comp(item_comp);
if (ic->attrs["is_pickable"] == "0" || ic->attrs["auto_pickup"] == "1") continue; if (ic->attrs["is_pickable"] == "0" || ic->attrs["auto_pickup"] == "1") continue;
@@ -878,10 +1034,12 @@ std::vector<int> EntityRuntime::pickup_step(double px, double py, bool interact)
int best = 0; int best = 0;
double best_d = 1e300; double best_d = 1e300;
std::vector<int> auto_items; std::vector<int> auto_items;
for (auto& [id, e] : ents_) { for (int id : with_component("ItemComponent")) {
if (!e.alive || id == player_id_) continue; const RtEntity& e = *ent(id);
if (id == player_id_) continue;
int item_comp = 0; 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; if (!item_comp) continue;
RtComponent* ic = comp(item_comp); RtComponent* ic = comp(item_comp);
if (ic->attrs["is_pickable"] == "0") continue; if (ic->attrs["is_pickable"] == "0") continue;
@@ -899,9 +1057,10 @@ std::vector<int> EntityRuntime::pickup_step(double px, double py, bool interact)
std::string EntityRuntime::interact_hint(double px, double py) { std::string EntityRuntime::interact_hint(double px, double py) {
std::string best; std::string best;
double best_d = 22.0; double best_d = 22.0;
for (auto& [id, e] : ents_) { for (int id : with_component("ItemComponent")) {
if (!e.alive || id == player_id_) continue; const RtEntity& e = *ent(id);
for (int cid : e.comps) { if (id == player_id_) continue;
for (int cid : components(id, "ItemComponent", "", false)) {
RtComponent* c = comp(cid); RtComponent* c = comp(cid);
if (!c || c->type != "ItemComponent" || !c->enabled || c->attrs["is_pickable"] == "0" || c->attrs["auto_pickup"] == "1") continue; 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)); 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<int(double, double, d
// otherwise Message_MaterialAreaCheckerSuccess when every cell is one of them, or at least count_min cells when count_min > 0 // otherwise Message_MaterialAreaCheckerSuccess when every cell is one of them, or at least count_min cells when count_min > 0
struct Job { int entity, comp; bool failure; double x0, y0, x1, y1; std::string m1, m2; int count_min; }; struct Job { int entity, comp; bool failure; double x0, y0, x1, y1; std::string m1, m2; int count_min; };
std::vector<Job> jobs; std::vector<Job> jobs;
for (auto& [id, e] : ents_) { for (int id : with_component("MaterialAreaCheckerComponent")) {
if (!e.alive) continue; const RtEntity& e = *ent(id);
for (int cid : e.comps) { for (int cid : components(id, "MaterialAreaCheckerComponent", "", false)) {
RtComponent* c = comp(cid); RtComponent* c = comp(cid);
if (!c || c->type != "MaterialAreaCheckerComponent" || !c->enabled) continue; if (!c || c->type != "MaterialAreaCheckerComponent" || !c->enabled) continue;
int every = (int)std::atof(c->attrs["update_every_x_frame"].c_str()); 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<int(double, double, d
} }
bool EntityRuntime::teleport_step(double px, double py, double& tx, double& ty, int* teleporter) { bool EntityRuntime::teleport_step(double px, double py, double& tx, double& ty, int* teleporter) {
for (auto& [id, e] : ents_) { for (int id : with_component("TeleportComponent")) {
if (!e.alive || id == player_id_) continue; const RtEntity& e = *ent(id);
if (id == player_id_) continue;
RtComponent* tc = nullptr; RtComponent* tc = nullptr;
RtComponent* hb = nullptr; RtComponent* hb = nullptr;
for (int cid : e.comps) { for (int cid : e.comps) {
@@ -1018,11 +1178,15 @@ void EntityRuntime::load_entities(int id, int lc, bool notify) { // a LoadEnti
} }
void EntityRuntime::tick() { void EntityRuntime::tick() {
static const bool runtimeProfile = std::getenv("NG_RUNTIMEPROF") != nullptr;
static double runtimeTimes[3] = {};
static int runtimeFrames = 0;
auto runtimeMark = runtimeProfile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
auto runtimeStamp = [&](int phase) { if (runtimeProfile) { const auto now = std::chrono::steady_clock::now(); runtimeTimes[phase] += std::chrono::duration<double, std::milli>(now - runtimeMark).count(); runtimeMark = now; } };
{ // LoadEntitiesComponent timers (timeout_frames): the load when mTimerTriggerFrame comes { // LoadEntitiesComponent timers (timeout_frames): the load when mTimerTriggerFrame comes
std::vector<std::pair<int, int>> due; std::vector<std::pair<int, int>> due;
for (auto& [id, e] : ents_) { for (int id : with_component("LoadEntitiesComponent")) {
if (!e.alive) continue; for (int cid : components(id, "LoadEntitiesComponent", "", false)) {
for (int cid : e.comps) {
RtComponent* c = comp(cid); RtComponent* c = comp(cid);
if (!c || c->type != "LoadEntitiesComponent" || !c->enabled) continue; if (!c || c->type != "LoadEntitiesComponent" || !c->enabled) continue;
const auto it = c->attrs.find("mTimerTriggerFrame"); const auto it = c->attrs.find("mTimerTriggerFrame");
@@ -1043,15 +1207,16 @@ void EntityRuntime::tick() {
std::vector<int> gone; std::vector<int> 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 (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 id : gone) {
for (int cid : ents_[id].comps) comps_.erase(cid); for (int cid : std::vector<int>(ents_[id].comps)) erase_comp_internal(cid);
ents_.erase(id); ents_.erase(id);
native_guard_states_.erase(id);
} }
} }
std::vector<int> expire; std::vector<int> expire;
std::vector<std::pair<int, int>> endedEffects; // (effect entity, GameEffectComponent) whose frames ran out this update std::vector<std::pair<int, int>> endedEffects; // (effect entity, GameEffectComponent) whose frames ran out this update
for (auto& [id, e] : ents_) { for (int id : with_component("LifetimeComponent")) {
if (!e.alive) continue; const RtEntity& e = *ent(id);
for (int cid : e.comps) { for (int cid : components(id, "LifetimeComponent", "", false)) {
RtComponent* c = comp(cid); RtComponent* c = comp(cid);
if (!c || c->type != "LifetimeComponent" || !c->enabled) continue; if (!c || c->type != "LifetimeComponent" || !c->enabled) continue;
const double life = std::atof(c->attrs["lifetime"].c_str()); const double life = std::atof(c->attrs["lifetime"].c_str());
@@ -1084,9 +1249,9 @@ void EntityRuntime::tick() {
} }
return animFrames[key] = len; return animFrames[key] = len;
}; };
for (auto& [id, e] : ents_) { for (int id : with_component_field("SpriteComponent", "kill_entity_after_finished", "1")) {
if (!e.alive) continue; const RtEntity& e = *ent(id);
for (int cid : e.comps) { for (int cid : components(id, "SpriteComponent", "", false)) {
RtComponent* c = comp(cid); RtComponent* c = comp(cid);
if (!c || c->type != "SpriteComponent" || !c->enabled) continue; if (!c || c->type != "SpriteComponent" || !c->enabled) continue;
auto k = c->attrs.find("kill_entity_after_finished"); auto k = c->attrs.find("kill_entity_after_finished");
@@ -1119,9 +1284,8 @@ void EntityRuntime::tick() {
} }
for (int id : dead) kill(id); 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 for (int eid : with_component("GameEffectComponent")) { // effect durations: -1 and 0 never run out
if (!e.alive) continue; for (int cid : components(eid, "GameEffectComponent", "", false)) {
for (int cid : e.comps) {
RtComponent* c = comp(cid); RtComponent* c = comp(cid);
if (!c || c->type != "GameEffectComponent" || !c->enabled) continue; if (!c || c->type != "GameEffectComponent" || !c->enabled) continue;
const int fr = std::atoi(c->attrs["frames"].c_str()); const int fr = std::atoi(c->attrs["frames"].c_str());
@@ -1160,6 +1324,7 @@ void EntityRuntime::tick() {
} }
} }
} }
runtimeStamp(0);
if (!scripts_on_) return; if (!scripts_on_) return;
// ---- the scripts of the entities in the loaded area ---- // ---- the scripts of the entities in the loaded area ----
std::vector<int> ids; std::vector<int> ids;
@@ -1168,9 +1333,23 @@ void EntityRuntime::tick() {
for (int id : ids) { for (int id : ids) {
RtEntity* e = ent(id); RtEntity* e = ent(id);
if (!e || !e->alive) continue; if (!e || !e->alive) continue;
const std::vector<int> 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<int> snapshot;
std::span<const int> comps;
if (single && !hasCollisions) comps = std::span<const int>(&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<std::string> hit_scripts; // script_collision_trigger_hit of the entity's LuaComponents std::vector<std::string> 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); 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"]); 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); RtComponent* c = comp(cid);
if (!c || !c->enabled) continue; 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 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; if (file.empty()) continue;
const int every = std::atoi(c->attrs["execute_every_n_frame"].c_str()); const int every = schedule.every, times = schedule.times, done = schedule.done;
const int times = std::atoi(c->attrs["execute_times"].c_str()); // < 1: no limit
const int done = std::atoi(c->attrs["mTimesExecuted"].c_str());
if (every <= 0 || (times > 0 && done >= times)) continue; if (every <= 0 || (times > 0 && done >= times)) continue;
if ((frame_ + id) % every != 0) 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_; ++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 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<std::string, std::pair<double, int>> sprofT; static std::map<std::string, std::pair<double, int>> sprofT;
const auto st0 = sprof ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; 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, "", {}); else run_script_callback(id, cid, file, "", {});
if (sprof) { if (sprof) {
auto& pt = sprofT[file]; pt.first += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - st0).count(); ++pt.second; auto& pt = sprofT[file]; pt.first += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - st0).count(); ++pt.second;
@@ -1209,6 +1399,7 @@ void EntityRuntime::tick() {
if (!alive(id)) break; if (!alive(id)) break;
if (RtComponent* c2 = comp(cid)) { if (RtComponent* c2 = comp(cid)) {
c2->attrs["mTimesExecuted"] = std::to_string(done + 1); 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) 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) } 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 // 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. // 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. // 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; const std::string saved_env = host_.env;
std::set<std::string> woken; std::set<std::string> woken;
std::vector<std::pair<int, int>> vms; std::vector<std::pair<int, int>> 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) { for (auto& [cid, eid] : vms) {
RtComponent* c = comp(cid); RtComponent* c = comp(cid);
if (!c || !alive(eid)) continue; if (!c || !alive(eid)) continue;
@@ -1269,6 +1466,8 @@ void EntityRuntime::tick() {
host_.env = saved_env; host_.env = saved_env;
updated_entity_ = saved_e; updated_comp_ = saved_c; 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 } // namespace ng
+45 -2
View File
@@ -4,8 +4,12 @@
// synchronised by the host; item pickup, chest opening and loot spawning run here, through the original scripts. // synchronised by the host; item pickup, chest opening and loot spawning run here, through the original scripts.
#include <functional> #include <functional>
#include <map> #include <map>
#include <optional>
#include <set> #include <set>
#include <string> #include <string>
#include <string_view>
#include <tuple>
#include <unordered_map>
#include <vector> #include <vector>
#include "entity/entity.h" #include "entity/entity.h"
@@ -19,6 +23,9 @@ struct RtComponent {
std::string type; std::string type;
std::map<std::string, std::string> attrs; std::map<std::string, std::string> attrs;
bool enabled = true; bool enabled = true;
uint64_t value_revision = 0;
int gpu_lifetime = 0;
bool gpu_lifetime_pending = false; // comp() materializes this GPU mirror before exposing the component
std::set<std::string> tags; std::set<std::string> tags;
}; };
@@ -29,6 +36,9 @@ struct RtEntity {
double x = 0, y = 0, rot = 0, sx = 1, sy = 1; double x = 0, y = 0, rot = 0, sx = 1, sy = 1;
bool alive = true; bool alive = true;
std::vector<int> comps; std::vector<int> comps;
uint64_t component_revision = 0;
int single_lua_component = 0, lua_component_count = 0, collision_component_count = 0;
std::map<std::string, std::vector<int>, std::less<>> comps_by_type;
int parent = 0; int parent = 0;
int born = 0; int born = 0;
int died = -1; // the frame of kill(); the record and its components are erased a few frames later (tick) 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 tick();
void load_entities(int id, int lc, bool notify); // a LoadEntitiesComponent loads its entities (at spawn, or when its timeout_frames timer comes) 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) 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. // 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; } 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. // 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). // Kills every entity of the world except the player and what the player carries (the area was left).
void clear_world_entities(); void clear_world_entities();
std::vector<int> with_file(const std::string& part); std::vector<int> with_file(const std::string& part);
std::vector<int> with_component(const std::string& type); // living entities with a component of the type // living entities whose file name contains the text std::vector<int> with_component(std::string_view type); // living entities with a component of the type
// Incremental indexes for fields changed through set_value (null value: nonempty).
std::vector<int> with_component_field(const std::string& type, const std::string& key, const std::optional<std::string>& value = std::nullopt);
int player() const { return player_id_; } int player() const { return player_id_; }
void set_player(int id) { player_id_ = 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 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; int add_gun_action(int wand, const std::string& action_id, bool permanent) override;
std::vector<int> components(int entity, const std::string& type, const std::string& tag, bool include_disabled) override; std::vector<int> components(int entity, const std::string& type, const std::string& tag, bool include_disabled) override;
std::vector<int> components(int entity, const char* type, const std::string& tag, bool include_disabled) { return components_view(entity, type, tag, include_disabled); }
int add_component(int entity, const std::string& type, const std::map<std::string, std::string>& attrs) override; int add_component(int entity, const std::string& type, const std::map<std::string, std::string>& attrs) override;
void remove_component(int comp) override; void remove_component(int comp) override;
void set_component_enabled(int comp, bool enabled) override; void set_component_enabled(int comp, bool enabled) override;
@@ -229,19 +244,47 @@ public:
private: private:
int add_comp_internal(int entity, const std::string& type, std::map<std::string, std::string> attrs); int add_comp_internal(int entity, const std::string& type, std::map<std::string, std::string> attrs);
void erase_comp_internal(int component);
std::vector<int> components_view(int entity, std::string_view type, const std::string& tag, bool include_disabled);
bool run_script_callback(int entity, int comp, const std::string& file, const std::string& function, const std::vector<LVal>& args); bool run_script_callback(int entity, int comp, const std::string& file, const std::string& function, const std::vector<LVal>& args);
bool run_native_guard(int entity, const std::string& file);
void update_component_fields(const RtComponent& component, int delta, std::string_view key = {});
const Wak& wak_; const Wak& wak_;
LuaHost& host_; LuaHost& host_;
EntityDb db_; EntityDb db_;
std::map<int, RtEntity> ents_; std::map<int, RtEntity> ents_;
std::map<int, RtComponent> comps_; std::unordered_map<int, RtComponent> comps_;
std::map<std::string, std::map<int, int>, std::less<>> entities_by_component_;
using FieldQuery = std::tuple<std::string, std::string, std::optional<std::string>>;
std::map<FieldQuery, std::map<int, int>> component_fields_;
struct LuaSchedule {
uint64_t revision = uint64_t(-1);
std::string file;
int every = 1, times = 0, done = 0, next = -1;
bool call_init = false, inited = false, remove = false;
};
std::unordered_map<int, LuaSchedule> lua_schedules_;
struct NativeGuardState {
uint64_t revision = uint64_t(-1);
int angle_component = 0;
const std::string *angle = nullptr, *rotation = nullptr, *owner = nullptr;
std::string rotation_text, owner_text;
double rotation_value = 0, owner_value = 0;
bool has_storage = false;
};
std::unordered_map<int, NativeGuardState> native_guard_states_;
double act_cx_ = 0, act_cy_ = 0, act_hw_ = 0, act_hh_ = 0; double act_cx_ = 0, act_cy_ = 0, act_hw_ = 0, act_hh_ = 0;
bool scripts_on_ = false; bool scripts_on_ = false;
long script_runs_ = 0; 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) 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 next_ent_ = kDynamicIdBase, next_comp_ = 1, frame_ = 0, player_id_ = 0, world_state_ = 0;
int updated_entity_ = 0, updated_comp_ = 0; int updated_entity_ = 0, updated_comp_ = 0;
uint64_t component_revision_ = 0;
std::vector<GameMessage> messages_; std::vector<GameMessage> messages_;
std::vector<std::string> errors_; std::vector<std::string> errors_;
std::vector<int> pending_scripts_; // entities whose on-added scripts still have to run std::vector<int> pending_scripts_; // entities whose on-added scripts still have to run
+21 -7
View File
@@ -94,11 +94,13 @@ int GpuFx::type_for(const ProjDef& d, ProjectileSim* sim) {
if (it != typeOf_.end()) return it->second; if (it != typeOf_.end()) return it->second;
if (types_ >= kMaxTypes) return 0; 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 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<uint32_t> packedEmitters;
packedEmitters.reserve(d.fx.emitters.size() * kEmitStride * 4);
for (const EmitterDef& e : d.fx.emitters) { 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); 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}; 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 // 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); 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); 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) 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)); 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); 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; 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}; 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; 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); 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_.resize(types_ + 1);
typeEffects_[types_] = d.hit_effects; typeEffects_[types_] = d.hit_effects;
typeDef_.resize(types_ + 1); 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); 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; 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; 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 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; return true;
} }
+2 -1
View File
@@ -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). // 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) 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. // 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. // 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); 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. // 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<const ProjDef*> typeDef_; // the definition behind every projectile type (stable: owned by the ProjectileSim map) std::vector<const ProjDef*> typeDef_; // the definition behind every projectile type (stable: owned by the ProjectileSim map)
std::vector<std::vector<ProjDef::HitEffect>> typeEffects_; // the hit effects of every projectile type std::vector<std::vector<ProjDef::HitEffect>> typeEffects_; // the hit effects of every projectile type
std::map<std::string, int> boomOf_; std::map<std::string, int> boomOf_;
std::map<std::vector<uint32_t>, std::pair<int, int>> emitterOf_; // packed definitions -> shared table range
std::map<const void*, int> texOf_; std::map<const void*, int> texOf_;
}; };
+1
View File
@@ -173,6 +173,7 @@ std::vector<GunRuntime::CardRef> GunRuntime::cards(int wand) {
} }
bool GunRuntime::equip(int wand) { bool GunRuntime::equip(int wand) {
if (!wand) { wand_ = 0; return true; }
if (!loaded_ || !rt_.alive(wand) || !ability(wand)) return false; if (!loaded_ || !rt_.alive(wand) || !ability(wand)) return false;
auto num = [&](const char* k, double d) { return abil(wand, k, d); }; auto num = [&](const char* k, double d) { return abil(wand, k, d); };
std::ostringstream lua; std::ostringstream lua;
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include "entity/entity.h"
#include <cstdlib>
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;
}
}
+25 -1
View File
@@ -24,6 +24,20 @@ constexpr double kDeg = 3.14159265358979323846 / 180.0;
} // namespace } // 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<std::string>()(state));
}
const ProjDef* ProjectileSim::def(const std::string& file) { const ProjDef* ProjectileSim::def(const std::string& file) {
auto it = defs_.find(file); auto it = defs_.find(file);
if (it != defs_.end()) return &it->second; 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 // 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) { 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"}) 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 (!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; 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; std::istringstream tags(entity.tags);std::string tag;
while(std::getline(tags,tag,',')) { while(std::getline(tags,tag,',')) {
auto begin=tag.find_first_not_of(' '), end=tag.find_last_not_of(' '); 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; unsigned velocity_count=0;
for(const auto* component:entity.all("VelocityComponent"))if(component->get("_enabled","1")!="0")++velocity_count; 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.friendly_fire = attr(pc, "friendly_fire", 0) != 0;
d.dont_hit_player = attr(pc, "dont_hit_player", 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) 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(); d.physics = !e->all("PhysicsBodyComponent").empty();
if (d.physics) d.on_collision_die = false; 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; d.explode_on_death = attr(pc, "on_death_explode", 0) != 0 || attr(pc, "on_lifetime_out_explode", 0) != 0;
+4 -1
View File
@@ -26,6 +26,7 @@ struct ProjDef {
bool on_collision_die = true; bool on_collision_die = true;
int bounces_left = 0; // ProjectileComponent bounces_left: grenades bounce this often before they explode 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 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 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 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 // 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 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 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 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 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 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; bool explode_on_death = false;
@@ -147,6 +148,8 @@ public:
const ProjDef* def(const std::string& file); 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`. // 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) 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_; } 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. // 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); const ProjDef* def(const std::string& file, const std::string& extras);
+109 -4
View File
@@ -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) : ""; } std::string str(lua_State* L, int i) { return lua_isstring(L, i) ? lua_tostring(L, i) : ""; }
// ---- file loading ---- // ---- 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) { bool load_from_wak(LuaHost& h, lua_State* L, const std::string& path, std::string* err) {
static std::unordered_map<std::string, std::vector<uint8_t>> cache; // the scripts of the entities run again and again: read the archive once static std::unordered_map<std::string, std::vector<uint8_t>> 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<std::string, std::vector<uint8_t>> 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); auto it = cache.find(path);
if (it == cache.end()) { if (it == cache.end()) {
std::vector<uint8_t> raw; std::vector<uint8_t> raw;
@@ -117,6 +126,14 @@ bool load_from_wak(LuaHost& h, lua_State* L, const std::string& path, std::strin
lua_pop(L, 1); lua_pop(L, 1);
return false; return false;
} }
std::vector<uint8_t> bytecode;
const auto writer = [](lua_State*, const void* bytes, size_t size, void* output) -> int {
auto& out = *static_cast<std::vector<uint8_t>*>(output);
const auto* begin = static_cast<const uint8_t*>(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; return true;
} }
@@ -461,6 +478,21 @@ LuaHost::LuaHost(const Wak& wak, GameRandom& rng) : wak_(wak), rng_(rng) {
L_ = luaL_newstate(); L_ = luaL_newstate();
luaL_openlibs(L_); luaL_openlibs(L_);
register_api(); 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). // 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). // 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[] = { static const struct { uint32_t rgba; const char* fn; } kDefaults[] = {
@@ -554,7 +586,7 @@ void LuaHost::register_api() {
register_host_natives(*this); 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 // 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) { 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) { void LuaHost::drop_env(const std::string& key) {
++execution_revision_;
if (key.empty()) return; 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"); lua_getfield(L_, LUA_REGISTRYINDEX, "ng_envs");
if (lua_istable(L_, -1)) { lua_pushnil(L_); lua_setfield(L_, -2, key.c_str()); } if (lua_istable(L_, -1)) { lua_pushnil(L_); lua_setfield(L_, -2, key.c_str()); }
lua_pop(L_, 1); lua_pop(L_, 1);
@@ -593,7 +630,50 @@ bool LuaHost::has_function(const std::string& name, bool own) {
return f; 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<const char*> 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<LVal>& args, std::string* err) { bool LuaHost::call_function(const std::string& name, const std::vector<LVal>& args, std::string* err) {
++execution_revision_;
get_fn(L_, env, name); get_fn(L_, env, name);
if (!lua_isfunction(L_, -1)) { if (!lua_isfunction(L_, -1)) {
lua_pop(L_, 1); lua_pop(L_, 1);
@@ -612,6 +692,7 @@ bool LuaHost::call_function(const std::string& name, const std::vector<LVal>& ar
} }
bool LuaHost::call_function_numbers(const std::string& name, const std::vector<LVal>& args, int nresults, std::vector<double>& results, std::string* err) { bool LuaHost::call_function_numbers(const std::string& name, const std::vector<LVal>& args, int nresults, std::vector<double>& results, std::string* err) {
++execution_revision_;
results.assign(nresults, 0.0); results.assign(nresults, 0.0);
get_fn(L_, env, name); // (not a function: the pcall fails, as the engine's does) get_fn(L_, env, name); // (not a function: the pcall fails, as the engine's does)
for (const LVal& a : args) { 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::load_once(const std::string& path) { return loaded_once_.insert(path).second; }
bool LuaHost::run_file(const std::string& path, std::string* err) { bool LuaHost::run_file(const std::string& path, std::string* err) {
if (!load_from_wak(*this, L_, path, err)) return false; ++execution_revision_;
if (!env.empty()) { push_env_table(L_, env); lua_setfenv(L_, -2); } 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); return pcall_traced(L_, 0, 0, err);
} }
bool LuaHost::run_string(const std::string& code, const std::string& chunk_name, std::string* 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 (luaL_loadbuffer(L_, code.data(), code.size(), chunk_name.c_str()) != 0) {
if (err) *err = lua_tostring(L_, -1); if (err) *err = lua_tostring(L_, -1);
lua_pop(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<double>& args, std::string* err) { bool LuaHost::call_global(const std::string& name, const std::vector<double>& args, std::string* err) {
++execution_revision_;
lua_getglobal(L_, name.c_str()); lua_getglobal(L_, name.c_str());
if (!lua_isfunction(L_, -1)) { if (!lua_isfunction(L_, -1)) {
lua_pop(L_, 1); lua_pop(L_, 1);
+7
View File
@@ -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. // 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; std::string env;
void drop_env(const std::string& key); 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<const char*> 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. // 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<LVal>& args, int nresults, std::vector<double>& results, std::string* err = nullptr); bool call_function_numbers(const std::string& name, const std::vector<LVal>& args, int nresults, std::vector<double>& results, std::string* err = nullptr);
// The game world behind the entity/component API (null: scripts only record events). // The game world behind the entity/component API (null: scripts only record events).
@@ -184,6 +187,10 @@ private:
std::vector<HostEvent> events_; std::vector<HostEvent> events_;
std::map<std::string, int> missing_; std::map<std::string, int> missing_;
std::set<std::string> loaded_once_; std::set<std::string> loaded_once_;
std::map<std::string, std::map<std::string, int>> compiled_files_; // environment -> file -> registry reference
size_t compiled_revision_ = size_t(-1);
uint64_t execution_revision_ = 0;
std::map<std::string, int> original_api_;
std::vector<ActionRow> actions_; std::vector<ActionRow> actions_;
size_t actions_len_ = (size_t)-1; size_t actions_len_ = (size_t)-1;
bool tried_actions_ = false; bool tried_actions_ = false;