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:
1 parent
87bff164f1
commit
a34f42d53e
17 files changed
+727
-131
No files matched your search
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user