Lot of bugfixes

* Adding clipper to generate the terrain's negative powder-contour offset.
* Fixing RDB issues (fracture, active trigger, performances)
* Fixing blood emission
* Fixing the freezing formula on liquids
* Fixing Steve spawning due to a missed test.
* Reaction table properly build
This commit is contained in:
Gaël committed 2026-10-21 20:46:13 +02:00
1 parent a34f42d53e
commit 00a9c1c26c
48 files changed
+5771 -407

No files matched your search

+2 -1
View File
@@ -195,8 +195,9 @@ endif()
set(NG_CPU_PHYS_SOURCES src/phys/entity_physics.cpp src/phys/rigid_dynamics.cpp src/phys/rigid_collision.cpp src/phys/convex_distance.cpp src/phys/rigid_world_toi.cpp src/phys/dynamic_tree.cpp
src/phys/contact_solver.cpp src/phys/rigid_contact_world.cpp src/phys/rigid_world_step.cpp src/phys/joint_solver.cpp src/phys/shape_queries.cpp src/phys/recovered_units.cpp src/phys/world.cpp src/phys/rigid_fracture.cpp src/phys/terrain_outline.cpp src/phys/terrain_world.cpp src/phys/poly_decompose.cpp src/phys/phys_system.cpp)
file(GLOB NG_TESS_SOURCES third_party/libtess2/Source/*.c) # libtess2 (SGI free software licence B), the tessellator of the original
add_library(ngphys STATIC ${NG_CPU_PHYS_SOURCES} ${NG_TESS_SOURCES})
add_library(ngphys STATIC ${NG_CPU_PHYS_SOURCES} ${NG_TESS_SOURCES} third_party/clipper/clipper.cpp)
target_link_libraries(ngspawn PUBLIC ngphys)
target_include_directories(ngphys PRIVATE third_party/libtess2/Source third_party/clipper)
target_include_directories(ngphys PUBLIC cpu src third_party/libtess2/Include)
target_include_directories(ngphys PRIVATE third_party/libtess2/Source)
+2
View File
@@ -19,6 +19,8 @@ void main() {
uint cs = cells[i], ct = cells[t];
vec2 et = ext[t];
if ((it.x >> 31) != 0u) cs ^= DIR_BIT; // gas bounced: flip horizontal direction
terrain_age[t] = (cell_mat(cs) << 16) | 0x100u;
terrain_age[i] = (cell_mat(ct) << 16) | 0x100u;
cells[t] = cs;
ext[t] = vec2(uintBitsToFloat(it.z), uintBitsToFloat(it.w));
cells[i] = ct;
-14
View File
@@ -54,20 +54,6 @@ void main() {
int fireHp = int(mz << 16) >> 16; // fire_hp as the signed 16 bits
bool noBurnOut = fireHp == -1; // exactly -1 never burns out (0070e060); other negative values satisfy fire_hp < age
{ // the biome's reaction_freeze_chance / reaction_unfreeze_chance (0070e8c0; FREEZING, HOT): r % (cold_freezes_chance_rev + 1) < freeze turns the cell into its
// cold_freezes_to_material when the random neighbour is free; r % (warmth_melts_chance_rev + 1) < unfreeze turns it into its warmth_melts_to_material when that
// neighbour is free or of another material (the cell-type gate +0xa0 and the contact predicate 004ac0f0 of the freeze are not ported)
const uint bb = bm_bytes(vec2(p + ivec2(pc.view_x, pc.view_y)));
const uint fc = (bb >> 16) & 0xFFu, uc = bb >> 24;
if ((fc | uc) != 0u && !burning) {
const uvec2 cv = bm_conversions(m);
const uint nb = cell_at(p + D4[hash3(seed, 41u, 9u) & 3u]);
const uint nm = nb == NONE ? 0u : cell_mat(nb);
if (fc != 0u && (cv.x & 0xFFFFu) != 0u && (hash3(seed, 42u, 9u) & 0x7FFFu) % ((cv.x >> 16) + 1u) < fc && nm == 0u) { cells[i] = ((cv.x & 0xFFFFu) - 1u) | (c & 0xFF0000u); ext[i] = vec2(0.0); birth[i] = birth_frame(); return; }
if (uc != 0u && (cv.y & 0xFFFFu) != 0u && (hash3(seed, 43u, 9u) & 0x7FFFu) % ((cv.y >> 16) + 1u) < uc && nm != m) { cells[i] = ((cv.y & 0xFFFFu) - 1u) | (c & 0xFF0000u); ext[i] = vec2(0.0); birth[i] = birth_frame(); return; }
}
}
if (!burning && (f & F_GAS) != 0u) { // CGasCell update 0070a670: with probability (gas_speed + 1) / 101 (else the update only drifts the gas), intensity -= rand % 3,
// fade = (char) intensity; a positive one then spreads (rand % 101 < fade, then 0070f9e0) - the targets below count this smoke as a source
const uint raw = (birth[i] >> 24) & 255u;
+20
View File
@@ -13,6 +13,8 @@ layout(std430, set = 0, binding = 1) readonly buffer Mats { uvec4 mats[]; };
layout(std430, set = 0, binding = 2) buffer Pixels { uint pixels[]; };
layout(std430, set = 0, binding = 3) buffer Ext { vec2 ext[]; };
layout(std430, set = 0, binding = 42) buffer Birth { uint birth[]; }; // the frame a cell got its material (cell age = frame - birth, 0070ebf0 req_lifetime)
// Material stamp (high 16), CLiquidCell movement state (bits 8..11), rest age (low byte).
layout(std430, set = 0, binding = 65) buffer TerrainAge { uint terrain_age[]; };
layout(std430, set = 0, binding = 4) buffer Claims { uint claims[]; };
// per cell: x = target index | (flip direction << 31), y = priority, z/w = new vy/vx bits
layout(std430, set = 0, binding = 5) buffer Intent { uvec4 intent[]; };
@@ -66,6 +68,24 @@ uint with_fade(uint b, int f) { return (b & BIRTH_MASK) | (uint(f & 255) << 24);
uint cell_mat(uint c) { return c & 0xFFFFu; }
uint mflags(uint m) { return mats[3u * m].y; }
// 0070b1d0: state 3 and zero vertical accumulator; support is not probed.
uint liquid_state(uint i, uint m) {
return (terrain_age[i] >> 16) == m ? (terrain_age[i] >> 8) & 15u : 3u;
}
bool liquid_freeze_ready(ivec2 p, uint c) {
const uint m = cell_mat(c);
const int i = p.y * pc.gw + p.x;
return (mflags(m) & F_LIQCELL) == 0u || (liquid_state(uint(i), m) == 3u && ext[i].x == 0.0);
}
// ChangeMaterial preserves the movement accumulators for nonburning same-class cells.
void material_changed(int i, uint oldCell, uint m) {
const uint old = cell_mat(oldCell);
const uint classMask = F_LIQCELL | F_GAS | F_FIRE | F_AIR;
const bool sameClass = (oldCell & BURN_BIT) == 0u && (mflags(old) & classMask) == (mflags(m) & classMask);
terrain_age[i] = (m << 16) | (sameClass && (terrain_age[i] >> 16) == old ? (terrain_age[i] & 0xFFFFu) : 0x300u);
if (!sameClass) ext[i] = vec2(0.0);
birth[i] = birth_frame();
}
float mdensity(uint m) { return uintBitsToFloat(mats[3u * m].z); }
float mgravity(uint m) { return uintBitsToFloat(mats[3u * m].w); }
int mviscosity(uint m) { return int(mats[3u * m + 1u].x & 0xFFFFu); }
+22 -56
View File
@@ -46,6 +46,7 @@ layout(std430, set = 0, binding = 23) buffer Ctrl {
vec4 hand2; // host: held wand: pivot
ivec4 burnN; // rigid_burn.comp: x the burnt pixels; y: the player's respawns, z: saved by SAVING_GRACE, w: an extra life used (the host reads and clears them)
} ctrl;
#include "hit_liquid.glsl"
const int ST_HOVER = 128; // an item that hovers (a wand in the world): no physics, a small bob (play_hover_animation)
const int ST_SWIMMER = 256; // the immersion probe at ex.w (the integer buoyancy_check_offset_y) is tested
@@ -54,20 +55,19 @@ const int ST_GROUNDED = 1, ST_CANJUMP = 2, ST_CANFLY = 4, ST_DEAD = 8, ST_LIQUID
// rigid bodies are solid for creatures and the player: rigid_raster.comp marks the cells their masks cover (read here with a one-tick delay)
layout(std430, set = 0, binding = 40) readonly buffer BodyGrid { uint bodyGrid[]; };
layout(std430, set = 0, binding = 46) readonly buffer GlowTab { uint gtab[]; }; // per material: gfx_glow in the low 16 bits, the stain statuses it gives in bits 16-21 (WET, OILED, BLOODY, ALCOHOLIC, SLIMY, RADIOACTIVE)
bool simplePhysics = false;
bool body_solid(ivec2 w) {
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false;
const uint b = bodyGrid[p.y * pc.gw + p.x];
return b != 0u && (b & 0x80000000u) == 0u; // (bit 31: a body that is no platform - the lanterns and their chains do not block, main.cpp isPlatform)
}
// 00bb6820: the leak emitter's offset is the local hit point, clamped to the HitboxComponent (a corpse part: +-12 here); without one (ex7.w, a prop) as it is, zero beyond 256 px
vec2 leak_offset(Ent e, vec2 rel) { return e.ex7.w > 0.5 ? (dot(rel, rel) > 65536.0 ? vec2(0.0) : rel) : clamp(rel, vec2(-12.0), vec2(12.0)); }
bool solid_cell_at(ivec2 w) {
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
if (p.x < 0 || p.y < 0 || p.x >= pc.gw || p.y >= pc.gh) return false; // outside the window there is no ground (such entities are frozen)
uint m = cell_mat(cells[p.y * pc.gw + p.x]);
if (m != 0u && (mflags(m) & (F_STATIC | F_SAND)) != 0u) return true;
return body_solid(w);
return !simplePhysics && body_solid(w);
}
// The character's cell test 0063ef70: a platform cell counts only when one of its four neighbours (below, left, right, above) is one too - a lone pixel (a crumb of
// rock, a single body pixel) does not stop the player. (Its other exception, the bodies held by telekinesis - 00c2dfc0 stamps their b2Body user data +0x48 with
@@ -112,6 +112,9 @@ bool line_clear(vec2 a, vec2 b) { // no solid cell between two points (3-cell s
return true;
}
bool box_hits(ivec2 o, vec4 box) {
// SimplePhysicsSystem (00c17fc0 -> 0063f6b0) traces the entity origin
// through grid cells. The character box/platform push-out is a different system.
if (simplePhysics) return solid_cell_at(o);
ivec2 lo = o + ivec2(int(box.x), int(box.z)), hi = o + ivec2(int(box.y), int(box.w)); // hi exclusive
for (int y = lo.y; y < hi.y; ++y)
for (int x = lo.x; x < hi.x; ++x)
@@ -120,32 +123,6 @@ bool box_hits(ivec2 o, vec4 box) {
}
// Blood: real particles of the creature's blood material that fly out along the hit and become cells where they land.
void blood_burst(uint seed, vec2 pos, vec2 dirv, int count, float speed, int matp1, uint colour) {
if (matp1 <= 0) return;
for (int k = 0; k < count && k < 64; ++k) {
float r1 = float(hash3(seed, uint(k), 1u) & 0xFFFFu) / 65536.0, r2 = float(hash3(seed, uint(k), 2u) & 0xFFFFu) / 65536.0;
vec2 v = dirv * (0.4 + r1 * 1.2) * speed + vec2((r2 - 0.5) * 1.4 * speed, -0.3 - r1 * 0.9 * speed);
emit_material_particle(pos + vec2(0.0, -3.0), v, 70.0 + r2 * 40.0, PART_GRAV, colour, matp1 - 1);
}
}
// Liquid of a living hit (00ab9a90): the count does not depend on the damage, N = trunc((u * (40 - 20) + 20) * blood_multiplier + 0.5) (the incoming multiplier and the world
// multiplier are 1); the first droplet leaves along the hit, each next one is turned by a random angle (-0.6..0.6 rad) from the previous one, a speed of 0.5..1.25 times
// the base, at the hit point +-1.5. The velocity is 20 x s x the packet impulse (px/s): a projectile's velocity x ragdoll_force_multiplier (00c091f0), an explosion's radial force (00657960).
void hit_blood(uint seed, vec2 hit, vec2 dirv, float bloodMult, int matp1, uint colour, float speedMul) {
if (matp1 <= 0 || bloodMult <= 0.0) return;
const float u0 = float(hash3(seed, 0u, 11u) & 0x7FFFu) / 32767.0;
int n = int((u0 * 20.0 + 20.0) * bloodMult + 0.5);
vec2 v = dirv;
for (int k = 0; k < n && k < 64; ++k) {
float u1 = float(hash3(seed, uint(k), 12u) & 0x7FFFu) / 32767.0, u2 = float(hash3(seed, uint(k), 13u) & 0x7FFFu) / 32767.0;
float u3 = float(hash3(seed, uint(k), 14u) & 0x7FFFu) / 32767.0, u4 = float(hash3(seed, uint(k), 15u) & 0x7FFFu) / 32767.0;
float sp = 0.5 + 0.75 * u1, a = -0.6 + 1.2 * u2;
v = vec2(v.x * cos(a) - v.y * sin(a), v.x * sin(a) + v.y * cos(a));
emit_material_particle(vec2(hit.x - 1.5 + 3.0 * u4, hit.y - 1.5 + 3.0 * u3), v * (20.0 * sp / 60.0) * speedMul, 70.0 + u2 * 40.0, PART_GRAV, colour, matp1 - 1); // (20 s v px/s, v the packet impulse)
}
}
layout(std430, set = 0, binding = 27) buffer Edits { ivec4 ehead; vec4 ee[512]; }; // cell edits (see edits.comp): flames falling from a burning creature
bool extinguishes_at(ivec2 w) { // water and the like put a burning creature out
ivec2 p = w - ivec2(pc.view_x, pc.view_y);
@@ -166,6 +143,7 @@ void main() {
if (id >= uint(ctrl.inp.w)) return;
Ent e = ents[id];
int kind = e.st.x;
simplePhysics = kind == 4 && (e.st.w & ST_RIGID) == 0;
if (kind == 0 || (e.st.w & ST_DEAD) != 0) return;
const bool is_prop = kind == 4;
if (is_prop && e.ex7.y > 0.0) { // a corpse part with a liquid inventory: the particle emitter of the ragdoll piece (docs/BLOOD_EMITTER_LIFECYCLE.md)
@@ -177,49 +155,35 @@ void main() {
vec4 vr = e.ex;
float nextf = e.ex2.x, mode = e.ex2.y;
const uint fr = uint(ctrl.inp2.x);
const int dmg = atomicExchange(ents[id].ex8.z, 0);
if (dmg > 90) { // 00bb6820: strictly more than min_damage_to_leak (0.09)
const vec2 rel = vec2(float(ents[id].ex8.x), float(ents[id].ex8.y)) * 0.001;
const uint sd = hash3(id, fr, 53u);
float u[8];
for (int k = 0; k < 8; ++k) u[k] = float(hash3(sd, uint(k), 54u) & 0x7FFFu) / 32767.0;
if (u[7] < 1.0) { // u < leak_on_damage_percent (1): fails only for the draw 32767 / 32767
vec2 d = leak_offset(e, rel);
const float dl = length(d);
d = dl > 1e-4 ? d / dl : vec2(0.0);
const float dx = d.x * (0.85 + 0.29999995 * u[0]), dy = d.y * (0.85 + 0.29999995 * u[1]);
const float speed = 100.0 * (0.7 + (1.1 - 0.7) * u[2]);
const float vx = speed * dx, vy = speed * dy;
vr = vec4(vx * (0.6 + 0.39999998 * u[3]), vx * (1.0 + 0.39999998 * u[4]), vy * (0.6 + 0.39999998 * u[5]), vy * (1.0 + 0.39999998 * u[6]));
mode = 2.0;
}
}
atomicExchange(ents[id].ex8.z, 0); // Per-hit reconfiguration already happened at the source.
if (mode > 0.5 && float(fr) >= nextf && e.ex7.z > 0.0) {
const uint seed = hash3(id, fr, 51u);
nextf = float(fr) + ((e.ex2.z > 0.5 && mode < 1.5) ? 3.0 + float(hash3(seed, 3u, 58u) % 3u) : 1.0 + float(seed & 1u)); // the next emission is scheduled before anything is spawned (3-5 frames when the cosmetic spray exists and no damage has rewritten the emitter)
const int cnt = mode > 1.5 ? 1 : 1 + int(hash3(seed, 7u, 56u) % 3u);
if (reserve >= float(cnt)) {
if ((bloodPolicy[id].state.x & 2u) == 0u && reserve >= float(cnt)) {
const float ca = cos(e.a.z), sa = sin(e.a.z);
int made = 0;
for (int k = 0; k < cnt; ++k) {
const uint r = hash3(seed, uint(k), 52u);
const float ux = float(r & 0x7FFFu) / 32767.0, uy = float((r >> 15) & 0x7FFFu) / 32767.0, px = float(hash3(r, 1u, 57u) & 0xFFFFu) / 65536.0, py = float(hash3(r, 2u, 57u) & 0xFFFFu) / 65536.0;
const vec2 lo = leak_offset(e, vec2(float(ents[id].ex8.x), float(ents[id].ex8.y)) * 0.001);
const vec2 lo = vec2(float(ents[id].ex8.x), float(ents[id].ex8.y)) * 0.001;
const vec2 off = mode > 1.5 ? vec2(ca * lo.x - sa * lo.y, sa * lo.x + ca * lo.y) : vec2(0.0); // the damage event's offset (the hit point), turned with the body
const vec2 at = e.a.xy + off + vec2(-1.0 + 2.0 * px, -1.0 + 2.0 * py); // rect (-1,-1) .. (1,1)
const vec2 along = (e.ex7.w < 0.5 || (bloodPolicy[id].state.x & 32u) != 0u) ? mix(e.ex10.xy, e.a.xy, float(k + 1) / float(cnt)) : e.a.xy;
const vec2 at = along + off + vec2(-0.5 + 2.0 * px, -0.5 + 2.0 * py); // rect (-1,-1) .. (1,1)
if (solid_cell_at(ivec2(floor(at)))) break;
vec2 v = vec2(mix(vr.x, vr.y, ux), mix(vr.z, vr.w, uy)) * (1.0 / 60.0); // px/s -> cells/frame
vec2 v = vec2(mix(min(vr.x, vr.y), max(vr.x, vr.y), ux), mix(min(vr.z, vr.w), max(vr.z, vr.w), uy)) * (1.0 / 60.0); // px/s -> cells/frame
v = vec2(ca * v.x - sa * v.y, sa * v.x + ca * v.y); // the entity's rotation
if (emit_material_particle(at, v, 70.0 + float((r >> 24) & 0x3Fu), PART_GRAV, uint(e.ex3.y), int(e.ex7.z) - 1)) ++made; // only the particles that were created cost reserve (00bc75e0)
}
reserve -= float(made);
ents[id].ex10.xy = e.a.xy;
}
}
if (e.ex2.z > 0.5 && e.ex7.z > 0.0 && float(fr) >= e.ex6.z) { // blood_spray_create_some_cosmetic: a second emitter on the same reserve, 1-3 cosmetic droplets (no cells) every 1-2 frames
const uint seed2 = hash3(id, fr, 59u);
ents[id].ex6.z = float(fr) + 1.0 + float(seed2 & 1u);
const int cnt2 = 1 + int(hash3(seed2, 7u, 56u) % 3u);
if (reserve >= float(cnt2)) {
const int cnt2 = 1 + int(hash3(seed2, 7u, 56u) % 2u);
if ((bloodPolicy[id].state.x & 2u) == 0u && reserve >= float(cnt2)) {
float u2[8];
for (int k = 0; k < 8; ++k) u2[k] = float(hash3(seed2, uint(k), 60u) & 0x7FFFu) / 32767.0;
const vec2 d = e.ex6.xy * vec2(0.85 + 0.29999995 * u2[0], 0.85 + 0.29999995 * u2[1]); // the same formula as the death emitter, other draws
@@ -231,13 +195,15 @@ void main() {
for (int k = 0; k < cnt2; ++k) {
const uint r = hash3(seed2, uint(k), 61u);
const float ux = float(r & 0x7FFFu) / 32767.0, uy = float((r >> 15) & 0x7FFFu) / 32767.0, px = float(hash3(r, 1u, 62u) & 0xFFFFu) / 65536.0, py = float(hash3(r, 2u, 62u) & 0xFFFFu) / 65536.0;
const vec2 at = e.a.xy + vec2(-1.0 + 2.0 * px, -1.0 + 2.0 * py);
const vec2 along = mix(e.ex10.zw, e.a.xy, float(k + 1) / float(cnt2));
const vec2 at = along + vec2(-1.0 + 2.0 * px, -1.0 + 2.0 * py);
if (solid_cell_at(ivec2(floor(at)))) break;
vec2 v = vec2(mix(cr.x, cr.y, ux), mix(cr.z, cr.w, uy)) * (1.0 / 60.0);
vec2 v = vec2(mix(min(cr.x, cr.y), max(cr.x, cr.y), ux), mix(min(cr.z, cr.w), max(cr.z, cr.w), uy)) * (1.0 / 60.0);
v = vec2(ca * v.x - sa * v.y, sa * v.x + ca * v.y);
if (emit_particle_hit(at, v, 25.0 + float((r >> 24) & 0x1Fu), 0.1, uint(e.ex3.y), int(e.ex7.z) - 1) != 0xFFFFFFFFu) ++made; // a droplet without a cell: it fades in the air and stains where it lands (it owns no cell: 00716ce0)
}
reserve -= float(made);
ents[id].ex10.zw = e.a.xy;
}
}
ents[id].ex = vr; ents[id].ex2.x = nextf; ents[id].ex2.y = mode; ents[id].ex7.y = max(reserve, 0.0);
@@ -260,7 +226,7 @@ void main() {
int energy = e.x.y;
// pushed out of solid cells (sand piles landing on it, an explosion refilling, a wall appearing): find the nearest free spot above or beside
const bool rigidBody = is_prop && (flags & ST_RIGID) != 0; // moved by the rigid-body engine: its own mask must not count as an obstacle
if (kind != 0 && !rigidBody && box_hits(ip, e.box)) {
if (kind != 0 && !rigidBody && !simplePhysics && box_hits(ip, e.box)) {
bool freed = false;
for (int k = 1; k <= 12 && !freed; ++k) {
const ivec2 cand[5] = ivec2[5](ivec2(0, -k), ivec2(-k, -k), ivec2(k, -k), ivec2(-k, 0), ivec2(k, 0));
@@ -403,11 +369,11 @@ void main() {
hit_flash(id, dmg > 0, (types & 0x10000) != 0);
if (dmg > 0) {
hitv = vec2(float(atomicExchange(ents[id].aux.x, 0)), float(atomicExchange(ents[id].aux.y, 0))) * 0.001;
hit_blood(hash3(id, uint(ctrl.inp2.x), 31u), pos + vec2(0.0, 0.5 * (e.box.z + e.box.w)), hitv, e.ex7.x, e.ex3.x, uint(e.ex3.y), 1.0); // (hitv: the packet impulse, px/s)
}
}
// melee attack
if (tAlive && !frozen && (ents[tgt].ex8.x & 8192) == 0 && e.ex.z > 0.5 && abs(dx) < e.ai.z && abs(dy) < e.ai.z + 4.0 && cooldown <= 0.0) {
hit_liquid(tgt, e.ai.w * dealt_mult(e) * ents[tgt].ex9.w, ents[tgt].a.xy, vec2(0.0), 1.0, 1.0, 1, hash3(id, uint(pc.frame), 801u));
atomicAdd(ents[tgt].x.x, int(e.ai.w * dealt_mult(e) * ents[tgt].ex9.w * 1000.0)); // the target's melee multiplier
if (e.ai.w > 0.0 && e.ai.w * dealt_mult(e) * ents[tgt].ex9.w <= 0.0 && ents[tgt].st.x >= 1 && ents[tgt].st.x <= 3) ents[tgt].ex11.w = float(pc.frame); // (a blocked hit: BLOCK on the host)
ents[id].rg2.w = 1 | ((ctrl.inp2.x & 0xFFFFFF) << 4);
+1 -1
View File
@@ -12,5 +12,5 @@ layout(std430, set = 0, binding = 39) buffer EntPrevW { vec4 prevw[]; };
void main() {
uint i = gl_GlobalInvocationID.x;
if (i >= uint(ctrl.inp.w)) return;
prevw[i] = vec4(ents[i].a.xy, 0.0, 0.0);
prevw[i] = ents[i].a; // rigid bodies also need their previous angle
}
+27 -4
View File
@@ -29,6 +29,7 @@ layout(std430, set = 0, binding = 34) readonly buffer FxTex { uint fxtex[]; };
#include "fx_host.glsl"
#include "fx_state.glsl"
#include "hit_liquid.glsl"
// rigid bodies (read only): projectiles stop at them
layout(std430, set = 0, binding = 30) readonly buffer RShapes { uint rsh[]; };
struct RBody { vec4 p; vec4 v; ivec4 i; vec4 q; vec4 l; vec4 n; vec4 m; ivec4 jn; vec4 ja0; vec4 ja1; vec4 ja2; vec4 ja3; };
@@ -103,10 +104,30 @@ void note_block(int victim, float raw, float dealt) {
// A damage event on a body with a liquid inventory: the corpse part emits (entities.comp).
void body_damage(int slot, vec2 hit, float dmg) {
if (slot < 0 || ents[slot].ex7.y <= 0.0) return;
atomicAdd(ents[slot].ex8.z, max(1, int(dmg * 1000.0)));
const vec2 r = hit - ents[slot].a.xy; const float ca = cos(ents[slot].a.z), sa = sin(ents[slot].a.z);
const vec2 rel = vec2(ca * r.x + sa * r.y, -sa * r.x + ca * r.y); // in the body's frame (entities.comp turns the leak back with the rotation)
atomicExchange(ents[slot].ex8.x, int(rel.x * 1000.0)); atomicExchange(ents[slot].ex8.y, int(rel.y * 1000.0));
const HitLiquidPolicy policy = bloodPolicy[slot];
if ((policy.state.x & 1u) == 0u || policy.inventory.x <= 0.0 || dmg >= ents[slot].hp.x || dmg <= policy.inventory.y) return;
const uint serial = uint(atomicAdd(ents[slot].ex8.z, 1));
const uint seed = hash3(uint(slot), uint(pc.frame), serial + floatBitsToUint(hit.x));
const float probability = float(hash3(seed, 7u, 54u) & 32767u) / 32767.0;
if (probability >= policy.inventory.x || (policy.state.x & 16u) == 0u) return;
vec2 rel = hit - ents[slot].a.xy;
if (policy.inventory.z != 0.0) rel.x /= policy.inventory.z;
if (policy.inventory.w != 0.0) rel.y /= policy.inventory.w;
const float ca = cos(ents[slot].a.z), sa = sin(ents[slot].a.z);
rel = vec2(ca * rel.x + sa * rel.y, -sa * rel.x + ca * rel.y);
if ((policy.state.x & 4u) != 0u) rel = clamp(rel, policy.bounds.xz, policy.bounds.yw);
else if (dot(rel, rel) > 65536.0) rel = vec2(0.0);
const float dl = length(rel);
const vec2 direction = dl > 0.0 ? rel / dl : vec2(0.0);
float u[7];
for (int k = 0; k < 7; ++k) u[k] = float(hash3(seed, uint(k), 54u) & 32767u) / 32767.0;
const float speed = 100.0 * mix(policy.pressure.x, policy.pressure.y, u[2]);
const vec2 v = direction * (vec2(0.85) + 0.29999995 * vec2(u[0], u[1])) * speed;
ents[slot].ex = vec4(v.x * (0.6 + 0.39999998 * u[3]), v.x * (1.0 + 0.39999998 * u[4]),
v.y * (0.6 + 0.39999998 * u[5]), v.y * (1.0 + 0.39999998 * u[6]));
ents[slot].ex2.y = 2.0; // Keep the emitter's next scheduled frame.
atomicExchange(ents[slot].ex8.x, int(rel.x * 1000.0));
atomicExchange(ents[slot].ex8.y, int(rel.y * 1000.0));
}
// The damage multiplier the perks give a shooter; applied ONCE when the shot is made (Message_Shot, 00b06870: projectile damage, and the explosion damage unless not_scaled_by_gamefx)
@@ -206,3 +227,5 @@ float hit_damage(int k, float proj, int tb, vec4 f) {
+ c1.x * v.ex12.z + c1.y * v.ex12.y + c1.z * v.ex13.z + c1.w * v.ex13.w
+ c2.x * v.ex13.x + c2.y * v.ex14.y + c2.z * v.ex14.z + c2.w * v.ex9.y;
}
#include "hit_liquid_channels.glsl"
+2
View File
@@ -4,4 +4,6 @@ layout(std430, set = 0, binding = 35) buffer FxHost {
vec4 hcast[1024]; // 2 lists x 256 requests x 2 vec4: (x, y, dir x, dir y), (node or -boom-1, team, seed, flags)
vec4 hnodes[8192]; // 2048 nodes x 4 vec4
ivec4 hspecs[16384]; // 4096 specs x 4 ivec4
ivec4 hcollision; // x: compact collision target count, rebuilt on the GPU before flight
int htargets[4096]; // ascending entity slots; preserves first-hit ordering
};
+8 -7
View File
@@ -44,14 +44,14 @@ void displaced_particle(uint id, FPart p) {
uint seed = id * 7u + uint(pc.frame);
ivec2 here = ivec2(floor(pos));
if (kind == 3u) { // waiting for a free place
if (reinsert(cw, here, seed, floatBitsToUint(p.b.z))) { fparts[id].b.x = 0.0; return; }
if (reinsert(cw, here, seed, floatBitsToUint(p.b.z))) { release_part(id); return; }
// No place: out of the window it is gone (00712390: a particle out of the loaded area dies; it waited there forever and filled the pool - 1000+ after 25 s).
// In the window the search of its column ended on a solid (a pocket under an overhang, a pool under ice, a column shifted into the rock): it drifts a cell,
// down into a free cell (air, gas), else sideways, and searches again from there; after 10 s it is dropped. (It used to wait where it was: blood and water
// pixels frozen in the air near pools.)
const ivec2 hq = here - ivec2(pc.view_x, pc.view_y);
const uint tries = p.c.w & 0xFFFFFu;
if (hq.x < 0 || hq.y < 0 || hq.x >= pc.gw || hq.y >= pc.gh || tries >= 600u) { fparts[id].b.x = 0.0; return; }
if (hq.x < 0 || hq.y < 0 || hq.x >= pc.gw || hq.y >= pc.gh || tries >= 600u) { release_part(id); return; }
ivec2 step = ivec2(0, 1);
if (hq.y + 1 >= pc.gh || (mflags(cell_mat(cells[(hq.y + 1) * pc.gw + hq.x])) & (F_STATIC | F_LIQUID)) != 0u) {
const int sx = (hash3(seed, tries, 67u) & 1u) != 0u ? 1 : -1;
@@ -112,7 +112,7 @@ void displaced_particle(uint id, FPart p) {
} else if (!dead) cur = target; // beyond the 60 probes the end point is taken as it is
pos = cur;
if (dead) {
if (reinsert(cw, ivec2(floor(pos)), seed, floatBitsToUint(p.b.z))) { fparts[id].b.x = 0.0; return; }
if (reinsert(cw, ivec2(floor(pos)), seed, floatBitsToUint(p.b.z))) { release_part(id); return; }
kind = 3u; // no place yet: it keeps trying
}
fparts[id].a = vec4(pos, vel);
@@ -129,7 +129,7 @@ void main() {
uint id = gl_GlobalInvocationID.x;
if (id >= uint(FPART_MAX)) return;
FPart p = fparts[id];
if (p.b.x <= 0.0) return;
if (p.b.x <= 0.0 || p.b.x >= 2.9e38) return; // Allocation may still be initializing this record.
if (p.c.y != 0u) { displaced_particle(id, p); return; }
const bool real = p.b.y < 0.0;
vec2 pos = p.a.xy, vel = p.a.zw;
@@ -217,7 +217,6 @@ void main() {
vel += vec2(-sin(ang), cos(ang)) * force / 60.0;
}
fparts[id].a = vec4(pos, vel);
fparts[id].b.x = p.b.x;
if (landed) { // 00716600 / 0071fba0: the cell goes back into the grid where the particle ended, else the first free (or lighter) cell up to 128 above, drifting
// sideways; with no place yet it waits as a returning cell (not lost: a barrel's 750 units of spill all come out)
const uint cw = uint(landMat) | (((hash3(id + uint(pc.frame) * 7u, uint(landMat), 91u) >> 16) & 0xFFu) << 16);
@@ -225,11 +224,13 @@ void main() {
const ivec2 lq = ivec2(floor(landPos)) - ivec2(pc.view_x, pc.view_y);
if (lq.x >= 0 && lq.y >= 0 && lq.x < pc.gw && lq.y < pc.gh && !reinsert(cw, ivec2(floor(landPos)), id * 7u + uint(pc.frame), bv)) {
fparts[id].a = vec4(landPos, 0.0, 0.0);
fparts[id].b = vec4(1.0e6, 1.0, uintBitsToFloat(bv), 0.0);
fparts[id].c = uvec4(cw, 3u, 0u, 0u);
}
fparts[id].b = vec4(1.0e6, 1.0, uintBitsToFloat(bv), 0.0);
} else release_part(id);
return;
}
if (p.b.x <= 0.0) release_part(id);
else fparts[id].b.x = p.b.x;
}
if (p.b.x <= 0.0) return;
ivec2 q = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
+6 -10
View File
@@ -4,23 +4,19 @@ const int FP_MAX = 32768, FPART_MAX = 16384, FANIM_MAX = 256, FANIM_ALL = 512;
struct FProj { vec4 a; vec4 b; vec4 c; ivec4 d; vec4 e; vec4 f; }; // d.w: the cast serial (0 = dead; the host's twins match it), d.z bit 10 = friendly fire of the shot; f: the wand's damage_electricity_add, damage_ice_add, knockback_force add, trail material (material + 1 | amount << 12) // e: rotation angle, angular velocity (thrown bodies) // pos, vel / damage, life left, gravity, friction / explosion radius, damage, age, bounces / type, spec, fired mask + team, alive
struct FPart { vec4 a; vec4 b; uvec4 c; }; // pos, vel / life, max life, gravity, colour bits (rgb, bit 24 = fades)
struct FAnim { vec4 a; ivec4 b; vec4 c; }; // x, y, light radius, light strength / boom, age, max age, colour bits / angle
layout(std430, set = 0, binding = 36) buffer FxState {
layout(std430, set = 0, binding = 36) coherent buffer FxState {
uint fcnt[4]; // projectile ring, particle ring, animation ring
FProj fprojs[FP_MAX]; // (src/gun/gpu_fx.h kMaxProj)
FPart fparts[16384];
FAnim fanims[512];
};
// Integer alias of the same storage, solely for atomically claiming a particle's
// life word. A plain float test allowed concurrent searches to return the same slot.
layout(std430, set = 0, binding = 36) coherent buffer FxStateAtomic { uint fxwords[]; };
// A free record (00717400 / 007174e0: the pools return null when they are full, a live particle is never replaced): the search starts at the ring position and takes the first dead
// record of the next 64; -1 when there is none.
int alloc_part() {
const uint s0 = atomicAdd(fcnt[1], 1u);
for (uint k = 0u; k < 64u; ++k) {
const uint s = (s0 + k) % uint(FPART_MAX);
if (fparts[s].b.x <= 0.0) return int(s);
}
return -1;
}
#include "particle_alloc.glsl"
// colour bits: rgb, bit 24 = fades with its life, bit 25 = additive. c.w of a cosmetic: bit 1 = no grid collision (collide_with_grid 0), bit 2 = the bounce is spent
// (00712010 sets the bounce flag 6a:20 on every record; fx_particles.comp)
@@ -39,7 +35,7 @@ bool cell_free_at(vec2 w) {
return q.x >= 0 && q.y >= 0 && q.x < pc.gw && q.y < pc.gh && cell_mat(cells[q.y * pc.gw + q.x]) == 0u;
}
void emit_particle(vec2 pos, vec2 vel, float life, float grav, uint rgb, bool fade, bool additive) { emit_particle_g(pos, vel, life, grav, rgb, fade, additive, true); }
// A droplet without a cell that ends where it meets any cell (the cosmetic blood spray): c.x = 1; with a material (c.z = material + 1) it leaves a cell of it there, so the spray feeds the pools.
// A droplet without a cell that ends where it meets any cell (the cosmetic blood spray): c.x = 1; c.z = material + 1 supplies the stain colour. It never owns or deposits a cell.
uint emit_particle_hit(vec2 pos, vec2 vel, float life, float grav, uint rgb, int material) { // returns 0xFFFFFFFF when the pool is full
const int si = alloc_part();
if (si < 0) return 0xFFFFFFFFu;
+31
View File
@@ -0,0 +1,31 @@
// Native hit-liquid policy (00ab9a90). Called at the damage source, once per channel.
struct HitLiquidPolicy { uvec4 liquid; uvec4 state; vec4 bounds; vec4 inventory; vec4 pressure; };
layout(std430, set = 0, binding = 67) readonly buffer HitLiquid {
ivec4 bloodSettings;
HitLiquidPolicy bloodPolicy[4096];
vec4 bloodProjectile[512];
};
void hit_liquid(int target, float damage, vec2 hit, vec2 impulse, float incoming, float speedScale, int fx, uint seed) {
if (damage <= 0.0 || incoming <= 0.0 || fx == 0 || (bloodPolicy[target].state.x & 2u) != 0u) return;
const uvec4 policy = bloodPolicy[target].liquid;
int matp1 = int(policy.x & 65535u);
uint colour = policy.z;
if (bloodSettings.y != 0 && (policy.x >> 16) != 0u) { matp1 = int(policy.x >> 16); colour = policy.w; }
if (matp1 == 0) return;
const float u = float(hash3(seed, 0u, 11u) & 32767u) / 32767.0;
const int count = int(((u * 20.0 + 20.0) * uintBitsToFloat(policy.y) + 0.5) * incoming) * bloodSettings.x;
vec2 v = impulse;
for (int k = 0; k < count; ++k) {
const float su = float(hash3(seed, uint(k), 12u) & 32767u) / 32767.0;
const float au = float(hash3(seed, uint(k), 13u) & 32767u) / 32767.0;
const float yu = float(hash3(seed, uint(k), 14u) & 32767u) / 32767.0;
const float xu = float(hash3(seed, uint(k), 15u) & 32767u) / 32767.0;
const float a = -0.6 + 1.2 * au;
v = vec2(v.x * cos(a) - v.y * sin(a), v.x * sin(a) + v.y * cos(a));
// The constructor adds +0.5 after the hit handler's +/-1.5 jitter.
const vec2 pos = hit - vec2(1.0) + 3.0 * vec2(xu, yu);
const ivec2 p = ivec2(floor(pos)) - ivec2(pc.view_x, pc.view_y);
if (any(lessThan(p, ivec2(0))) || any(greaterThanEqual(p, ivec2(pc.gw, pc.gh)))) continue;
emit_material_particle(pos, v * ((20.0 / 60.0) * (0.5 + 0.75 * su) * speedScale), 70.0 + au * 40.0, PART_GRAV, colour, matp1 - 1);
}
}
+15
View File
@@ -0,0 +1,15 @@
// 00ab8f10: one hit-liquid event per positive damage channel, in native order.
void projectile_hit_liquid(int k, float projectile, int tb, vec4 adds, float scale, vec2 hit, vec2 impulse, uint seed) {
const Ent v = ents[k];
const vec4 a = ftf(tb + 12), b = ftf(tb + 13), c = ftf(tb + 14);
const float channel[13] = float[13](projectile * v.ex9.x, (a.y + adds.x) * v.ex12.x,
c.w * v.ex9.y, a.z * v.ex9.z, a.x * v.ex9.w, b.y * v.ex12.y,
b.x * v.ex12.z, (a.w + adds.y) * v.ex12.w, c.x * v.ex13.x,
b.z * v.ex13.z, b.w * v.ex13.w, c.y * v.ex14.y, c.z * v.ex14.z);
const vec2 factors = bloodProjectile[tb / TYPE_STRIDE].xy;
const int fx = (fti(tb + 11).y >> 4) & 15;
for (int i = 0; i < 13; ++i) {
if (i == 8) continue; // Healing is a negative damage channel.
hit_liquid(k, channel[i] * scale, hit, impulse, factors.x, factors.y, fx, hash3(seed, uint(i), 411u));
}
}
+9 -1
View File
@@ -46,6 +46,7 @@ void main() {
uint c = cells[i];
uint m = cell_mat(c);
uint f = mflags(m);
if (pc.pass_ == 0 && (terrain_age[i] >> 16) != m) terrain_age[i] = (m << 16) | 0x300u;
if ((f & (F_AIR | F_STATIC)) != 0u) return;
vec2 e = ext[i];
@@ -84,7 +85,7 @@ void main() {
const ivec2 bp = p + ivec2(0, 1);
release = r2 < ceilP && in_bounds(bp) && cell_mat(cells[idx(bp)]) == 0u;
}
if (release) cells[i] = c & ~(ATTACH_BIT | (3u << 24));
if (release) { cells[i] = c & ~(ATTACH_BIT | (3u << 24)); terrain_age[i] = (m << 16) | 0x300u; }
}
}
return;
@@ -256,6 +257,13 @@ void main() {
if ((f & F_FIRE) != 0u && target == NONE && (rnd(i, 30u) & 15u) == 0u) { cells[i] = 0u; return; } // 00709fc0: the extinction test (1/16) sits at the tail: a successful move and the early returns bypass it
if ((f & F_GAS) != 0u && target != NONE) nvy = vy + 1.0; // 0070aad0: the gas age counts its successful moves
// 0070b160: only a resting powder whose state age reaches 20 becomes terrain.
// Successful moves reset both swapped cells below in apply.comp.
if ((f & F_LIQCELL) != 0u) {
const bool resting = target == NONE && nvy == 0.0;
const uint age = resting ? min((terrain_age[i] & 255u) + (P == 0 ? 1u : 0u), 255u) : 0u;
terrain_age[i] = (m << 16) | (resting ? 0x300u : 0x100u) | age;
}
if (target != NONE) {
intent[i] = uvec4(target | (flip << 31), prio, floatBitsToUint(nvy), floatBitsToUint(nvx));
atomicMax(claims[i], prio);
+16
View File
@@ -0,0 +1,16 @@
int alloc_part() {
const uint s0 = atomicAdd(fcnt[1], 1u);
for (uint k = 0u; k < 64u; ++k) {
const uint s = (s0 + k) % uint(FPART_MAX);
const uint word = 4u + uint(FP_MAX) * 24u + s * 12u + 4u;
const uint previous = atomicAdd(fxwords[word], 0u);
if (uintBitsToFloat(previous) <= 0.0 && atomicCompSwap(fxwords[word], previous, floatBitsToUint(3.0e38)) == previous) return int(s);
}
return -1;
}
void release_part(uint slot) {
// Publish a free slot only after its previous owner has finished all writes.
memoryBarrierBuffer();
atomicExchange(fxwords[4u + uint(FP_MAX) * 24u + slot * 12u + 4u], 0u);
}
+28 -5
View File
@@ -58,7 +58,9 @@ bool shooter_safe(int flags, int age) { const int f = (flags >> 16) & 255; retur
int shot_flags(int typeFlags, int dz) { return ((dz >> 10) & 1) != 0 ? (typeFlags & ~(255 << 16)) | TF_FRIENDLY | (6 << 16) : typeFlags; }
bool victim_at(vec2 pos, int shooter, int herd, int flags, int age, out int hit) {
hit = -1;
for (int k = 0; k < ctrl.inp.w; ++k) {
const int aheadBody = body_slot(gAhead), hereBody = body_slot(pos);
for (int candidate = 0; candidate < hcollision.x; ++candidate) {
const int k = htargets[candidate];
if (ents[k].ex19.w == -float(gl_GlobalInvocationID.x + 1u)) continue; // its own damage proxy
const int kind = ents[k].st.x;
if (kind == 0 && (ents[k].st.w & 64) != 0 && ents[k].ex19.w > 0.5) { // a worm's segment (WormComponent hitbox_radius around every part): the hit goes to the worm
@@ -73,7 +75,7 @@ bool victim_at(vec2 pos, int shooter, int herd, int flags, int age, out int hit)
if ((ents[k].st.w & (8 | 4096)) != 0) continue; // (4096: its sprites, damage model and hitbox are switched off - Olematon away from an Evil Eye)
if (k == shooter - 1 && shooter_safe(flags, age)) continue;
if ((flags & TF_FRIENDLY) == 0 && !(shooter > 0 && (ents[shooter - 1].ex8.x & 4) != 0) && herd != 0 && ents[k].ex8.y == herd && kind != 4) continue; // (a berserk shooter has no herd: its projectiles hurt everybody)
if (kind == 4 && ents[k].ex7.w > 0.5) { if (body_slot(gAhead) == k || body_slot(pos) == k) { hit = k; return true; } continue; } // no HitboxComponent: its pixels (00c073b0)
if (kind == 4 && ents[k].ex7.w > 0.5) { if (aheadBody == k || hereBody == k) { hit = k; return true; } continue; } // no HitboxComponent: its pixels (00c073b0)
const float r = k == 0 ? 1.0 : 2.0;
const vec2 d = pos - ents[k].a.xy;
if (d.x >= ents[k].box.x - r && d.x <= ents[k].box.y + r && d.y >= ents[k].box.z - r && d.y <= ents[k].box.w + r) { hit = k; return true; }
@@ -83,7 +85,9 @@ bool victim_at(vec2 pos, int shooter, int herd, int flags, int age, out int hit)
bool creature_at(vec2 pos, float r, out int hit) {
hit = -1;
for (int k = 1; k < ctrl.inp.w; ++k) {
for (int candidate = 0; candidate < hcollision.x; ++candidate) {
const int k = htargets[candidate];
if (k == 0) continue;
int kind = ents[k].st.x;
if (!(kind == 1 || kind == 2 || (kind == 4 && ents[k].hp.x < 1.0e8)) || (ents[k].st.w & 8) != 0) continue;
vec2 d = pos - ents[k].a.xy;
@@ -131,8 +135,6 @@ void explode(vec2 pos, float radius, float damage, int type, int team, int shoot
int cellMat = int(ft[tb + 2].w);
float cellProb = uf(ft[tb + 3].x);
if (radius <= 0.0) return;
for (int k = 1; k < ctrl.inp.w; ++k) // corpse parts in range bleed
if (ents[k].st.x == 4 && ents[k].ex7.y > 0.0 && distance(ents[k].a.xy, pos) <= radius) body_damage(k, pos, 1.0);
// 00657960 (docs/EXPLOSION_KNOCKBACK_AND_VINES.md): every entity whose position is within explosion_radius, except the config's dont_damage_this (the shooter only when the projectile has
// explosion_dont_damage_shooter), takes the config's damage in full (only with damage_mortals; the perks scaled it at the Shot) and gets a knockback: scalar = radius * knockback_force *
// 10 * (0.5 t + 0.5), t = 1 - distance / radius clamped; its direction is the radial physics force (lerp of physics_explosion_power.min / .max by t: zero = no kick); it is compared with
@@ -160,6 +162,8 @@ void explode(vec2 pos, float radius, float damage, int type, int team, int shoot
{ // 00657960: the packet's impulse = the direction x ((power.max - power.min) x (1 - d / R) + power.min) x physics_multiplier_ragdoll_force x 3600 (the blood of the hit
// and the ragdoll take it); RAGDOLL_FX of the explosion packet: the constructor's NORMAL
const float a = ((pw.y - pw.x) * clamp(1.0 - dist / radius, 0.0, 1.0) + pw.x) * (float((fti(tb + 11).y >> 8) & 4095) / 256.0) * 3600.0;
hit_liquid(k, dk, c, away * a, 1.0, 1.0, 1, hash3(critSeed, uint(k), 711u));
if (kind == 4) body_damage(k, c, dk);
if (k != 0) { // (not the player: its aux.x is the maximum hp - a blast there made the respawn hp 765)
atomicExchange(ents[k].aux.x, int(away.x * a * 1000.0)); atomicExchange(ents[k].aux.y, int(away.y * a * 1000.0));
atomicAnd(ents[k].ex3.z, ~0xF000); atomicOr(ents[k].ex3.z, 1 << 12);
@@ -328,6 +332,7 @@ void verlet_step(uint id, FProj p, int tb, int vi, int age, int fired, int team,
if (rel.x < ents[e].box.x - r || rel.x > ents[e].box.y + r || rel.y < ents[e].box.z - r || rel.y > ents[e].box.w + r) continue;
if (kind == 3) { if (float(pc.frame) < aux.w) continue; aux.w = float(pc.frame + 20); } // mPlayerCooldownEnd
if (kind != 3 && herd != 0 && ents[e].ex8.y == herd) continue; // (the shooter's herd is spared)
hit_liquid(e, dmg, pts[k], v * 60.0 * ftf(vb + 3).x, 1.0, 1.0, 1, hash3(id, uint(k), uint(pc.frame)));
atomicAdd(ents[e].x.x, int(dmg * 1000.0));
note_attacker(e, shooter);
ents[e].ex19.yz = vec2(float(p.d.x + 1), float(pc.frame & 0xFFFFF));
@@ -375,6 +380,7 @@ void verlet_step(uint id, FProj p, int tb, int vi, int age, int fired, int team,
if (age > 1 && (tflags & 8) == 0 && ((tflags & 4) == 0 || age % max(fti(tb + 11).z & 0xFFFF, 1) == 0)
&& victim_at(tip, shooter, herd, shot_flags(tw, p.d.z), age, hk)) {
const float hitDmg = hit_damage(hk, p.b.x, tb, p.f) * boost_of(tw, hk) * fadeK;
projectile_hit_liquid(hk, p.b.x, tb, p.f, boost_of(tw, hk) * fadeK, tip, tipV * 60.0 * ftf(tb + 9).z, hash3(id, uint(pc.frame), 251u));
if (hitDmg > 0.0) atomicAdd(ents[hk].x.x, int(hitDmg * 1000.0));
note_attacker(hk, shooter);
ents[hk].ex19.yz = vec2(float(p.d.x + 1), float(pc.frame & 0xFFFFF));
@@ -396,6 +402,22 @@ void verlet_step(uint id, FProj p, int tb, int vi, int age, int fired, int team,
void main() {
uint id = gl_GlobalInvocationID.x;
if (pc.pass_ == 2) {
// Query once per simulation tick, shared by every flight substep.
// Dead creatures and props are omitted. Worm segments are validated
// against their root by victim_at, preserving its existing hit rules.
if (id == 0u) {
int count = 0;
for (int k = 0; k < min(ctrl.inp.w, 4096); ++k) {
const int kind = ents[k].st.x;
if (kind != 0 && (ents[k].st.w & 8) != 0) continue;
if (kind == 1 || kind == 2 || kind == 3 || (kind == 4 && ents[k].hp.x < 1.0e8) ||
(kind == 0 && (ents[k].st.w & 64) != 0 && ents[k].ex19.w > 0.5)) htargets[count++] = k;
}
hcollision.x = count;
}
return;
}
if (id >= uint(FP_MAX)) return;
FProj p = fprojs[id];
if (p.d.w == 0) return;
@@ -574,6 +596,7 @@ void main() {
const float cm = crit_mult((p.d.z >> 12) & 4095, t1.w, shooter, hk, seed);
const float hitDmg = hit_damage(hk, p.b.x * speedScale, tb, p.f) * boost_of(t1.w, hk) * cm;
if (cm > 1.0) note_crit(pos, hk);
projectile_hit_liquid(hk, p.b.x * speedScale, tb, p.f, boost_of(t1.w, hk) * cm, pos, vel * 60.0 * ftf(tb + 9).z, hash3(id, uint(pc.frame), uint(age)));
note_block(hk, p.b.x, hitDmg);
atomicAdd(ents[hk].x.x, int(hitDmg * 1000.0));
if (ents[hk].st.x == 4) body_damage(hk, pos, hitDmg); // a prop with a liquid inventory leaks
+132 -68
View File
@@ -3,56 +3,61 @@
layout(local_size_x = 8, local_size_y = 8) in;
#include "common.glsl"
// Material reactions (liquids mixing, acid, water on lava, ...). The grid is cut into disjoint pairs of neighbouring cells; pc.pass_
// (0..3) selects the pairing: 0/1 horizontal pairs starting at even/odd columns, 2/3 vertical pairs. Both cells of a pair evaluate the
// same reaction (symmetric hash) and each applies its own side, so no cell is written twice. Four passes visit every adjacency once per frame.
// Material reactions: pass 4 plans one attempt per source, pass 5 commits thermal
// changes. Passes 0..3 visit disjoint horizontal/vertical pairs; one invocation
// owns both outputs. Ordinary passes consume the plan, never a freshly converted material.
layout(std430, set = 0, binding = 28) readonly buffer RHead { uint rhead[]; }; // [a * n + b] = (offset << 8) | count
layout(std430, set = 0, binding = 23) buffer Ctrl { ivec4 inp; ivec4 inp2; ivec4 inp3; vec4 outp; vec4 outp2; vec4 view; vec4 hand0; ivec4 hand1; vec4 hand2; ivec4 burnN; ivec4 burnEv[256]; ivec4 reactN; ivec4 reactEv[64]; } ctrl; // reactEv: entities to spawn (x, y, entity index + 1)
layout(std430, set = 0, binding = 29) readonly buffer RRec { uint rrec[]; }; // 5 words per entry: outs (a | b << 16, 0xFFFF = same), probability / 65536
void main() {
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
if (p.x >= pc.gw || p.y >= pc.gh) return;
int ph = pc.pass_ & 3;
ivec2 q;
if (ph < 2) q = p + ivec2(((p.x & 1) == ph) ? 1 : -1, 0);
else q = p + ivec2(0, ((p.y & 1) == (ph - 2)) ? 1 : -1);
layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; };
#include "biome_mods.glsl"
const ivec2 RD4[4] = ivec2[4](ivec2(0, -1), ivec2(-1, 0), ivec2(1, 0), ivec2(0, 1));
uint canonical(uint m) { const uint n = mats[mats.length() - 1u].w; return rhead[2u * n * (n + 1u) + m]; }
uint draw15(uint seed, uint salt) { return hash(seed ^ salt) & 0x7FFFu; }
// One plan per source/update, stored in movement scratch after movement completes.
// y: 0 idle, 1 combined, 2 fast, 3 material change. z: shared R, w: neighbour / target.
void plan_reaction(ivec2 p, int i) {
const uint c = cells[i], m = cell_mat(c), f = mflags(m);
const uint n = mats[mats.length() - 1u].w, stride = n * (n + 1u);
const uint seed = hash3(uint(p.x + pc.view_x), uint(p.y + pc.view_y), uint(pc.frame) ^ 0x7C6E0u);
uint route = 2u;
// Worker +0x460 forces a full/combined update after ten ordinary updates.
// Initial loaded cells also receive the initial full update.
const bool full = (pc.frame % 11) == 0;
const bool passive = (f & F_STATIC) != 0u || ((f & F_POWDER) != 0u && (c & ATTACH_BIT) != 0u);
const bool slowBranch = full && (!passive || draw15(seed, 1u) % 101u < 20u);
if (slowBranch) route = !passive || draw15(seed, 2u) % 101u < 50u ? 1u : 0u;
if (m == 0u || (!full && !passive && (f & F_LIQCELL) != 0u && liquid_state(uint(i), m) == 3u && (terrain_age[i] & 255u) > 49u)) route = 0u;
const uint R = draw15(seed, 3u) % 10001u, dir = draw15(seed, 4u) & 3u;
intent[i] = uvec4(c, route, R, dir);
if (route == 1u && (c & BURN_BIT) == 0u) {
const uint bb = bm_bytes(vec2(p + ivec2(pc.view_x, pc.view_y)));
const uint fc = (bb >> 16) & 255u, uc = bb >> 24;
if ((fc | uc) != 0u) {
const uvec2 cv = bm_conversions(m);
const bool cold = fc != 0u && (cv.x & 65535u) != 0u && draw15(seed, 5u) % ((cv.x >> 16) + 1u) < fc && liquid_freeze_ready(p, c);
const bool warm = uc != 0u && (cv.y & 65535u) != 0u && draw15(seed, 6u) % ((cv.y >> 16) + 1u) < uc;
if (cold || warm) {
// A successful cold flag owns the attempt, including a blocked contact.
intent[i].y = 0u;
const ivec2 q = p + RD4[dir];
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return;
int i = p.y * pc.gw + p.x, j = q.y * pc.gw + q.x;
uint ci = cells[i], cj = cells[j];
uint mi = cell_mat(ci), mj = cell_mat(cj);
if (mi == 0u && mj == 0u) return;
uint n = mats[mats.length() - 1u].w;
uint h = rhead[mi * n + mj];
uint cnt = h & 255u;
if (cnt == 0u) return;
uint lo = uint(min(i, j)), hi = uint(max(i, j));
uint rr = hash3(lo, hi, uint(pc.frame * 4 + ph) ^ 0x51u);
uint k = 0u; // the record of the pair table: R % count with the roll R that also gates the attempt (0070eaf0)
// 0070eaf0: every cell that has a reaction threshold (CellData + 0x22c: the largest probability of a reaction it is the first input of; air is no cell and never starts one) tries once per
// update with ONE random neighbour of four, rolls R (0..10000) against min(that threshold, the record's probability) and the record must accept the direction: the pair gets the chance of
// each of its two cells separately (rec / 4 for a material next to air, not rec / 2).
{
const bool iLo = i < j;
const uint mA = iLo ? mi : mj, mB = iLo ? mj : mi;
const ivec2 pA = iLo ? p : q, pB = iLo ? q : p;
const uint hA = rhead[mA * n + mB], hB = rhead[mB * n + mA];
const uint eA = ((hA >> 8) + k % max(hA & 255u, 1u)) * 5u, eB = ((hB >> 8) + k % max(hB & 255u, 1u)) * 5u;
const uint pwA = rrec[eA + 1u], pwB = rrec[eB + 1u];
const ivec2 dAB = pB - pA;
const uint relA = dAB.y < 0 ? 1u : dAB.y > 0 ? 2u : dAB.x < 0 ? 3u : 4u, relB = relA == 1u ? 2u : relA == 2u ? 1u : relA == 3u ? 4u : 3u;
const uint dirA = (pwA >> 16) & 7u, dirB = (pwB >> 16) & 7u;
// each cell has its own roll R = next15 % 10001: the material threshold (CellData + 0x22c, positive) and the record's threshold both accept R <= threshold, the record is R % count
const uint RA = (hash(rr ^ 0xA2u) & 0x7FFFu) % 10001u, RB = (hash(rr ^ 0xB2u) & 0x7FFFu) % 10001u;
const uint kA = RA % max(hA & 255u, 1u), kB = RB % max(hB & 255u, 1u);
const uint eA2 = ((hA >> 8) + kA) * 5u, eB2 = ((hB >> 8) + kB) * 5u;
const uint matA = rhead[n * n + mA], matB = rhead[n * n + mB];
const bool fireA = mA != 0u && (hA & 255u) != 0u && matA > 0u && RA <= matA && RA <= (rrec[eA2 + 1u] & 0xFFFFu) && (((rrec[eA2 + 1u] >> 16) & 7u) == 0u || ((rrec[eA2 + 1u] >> 16) & 7u) == relA) && (hash(rr ^ 0xA1u) & 3u) == 0u;
const bool fireB = mB != 0u && (hB & 255u) != 0u && matB > 0u && RB <= matB && RB <= (rrec[eB2 + 1u] & 0xFFFFu) && (((rrec[eB2 + 1u] >> 16) & 7u) == 0u || ((rrec[eB2 + 1u] >> 16) & 7u) == relB) && (hash(rr ^ 0xB1u) & 3u) == 0u;
if (!fireA && !fireB) return;
k = fireA ? kA : kB;
const uint nm = cell_mat(cells[q.y * pc.gw + q.x]);
const bool allow = cold ? nm == 0u || (mflags(nm) & F_LIQCELL) == 0u || (mflags(nm) & F_POWDER) != 0u : canonical(nm) != canonical(m);
if (allow) { intent[i].y = 3u; intent[i].w = ((cold ? cv.x : cv.y) & 65535u) - 1u; }
return;
}
uint e = ((h >> 8) + k % cnt) * 5u;
}
}
// 0070ad30 is reached only on the passive full-update tail, after neither
// reaction method returned. It is not unconditional terrain cleanup.
if (full && passive && route != 1u && rhead[stride + n * n + m] == 0u && (c & BURN_BIT) == 0u && p.x > 0 && p.y > 0 && p.x + 1 < pc.gw && p.y + 1 < pc.gh) {
if (cell_mat(cells[i-1]) == 0u && cell_mat(cells[i+1]) == 0u && cell_mat(cells[i-pc.gw]) == 0u && cell_mat(cells[i+pc.gw]) == 0u) { intent[i].y = 3u; intent[i].w = 0u; }
}
}
void apply_side(ivec2 p, ivec2 q, int i, int j, uint ci, uint cj, uint mi, uint mj, uint e, uint rr) {
uint outs = rrec[e];
uint third = rrec[e + 2u];
uint blob = rrec[e + 3u];
@@ -60,31 +65,13 @@ void main() {
bool second = (mi == mj) && (i > j); // equal materials: the lower cell takes the first output
uint o = second ? (outs >> 16) : (outs & 0xFFFFu);
const bool firstIsI0 = (rrec[e + 4u] & 0x80000000u) == 0u; // (the record is the mirrored one when the first input is the second material of the pair)
const uint reqLife = blob >> 20;
if (reqLife > 0u && birth_age(birth[firstIsI0 ? i : j]) < reqLife) return; // req_lifetime: the first cell must be old enough
if (((blob >> 19) & 1u) != 0u && firstIsI0 && !second) { // destroy_horizontally_lonely_pixels: the first output becomes air when both horizontal neighbours are empty (both probed)
const bool le = p.x > 0 && cell_mat(cells[i - 1]) == 0u, ri = p.x + 1 < pc.gw && cell_mat(cells[i + 1]) == 0u;
if (le && ri) o = 0u;
}
// three inputs (0070ebf0): a third cell next to the first input's cell (a cardinal neighbour chosen at random; the second cell's place is mirrored to the other side)
// must hold the third material; the thread of the first cell writes its output
if ((third & 0xFFFFu) != 0xFFFFu) {
const bool firstIsI = firstIsI0;
const ivec2 pf = firstIsI ? p : q, ps = firstIsI ? q : p;
const ivec2 dirs[4] = ivec2[4](ivec2(0, -1), ivec2(-1, 0), ivec2(1, 0), ivec2(0, 1));
ivec2 p3 = pf + dirs[(rr >> 20) & 3u];
if (p3 == ps) p3 = 2 * pf - ps;
if (p3.x < 0 || p3.y < 0 || p3.x >= pc.gw || p3.y >= pc.gh) return;
const int k3 = p3.y * pc.gw + p3.x;
const uint c3 = cells[k3];
if (cell_mat(c3) != (third & 0xFFFFu)) return;
const uint o3 = (third >> 16) & 0xFFFFu;
if (firstIsI && o3 != 0xFFFFu && o3 != cell_mat(c3)) { atomicCompSwap(cells[k3], c3, o3 | (c3 & 0x00FF0000u)); birth[k3] = birth_frame(); }
}
if (o == 0xFFFFu || o == mi) return; // this side stays as it is
cells[i] = o | (ci & 0x00FF0000u); // keeps the colour variation, clears burning and direction bits
ext[i] = vec2(0.0);
birth[i] = birth_frame();
material_changed(i, ci, o);
if (entityIdx != 0u && firstIsI0 && !second) { // the reaction loads an entity at the position of the first input (the host spawns it)
const int ei = atomicAdd(ctrl.reactN.x, 1);
if (ei < 64) ctrl.reactEv[ei] = ivec4(p.x + pc.view_x, p.y + pc.view_y, int(entityIdx), 0);
@@ -100,14 +87,16 @@ void main() {
for (int ray = 0; ray < nr; ++ray) {
const float a = float(ray) * 6.2831853 / float(nr);
const vec2 dir = vec2(cos(a), sin(a));
vec2 samplePos = vec2(p + ivec2(pc.view_x, pc.view_y));
for (int st = 1; st <= int(rad); ++st) {
const ivec2 t = p + ivec2(trunc(dir * float(st)));
samplePos += dir;
const ivec2 t = ivec2(samplePos) - ivec2(pc.view_x, pc.view_y);
if (t.x < 0 || t.y < 0 || t.x >= pc.gw || t.y >= pc.gh) break;
const int ti = t.y * pc.gw + t.x;
const uint tc = cells[ti], tm = cell_mat(tc);
if (tm == o) continue;
if (tm != mi) break;
if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { ext[ti] = vec2(0.0); birth[ti] = birth_frame(); } // a converted cell starts afresh (0070d0b0)
if (canonical(tm) == canonical(o)) continue;
if (canonical(tm) != canonical(mi)) break;
if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { material_changed(ti, tc, o); } // same-class movement state survives (0070d0b0)
}
}
} else if (rad > 0u) { // direct mode (0071ab50, restriction explicitly off): every loaded cell of the disk dx^2 + dy^2 < r^2 takes the output, whatever it holds
@@ -120,8 +109,83 @@ void main() {
const int ti = t.y * pc.gw + t.x;
const uint tc = cells[ti];
if (cell_mat(tc) == o) continue;
if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { ext[ti] = vec2(0.0); birth[ti] = birth_frame(); }
if (atomicCompSwap(cells[ti], tc, o | (tc & 0x00FF0000u)) == tc) { material_changed(ti, tc, o); }
}
}
}
}
void main() {
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
if (p.x >= pc.gw || p.y >= pc.gh) return;
const int i = p.y * pc.gw + p.x;
if (pc.pass_ == 4) { plan_reaction(p, i); return; }
if (pc.pass_ == 5) {
if (intent[i].y == 3u && cells[i] == intent[i].x) {
cells[i] = intent[i].w | (intent[i].x & 0xFF0000u);
material_changed(i, intent[i].x, intent[i].w);
}
return;
}
int ph = pc.pass_ & 3;
ivec2 q;
if (ph < 2) q = p + ivec2(((p.x & 1) == ph) ? 1 : -1, 0);
else q = p + ivec2(0, ((p.y & 1) == (ph - 2)) ? 1 : -1);
if (q.x < 0 || q.y < 0 || q.x >= pc.gw || q.y >= pc.gh) return;
const int j = q.y * pc.gw + q.x;
if (i > j) return; // One invocation owns both outputs; no half-reaction race.
uint ci = cells[i], cj = cells[j];
// A conversion must not start a second reaction as its new material this tick.
if (ci != intent[i].x || cj != intent[j].x) return;
uint mi = cell_mat(ci), mj = cell_mat(cj);
if (mi == 0u && mj == 0u) return;
const uint n = mats[mats.length() - 1u].w, stride = n * (n + 1u);
const int a = min(i, j), b = max(i, j);
const ivec2 pa = i < j ? p : q, pb = i < j ? q : p;
const uint ma = cell_mat(intent[a].x), mb = cell_mat(intent[b].x);
uint selectedBase = 0u, k = 0u;
bool found = false;
for (int side = 0; side < 2; ++side) {
const int src = side == 0 ? a : b;
const uint route = intent[src].y;
if (route != 1u && route != 2u) continue;
const ivec2 delta = side == 0 ? pb - pa : pa - pb;
if (RD4[intent[src].w] != delta) continue;
const uint sm = side == 0 ? ma : mb, tm = side == 0 ? mb : ma;
const uint base = route == 2u ? stride : 0u;
const uint h = rhead[base + sm * n + tm], cnt = h & 255u, R = intent[src].z;
if (sm == 0u || cnt == 0u || rhead[base + n * n + sm] == 0u || R > rhead[base + n * n + sm]) continue;
const uint candidate = R % cnt, pw = rrec[((h >> 8) + candidate) * 5u + 1u];
const uint rel = delta.y < 0 ? 1u : delta.y > 0 ? 2u : delta.x < 0 ? 3u : 4u;
if (R > (pw & 65535u) || (((pw >> 16) & 7u) != 0u && ((pw >> 16) & 7u) != rel)) continue;
selectedBase = base; k = candidate; found = true; break;
}
if (!found) return;
const uint h = rhead[selectedBase + mi * n + mj], cnt = h & 255u;
if (cnt == 0u) return;
const uint rr = hash3(uint(a), uint(b), uint(pc.frame) ^ 0x51u);
uint e = ((h >> 8) + k % cnt) * 5u;
const uint third = rrec[e + 2u];
const bool firstIsI0 = (rrec[e + 4u] & 0x80000000u) == 0u;
const uint reqLife = rrec[e + 3u] >> 20;
if (reqLife > 0u && birth_age(birth[firstIsI0 ? i : j]) < reqLife) return;
// three inputs (0070ebf0): a third cell next to the first input's cell (a cardinal neighbour chosen at random; the second cell's place is mirrored to the other side)
// must hold the third material; the thread of the first cell writes its output
if ((third & 0xFFFFu) != 0xFFFFu) {
const bool firstIsI = firstIsI0;
const ivec2 pf = firstIsI ? p : q, ps = firstIsI ? q : p;
const ivec2 dirs[4] = ivec2[4](ivec2(0, -1), ivec2(-1, 0), ivec2(1, 0), ivec2(0, 1));
ivec2 p3 = pf + dirs[(rr >> 20) & 3u];
if (p3 == ps) p3 = 2 * pf - ps;
if (p3.x < 0 || p3.y < 0 || p3.x >= pc.gw || p3.y >= pc.gh) return;
const int k3 = p3.y * pc.gw + p3.x;
const uint c3 = cells[k3];
if (canonical(cell_mat(c3)) != canonical(third & 0xFFFFu)) return;
const uint o3 = (third >> 16) & 0xFFFFu;
if (o3 != 0xFFFFu && o3 != cell_mat(c3)) { if (atomicCompSwap(cells[k3], c3, o3 | (c3 & 0x00FF0000u)) == c3) material_changed(k3, c3, o3); }
}
apply_side(p, q, i, j, ci, cj, mi, mj, e, rr);
const uint reverseHead = rhead[selectedBase + mj * n + mi];
if ((reverseHead & 255u) != 0u)
apply_side(q, p, j, i, cj, ci, mj, mi, ((reverseHead >> 8) + k % (reverseHead & 255u)) * 5u, rr);
}
+72
View File
@@ -20,6 +20,53 @@ layout(std430, set = 0, binding = 8) readonly buffer Tables { uint tbl[]; }; /
uint flame_material(uint m, uint dflt) { const uint v = tbl[1024u + 4u * mats[mats.length() - 1u].w + 2u * m]; return v != 0u ? v - 1u : dflt; }
uint smoke_material(uint m, uint dflt) { const uint v = tbl[1024u + 4u * mats[mats.length() - 1u].w + 2u * m + 1u]; return v != 0u ? v - 1u : dflt; }
#include "biome_mods.glsl"
layout(std430, set = 0, binding = 28) readonly buffer RHead { uint rhead[]; };
layout(std430, set = 0, binding = 29) readonly buffer RRec { uint rrec[]; };
const ivec2 TD4[4] = ivec2[4](ivec2(0,-1), ivec2(-1,0), ivec2(1,0), ivec2(0,1));
uint native_material(uint m) { const uint n = mats[mats.length()-1u].w; return rhead[2u*n*(n+1u)+m]; }
uint body_neighbor(ivec2 q, Body B, int s, vec2 com, float cs, float sn, uint m, uint base) {
const ivec2 grid = q - ivec2(pc.view_x, pc.view_y);
if (grid.x < 0 || grid.y < 0 || grid.x >= pc.gw || grid.y >= pc.gh) return NONE;
const ivec2 local = ivec2(floor(rot2(cs, -sn, vec2(q) + 0.5 - B.p.xy) + com));
if (shape_solid(s, local.x, local.y) && (burn[base + uint(local.y*shi(s,0)+local.x)] & BURNT) == 0u) return m;
return cell_mat(cells[grid.y*pc.gw+grid.x]);
}
// Solid-cell 0070e060 calls the combined/fast paths when body record +0x2e
// permits them. Ice glass must keep that path after leaving the terrain grid.
int solid_conversion(ivec2 wc, Body B, int s, vec2 com, float cs, float sn, uint m, uint base, uint seed) {
if ((B.i.z & 65536) != 0) return -1; // native collapse record disables reactions
const uint r = hash3(seed, uint(pc.frame), 40u) & 32767u;
const uint R = r % 10001u;
const uint dir = hash3(seed, uint(pc.frame), 41u) & 3u;
const ivec2 q = wc + TD4[dir];
const uint nm = body_neighbor(q, B, s, com, cs, sn, m, base);
if (nm == NONE) return -1;
const bool full = (pc.frame % 11) == 0;
if (full) {
const uint bb = bm_bytes(vec2(wc));
const uint fc = (bb >> 16) & 255u, uc = bb >> 24;
const uvec2 cv = bm_conversions(m);
const bool cold = fc != 0u && (cv.x & 65535u) != 0u && (hash3(seed,uint(pc.frame),42u)&32767u) % ((cv.x>>16)+1u) < fc;
const bool warm = uc != 0u && (cv.y & 65535u) != 0u && (hash3(seed,uint(pc.frame),43u)&32767u) % ((cv.y>>16)+1u) < uc;
if (cold) return nm == 0u || (mflags(nm)&F_LIQCELL) == 0u || (mflags(nm)&F_POWDER) != 0u ? int((cv.x&65535u)-1u) : -1;
if (warm) return native_material(nm) != native_material(m) ? int((cv.y&65535u)-1u) : -1;
}
const uint n = mats[mats.length()-1u].w, table = full ? 0u : n*(n+1u);
const uint h = rhead[table + m*n+nm], count = h & 255u;
if (count == 0u || rhead[table+n*n+m] == 0u || R > rhead[table+n*n+m]) return -1;
const uint e = ((h>>8) + R%count)*5u, pw = rrec[e+1u];
const uint requiredDir = (pw>>16)&7u, actualDir = dir == 0u ? 1u : dir == 1u ? 3u : dir == 2u ? 4u : 2u;
if (R > (pw&65535u) || (requiredDir != 0u && requiredDir != actualDir)) return -1;
// Complex sprite reactions need per-pixel material storage; leave those to
// the existing fracture path rather than applying only half their outputs.
if ((rrec[e+2u]&65535u) != 65535u || (rrec[e+3u]&65535u) != 0u || (rrec[e+4u]&0x7FFFFFFFu) != 0u) return -1;
const uint target = rrec[e]&65535u, other = rrec[e]>>16;
if (target == 65535u || native_material(target) == native_material(m)) return -1;
if (other != 65535u && native_material(other) != native_material(nm)) return -1;
return int(target);
}
void main() {
int bi = int(gl_WorkGroupID.x);
if (bi >= bhdr.x) return;
@@ -29,6 +76,13 @@ void main() {
if (matp1 == 0u) return;
const uint m = matp1 - 1u;
const uint mz = mats[3u * m + 2u].z, mw = mats[3u * m + 2u].w;
// Most rigid materials have no fast reactions. Avoid visiting their pixels
// on the ten ordinary updates between combined updates.
if (((mz >> 16) & 1u) == 0u) {
if ((B.i.z & 65536) != 0) return;
const uint n = mats[mats.length()-1u].w;
if ((pc.frame % 11) != 0 && rhead[n*(n+1u)+n*n+m] == 0u) return;
}
const int s = B.i.x;
const int w = shi(s, 0), h = shi(s, 1);
if (w * h > 4096) return;
@@ -50,6 +104,24 @@ void main() {
const ivec2 wc = ivec2(floor(wp));
const uint seed = uint(bi) * 4099u + uint(px);
if ((st & BURNING) == 0u) {
const int target = solid_conversion(wc, B, s, com, cs, sn, m, base, seed);
if (target >= 0 && (mflags(uint(target)) & (F_LIQCELL | F_GAS | F_FIRE | F_AIR)) != 0u) {
const ivec2 grid = wc - ivec2(pc.view_x, pc.view_y);
if (grid.x >= 0 && grid.y >= 0 && grid.x < pc.gw && grid.y < pc.gh) {
const int index = grid.y*pc.gw+grid.x;
const int ei = atomicAdd(ctrl.burnN.x, 1);
if (ei < 256) {
if (atomicCompSwap(cells[index], 0u, uint(target)) == 0u) {
material_changed(index, 0u, uint(target));
ctrl.burnEv[ei] = ivec4(bi, px, target+1, 0);
burn[base+uint(px)] = BURNT;
continue;
}
ctrl.burnEv[ei] = ivec4(-1);
} else atomicAdd(ctrl.burnN.x, -1);
}
}
if (((mz >> 16) & 1u) == 0u) continue;
// fire sources in the pixel's cell and the four around it (cells are free under a body)
float keep = 1.0, bestFade = -1.0;
bool wet = false;
+21 -1
View File
@@ -69,8 +69,8 @@ void main() {
if (e.st.x != 0 && (fc & 7) >= 3) gFlash = (fc & 8) != 0 ? 0xFF0000u : 0xFFFFFFu;
}
gFade = (e.st.x == 1 || e.st.x == 2 || e.st.x == 3) ? 0.0 : clamp(e.ex10.w, 0.0, 1.0); // INVISIBILITY active (a prop's ex8.xy is the hit point of its leak) (00b015d0 case 0x51: its sprites are told not to draw)
{ vec2 pv = prevpos[h0.x].xy; if (distance(pv, e.a.xy) < 10.0) e.a.xy = mix(pv, e.a.xy, ctrl.view.z); } // drawn between the last two ticks
const bool rigid = (e.st.w & (32 | 64)) != 0; // drawn rotated by the body angle in a.z (64: the part of a Verlet chain, angle in ex7.x)
if (!rigid || (e.st.w & 64) != 0) { vec2 pv = prevpos[h0.x].xy; if (distance(pv, e.a.xy) < 10.0) e.a.xy = mix(pv, e.a.xy, ctrl.view.z); }
bool mobile = e.st.x != 0 && !rigid;
bool moving = mobile && abs(e.a.z) > 0.08;
ivec4 an = moving ? hw : hi; // tex, w, h, frames | wait << 16
@@ -88,6 +88,26 @@ void main() {
for (int k = int(gl_LocalInvocationID.x); k < min(bhdr.x, 1024); k += 64) if (bodies[k].i.y == int(h0.x) && (bodies[k].i.z & 2) == 0) sBody = k;
barrier();
const int body = (e.st.w & 64) == 0 && w * h <= 4096 ? sBody : -1;
if ((e.st.w & 64) == 0 && sBody >= 0) {
// Interpolate one rigid pose. Mixing the old origin with the current
// angle moves the image away from its collision body, particularly
// when the sprite anchor is far from the centre of mass.
const vec4 previous = prevpos[h0.x];
const vec2 centre = bodies[sBody].p.xy;
const float ca = cos(e.a.z), sa = sin(e.a.z);
const vec2 offset = e.a.xy - centre;
const vec2 local = vec2(ca * offset.x + sa * offset.y, -sa * offset.x + ca * offset.y);
const float cp = cos(previous.z), sp = sin(previous.z);
const vec2 previousCentre = previous.xy - vec2(cp * local.x - sp * local.y, sp * local.x + cp * local.y);
// The solver clamps translation to 20 pixels/tick. Larger changes
// are teleports and should be drawn immediately.
if (distance(previousCentre, centre) <= 20.01) {
const float delta = atan(sin(e.a.z - previous.z), cos(e.a.z - previous.z));
e.a.z = previous.z + delta * ctrl.view.z;
const float ci = cos(e.a.z), si = sin(e.a.z);
e.a.xy = mix(previousCentre, centre, ctrl.view.z) + vec2(ci * local.x - si * local.y, si * local.x + ci * local.y);
}
}
int rf = (pc.frame / wait + int(h0.x)) % max(frames, 1);
const float ang = (e.st.w & 64) != 0 ? e.ex7.x : e.a.z;
const float sx = (e.st.w & 64) != 0 && e.ex7.y > 0.0 ? e.ex7.y : 1.0; // a part stretched along its length (the segments of a limb: IKLimbComponent)
+3
View File
@@ -16,6 +16,7 @@ layout(std430, set = 0, binding = 10) readonly buffer CellsOld { uint cells_old[
layout(std430, set = 0, binding = 11) readonly buffer ExtOld { vec2 ext_old[]; };
layout(std430, set = 0, binding = 52) readonly buffer BirthOld { uint birth_old[]; }; // the ages before the scroll
layout(std430, set = 0, binding = 66) readonly buffer TerrainAgeOld { uint terrain_age_old[]; };
layout(std430, set = 0, binding = 12) readonly buffer Scene { uint scene[]; }; // pixel scenes: 0 = none, else material + 1
layout(std430, set = 0, binding = 18) readonly buffer Override { uint ovr[]; }; // cells restored from the persistent store (0xFFFFFFFF = none)
@@ -428,6 +429,7 @@ void main() {
if (pc.shift_mode == 1) { // scrolling: reuse the simulated overlap, generate only the exposed strips
ivec2 src = p + ivec2(pc.shift_x, pc.shift_y);
if (src.x >= 0 && src.y >= 0 && src.x < pc.gw && src.y < pc.gh) {
terrain_age[i] = terrain_age_old[src.y * pc.gw + src.x];
cells[i] = cells_old[src.y * pc.gw + src.x];
ext[i] = ext_old[src.y * pc.gw + src.x];
birth[i] = birth_old[src.y * pc.gw + src.x]; // a cell that stays keeps its age (0070d1e0: the age belongs to the cell)
@@ -435,6 +437,7 @@ void main() {
}
}
birth[i] = birth_frame();
terrain_age[i] = 0u;
if (ovr[i] != 0xFFFFFFFFu) { // a cell that comes back from the store (only its word is kept): a burning cell or a flame gets the fade a new cell of its material has
// (0070f450: the material's temperature_of_fire), so a fire that went out of the window goes on when it comes back instead of dying at its next update
const uint oc = ovr[i];
+21
View File
@@ -139,6 +139,26 @@ struct Canvas {
void frame(int x, int y, int rw, int rh, uint32_t c) {
rect(x, y, rw, 1, c); rect(x, y + rh - 1, rw, 1, c); rect(x, y, 1, rh, c); rect(x + rw - 1, y, 1, rh, c);
}
void line(int x0, int y0, int x1, int y1, uint32_t c) {
// Clip first: terrain fixtures may be far outside the camera.
double a = 0, b = 1;
const double dx = double(x1) - x0, dy = double(y1) - y0;
auto clip = [&](double p, double q) { if (p == 0) return q >= 0; const double t = q / p; if (p < 0) a = std::max(a, t); else b = std::min(b, t); return a <= b; };
if (!clip(-dx, x0) || !clip(dx, w - 1 - x0) || !clip(-dy, y0) || !clip(dy, h - 1 - y0)) return;
const int ax = (int)std::lround(x0 + a * dx), ay = (int)std::lround(y0 + a * dy);
const int bx = (int)std::lround(x0 + b * dx), by = (int)std::lround(y0 + b * dy);
// Integer stepping avoids two floating divisions/rounds per debug-line pixel.
int x = ax, y = ay;
const int ix = std::abs(bx - ax), iy = -std::abs(by - ay), sx = ax < bx ? 1 : -1, sy = ay < by ? 1 : -1;
int error = ix + iy;
for (;;) {
put(x, y, c);
if (x == bx && y == by) break;
const int twice = 2 * error;
if (twice >= iy) { error += iy; x += sx; }
if (twice <= ix) { error += ix; y += sy; }
}
}
int text_width(const std::string& s, int scale) const {
if (font && font->ok()) { int w = 0; for (unsigned char ch : s) { const GameFont::Glyph& gl = font->g[ch]; w += (gl.ok ? gl.width : font->word_space) + font->char_space; } return w * scale; }
return (int)s.size() * 6 * scale - scale;
@@ -601,6 +621,7 @@ inline void draw(Canvas& cv, const Assets& as, const State& s) {
cv.text(cv.w - 14 - cv.text_width(t, 2), s.debug ? 90 : 14, t, rgba(255, 220, 120), 2);
}
if (s.debug) {
cv.text(14, cv.h - 30, "F3+C colliders F3+L save physics dump (debug folder)", rgba(160, 255, 160), 1);
const std::string l1 = fmt("%.0f fps", s.fps) + " frame " + std::to_string(s.frame);
const std::string l2 = "player " + fmt("%.0f", s.px) + ", " + fmt("%.0f", s.py) + " view " + std::to_string(s.view_x) + ", " + std::to_string(s.view_y);
const std::string l3 = "entities " + std::to_string(s.entities) + " stored tiles " + std::to_string(s.stored_tiles) + " " + s.seed_text;
+515 -140
View File
File diff suppressed because it is too large. Load diff
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "phys/phys_system.h"
#include <deque>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <string>
namespace ng {
// Enabled by F3 only; retain three seconds of simulation poses, not render frames.
struct RigidDebug {
struct Row { phys::PhysSystem::DebugBody b; phys::PhysSystem::Lifecycle lifecycle; int slot = -1, entity = 0, flags = 0, material = -1; std::string file; };
struct Sample { int frame; std::vector<Row> rows; };
std::deque<Sample> history;
void append(int frame, std::vector<Row> rows) {
history.push_back({frame, std::move(rows)});
while (history.size() > 180) history.pop_front();
}
bool save(const std::filesystem::path& dir, const phys::PhysSystem& physics, const std::string& metadata) const {
std::error_code ec; std::filesystem::create_directories(dir, ec);
if (ec) return false;
std::ofstream meta(dir / "context.txt"), poses(dir / "poses.csv"), polygons(dir / "colliders.csv"), contacts(dir / "contacts.csv");
if (!meta || !poses || !polygons || !contacts) return false;
meta << metadata << "\nCoordinates: world pixels; velocities: pixels/frame; angles: radians.\n"
"Body -1 is cell terrain. Collider radius is the solver skin, in pixels.\nposes.csv retains up to 180 ticks while F3 is enabled; body slots can be reused.\n";
poses << std::setprecision(12) << "frame,body,cpu,shape,slot,entity,file,material,flags,type,group,x,y,angle,vx,vy,omega,mass,awake,active,frozen,bullet,nailed,born_tick,sleep_convert,conversion_target,settle_static\n";
for (const auto& sample : history) for (const auto& r : sample.rows) {
const auto& b = r.b;
poses << sample.frame << ',' << b.index << ',' << b.cpu << ',' << b.shape << ',' << r.slot << ',' << r.entity << ',' << std::quoted(r.file) << ',' << r.material << ',' << r.flags << ',' << b.type << ',' << b.group << ','
<< b.x << ',' << b.y << ',' << b.angle << ',' << b.vx << ',' << b.vy << ',' << b.omega << ',' << b.mass << ',' << b.awake << ',' << b.active << ',' << b.frozen << ',' << b.bullet << ',' << b.nailed << ',' << r.lifecycle.born << ',' << r.lifecycle.sleep_convert << ',' << r.lifecycle.target << ',' << r.lifecycle.settle_static << '\n';
}
polygons << std::setprecision(12) << "polygon,body,fixture,category,mask,group,sensor,radius,vertex,x,y\n";
int pi = 0;
for (const auto& p : physics.debug_polygons()) {
for (int k = 0; k < (int)p.points.size(); ++k) polygons << pi << ',' << p.body << ',' << p.fixture << ',' << p.category << ',' << p.mask << ',' << p.group << ',' << p.sensor << ',' << p.radius << ',' << k << ',' << p.points[k][0] << ',' << p.points[k][1] << '\n';
++pi;
}
contacts << std::setprecision(12) << "body_a,body_b,x,y,separation\n";
for (const auto& c : physics.debug_contacts()) contacts << c.a << ',' << c.b << ',' << c.x << ',' << c.y << ',' << c.separation << '\n';
return bool(meta && poses && polygons && contacts);
}
};
}
+4
View File
@@ -63,6 +63,7 @@ void apply_attrs(Material& m, const XmlNode& n) {
} // namespace
void MaterialDb::load(std::string_view xml_text) {
canonical_.clear();
auto root = parse_xml(xml_text);
if (root->name != "Materials") throw std::runtime_error("materials: root is not <Materials>");
@@ -133,6 +134,8 @@ void MaterialDb::load(std::string_view xml_text) {
void MaterialDb::add_loose_twins() {
const size_t n = mats_.size();
loose_twin_.assign(n, -1);
canonical_.resize(n);
for (size_t i = 0; i < n; ++i) canonical_[i] = (int)i;
for (size_t i = 0; i < n; ++i) {
if (mats_[i].type != CellType::Liquid || attr_int(mats_[i], "liquid_static", 0) != 1 || attr_int(mats_[i], "liquid_sand", 0) != 1) continue;
Material t = mats_[i];
@@ -141,6 +144,7 @@ void MaterialDb::add_loose_twins() {
t.index = (int)mats_.size();
by_name_[t.name] = t.index;
loose_twin_[i] = t.index;
canonical_.push_back((int)i);
mats_.push_back(std::move(t));
}
loose_twin_.resize(mats_.size(), -1);
+4
View File
@@ -43,7 +43,11 @@ public:
void add_loose_twins();
int loose_twin(int index) const { return index >= 0 && index < (int)loose_twin_.size() ? loose_twin_[index] : -1; }
// Synthetic looseness changes the static byte, not native CellData identity.
int canonical(int index) const { return index >= 0 && index < (int)canonical_.size() ? canonical_[index] : index; }
private:
std::vector<int> canonical_;
std::vector<Material> mats_;
std::vector<Reaction> reactions_;
std::vector<int> loose_twin_;
+9 -4
View File
@@ -400,10 +400,8 @@ EntityState make_state(EntityDb& db, const Wak& wak, EntityLayer& L, std::map<st
e.ex4[2] = comp_f(dm, "air_lack_of_damage", 0.4f) / 60.0f;
}
}
if (dm && (e.st[0] == 1 || e.st[0] == 2) && mat_lookup) { // the blood: blood_material (the liquid a hit makes), else blood_spray_material, else "blood" (00ab9a90 picks the spray only for a recent entity)
std::string bm = dm->get("blood_material");
if (bm.empty()) bm = dm->get("blood_spray_material");
if (bm.empty()) bm = "blood";
if (dm && mat_lookup) { // 00ab9a90: missing uses blood_fading; explicit empty means none.
const std::string bm = dm->get("blood_material", "blood_fading");
const int id = bm.empty() ? -1 : mat_lookup(bm);
e.ex7[0] = comp_f(dm, "blood_multiplier", 1.0f);
if (id >= 0) { e.ex3[0] = id + 1; e.ex3[1] = (int32_t)(mat_color ? mat_color(bm) : 0xFFA01010u); }
@@ -520,6 +518,13 @@ int EntityLayerBuilder::add_body_entity(int tex, int w, int h, int ox, int oy, d
if (reuse >= 0 && reuse < (int)L_.ents.size()) {
const int idx = first_instance(reuse);
if (idx >= 0) {
// The released slot may have been a lantern-chain segment or a
// secondary prop body. Its new chunk is independent: otherwise
// makeRigid treats it as an old part and never creates a collider.
L_.body_part_slots.erase(reuse);
L_.body_parts.erase(reuse);
for (auto& [owner, parts] : L_.body_parts)
parts.erase(std::remove_if(parts.begin(), parts.end(), [&](const BodyPart& p) { return p.slot == reuse; }), parts.end());
L_.ents[reuse] = st;
si.ent = reuse;
L_.inst[idx] = si;
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <cmath>
#include <cstdint>
namespace ng {
// 00ab9a90 and real-particle constructor 00716af0. RNG supplied by caller.
inline int hit_liquid_count(float u, float entity, float incoming, int world) {
return int(((u * 20.0f + 20.0f) * entity + 0.5f) * incoming) * world;
}
inline bool hit_liquid_enabled(float damage, float incoming, int fx, bool frozen) {
return damage > 0.0f && incoming > 0.0f && fx != 0 && !frozen;
}
inline float projectile_hit_liquid_speed(float ragdollForce) {
return ragdollForce != 0.0f ? std::sqrt(0.005f / ragdollForce) : 1.0f;
}
inline bool inventory_hit_liquid_enabled(float damage, float hp, float probability, float threshold, float u, bool emitter) {
return probability > 0 && damage < hp && damage > threshold && u < probability && emitter;
}
inline void hit_liquid_local_point(float& x, float& y, float sx, float sy, float angle, bool hitbox, const float* bounds) {
if (sx != 0) x /= sx;
if (sy != 0) y /= sy;
const float c = std::cos(angle), s = std::sin(angle), nx = c * x + s * y;
y = -s * x + c * y; x = nx;
if (hitbox) {
x = x < bounds[0] ? bounds[0] : (x > bounds[1] ? bounds[1] : x);
y = y < bounds[2] ? bounds[2] : (y > bounds[3] ? bounds[3] : y);
} else if (x * x + y * y > 65536.0f) x = y = 0;
}
struct HitLiquidDrop { float x, y, vx, vy; };
inline HitLiquidDrop hit_liquid_drop(float x, float y, float& vx, float& vy,
float speedScale, float speedU, float angleU, float yU, float xU) {
const float a = -0.6f + 1.2f * angleU, c = std::cos(a), s = std::sin(a);
const float nx = vx * c - vy * s;
vy = vx * s + vy * c; vx = nx;
const float speed = (20.0f / 60.0f) * (0.5f + 0.75f * speedU) * speedScale;
return {x - 1.0f + 3.0f * xU, y - 1.0f + 3.0f * yU, vx * speed, vy * speed};
}
inline int ragdoll_hit_liquid_reserve(float u, float world, float ragdoll, double partMass, double totalMass, int absolute) {
const double amount = absolute < 0 || totalMass <= 0 ? partMass * 1000.0 : partMass / totalMass * absolute;
return int((0.8f + 0.40000004f * u) * float(int(world * ragdoll) * int(amount)));
}
// std430 ABI: binding 67; one entry per entity, then one per projectile type.
struct HitLiquidPolicy { uint32_t materials = 0, multiplier = 0, baseColour = 0, sprayColour = 0;
uint32_t flags = 0, reserved[3] = {};
float bounds[4] = {}; // min x, max x, min y, max y
float inventory[4] = {0, 0.09f, 1, 1}; // probability, minimum damage, entity scale x/y
float pressure[4] = {0.7f, 1.1f, 0, 0}; // pressure range, trail gap, reserved
};
struct HitLiquidTable {
int32_t settings[4] = {1, 0, 0, 0};
HitLiquidPolicy entities[4096];
float projectile[512][4] = {};
};
static_assert(sizeof(HitLiquidPolicy) == 80);
}
+51 -3
View File
@@ -155,6 +155,7 @@ int EntityRuntime::add_comp_internal(int entity, const std::string& type, std::m
void EntityRuntime::erase_comp_internal(int cid) {
RtComponent* c = comp(cid);
if (!c) return;
if (c->type == "LifetimeComponent" && c->attrs.count("_ng_blink_hidden") && alive(c->entity) && on_sprite_fade) on_sprite_fade(c->entity, 0);
update_component_fields(*c, -1);
auto& owners = entities_by_component_[c->type];
auto owner = owners.find(c->entity);
@@ -877,6 +878,7 @@ void EntityRuntime::set_component_enabled(int cid, bool 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.erase("_ng_blink_hidden") && on_sprite_fade) on_sprite_fade(c->entity, 0);
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)});
if (changed && on_components_enabled) on_components_enabled(c->entity);
@@ -894,6 +896,19 @@ void EntityRuntime::set_component_enabled(int cid, bool en) {
}
}
bool EntityRuntime::component_enabled(int cid) { RtComponent* c = comp(cid); return c && c->enabled; }
bool EntityRuntime::item_emitter_enabled(int cid, int held_item) {
const RtComponent* c = comp(cid);
if (!c || !c->enabled) return false;
const bool world = comp_has_tag(cid, "enabled_in_world"), hand = comp_has_tag(cid, "enabled_in_hand"),
stored = comp_has_tag(cid, "enabled_in_inventory");
if (!world && !hand && !stored) return true;
const RtEntity* item = ent(c->entity);
if (!item) return false;
if (components(item->id, "ItemComponent", "", true).empty() &&
components(item->id, "ItemActionComponent", "", true).empty()) return true;
if (!item->parent) return world;
return item->id == held_item ? hand : stored;
}
std::string EntityRuntime::component_type(int cid) { RtComponent* c = comp(cid); return c ? c->type : ""; }
int EntityRuntime::component_entity(int cid) { RtComponent* c = comp(cid); return c ? c->entity : 0; }
bool EntityRuntime::comp_has_tag(int cid, const std::string& t) { RtComponent* c = comp(cid); return c && c->tags.count(t); }
@@ -1077,7 +1092,8 @@ std::string EntityRuntime::interact_hint(double px, double py) {
return best;
}
void EntityRuntime::material_area_step(const std::function<int(double, double, double, double, const std::string&, const std::string&, int*)>& query) {
void EntityRuntime::material_area_step(const std::function<int(double, double, double, double, const std::string&, const std::string&, int*)>& query,
const std::function<bool(double, double, double, double)>& ready) {
// 00bb5320: look_for_failure: Message_MaterialAreaCheckerFailed when a cell of the box (air too) is neither material nor material2 (the engine scans one row / column per update);
// 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; };
@@ -1092,10 +1108,27 @@ void EntityRuntime::material_area_step(const std::function<int(double, double, d
if ((frame_ + cid) % every != 0) continue;
auto num = [&](const char* k) { return std::atof(c->attrs[k].c_str()); };
Job j{id, cid, (c->attrs["look_for_failure"].empty() || c->attrs["look_for_failure"] == "1"), e.x + num("area_aabb.min_x"), e.y + num("area_aabb.min_y"), e.x + num("area_aabb.max_x"), e.y + num("area_aabb.max_y"), c->attrs["material"], c->attrs["material2"], (int)num("count_min")};
// 00bb5320 calls 008833e0 with the complete AABB before moving
// the failure cursor. A partial pixel scene is not a wall breach.
if (ready && !ready(j.x0, j.y0, j.x1, j.y1)) continue;
if (j.failure) {
// 00bb5320 scans one short row/column per update, advancing
// along the long axis. Scanning the whole temple boundary every
// frame also reports transient terrain changes far too eagerly.
const bool vertical = num("area_aabb.max_x") - num("area_aabb.min_x") <= num("area_aabb.max_y") - num("area_aabb.min_y");
const int lo = (int)num(vertical ? "area_aabb.min_y" : "area_aabb.min_x");
const int hi = (int)num(vertical ? "area_aabb.max_y" : "area_aabb.max_x");
const auto scan = c->attrs.find("_ng_area_cursor");
const int cursor = scan == c->attrs.end() ? lo : std::clamp(std::atoi(scan->second.c_str()), lo, std::max(lo, hi));
c->attrs["_ng_area_cursor"] = std::to_string(cursor < hi ? cursor + 1 : lo);
if (vertical) j.y0 = j.y1 = (int)e.y + cursor;
else j.x0 = j.x1 = (int)e.x + cursor;
}
jobs.push_back(std::move(j));
}
}
for (const Job& j : jobs) {
if (!alive(j.entity) || !component_enabled(j.comp)) continue;
int total = j.failure ? 1 : 0; // (in: 1 = the part of the box in the loaded window will do - the failure checks scan what is loaded)
const int r = query(j.x0, j.y0, j.x1, j.y1, j.m1, j.m2, &total);
if (r < 0) continue;
@@ -1105,7 +1138,7 @@ void EntityRuntime::material_area_step(const std::function<int(double, double, d
const char* attr = j.failure ? "script_material_area_checker_failed" : "script_material_area_checker_success";
const char* fn = j.failure ? "material_area_checker_failed" : "material_area_checker_success";
RtEntity* e = ent(j.entity);
if (!e) continue;
if (!e || !e->alive) continue;
std::vector<std::pair<int, std::string>> scripts;
for (int cid : e->comps) {
RtComponent* c = comp(cid);
@@ -1224,7 +1257,22 @@ void EntityRuntime::tick() {
const auto nb = c->attrs.find("_ng_born");
const int born = nb != c->attrs.end() ? std::atoi(nb->second.c_str()) : e.born;
if (life > 0 && frame_ - born >= (int)life) expire.push_back(id);
else if (life > 0 && on_sprite_fade && c->attrs["fade_sprites"] == "1") on_sprite_fade(id, (frame_ - born) / life);
else if (on_sprite_fade) {
const int remaining = (int)life - (frame_ - born);
const bool wasHidden = c->attrs.count("_ng_blink_hidden") != 0;
bool hidden = false;
if (life > 0 && remaining < 180 && e.parent == 0 &&
(!components(id, "PhysicsBodyComponent", "", false).empty() || !components(id, "PhysicsBody2Component", "", false).empty())) {
// 00b9a010: physics items blink for one frame at each interval,
// accelerating over their final three seconds (gold included).
const int period = remaining < 15 ? 2 : remaining < 60 ? 5 : remaining < 120 ? 8 : 20;
hidden = remaining % period == 0;
}
if (hidden) c->attrs["_ng_blink_hidden"] = "1";
else c->attrs.erase("_ng_blink_hidden");
const bool fade = life > 0 && c->attrs["fade_sprites"] == "1";
if (fade || hidden != wasHidden) on_sprite_fade(id, hidden ? 1.0 : fade ? (frame_ - born) / life : 0.0);
}
}
}
{ // SpriteComponent kill_entity_after_finished: the entity goes when the sprite's animation (rect_animation, else the default one) has played to its end once,
+5 -1
View File
@@ -142,7 +142,8 @@ public:
// (x0, y0, x1, y1 in world cells): -1 = the area is not loaded (nothing happens), 0 = no, 1 = yes. A checker that looks for failure calls
// script_material_area_checker_failed of the entity's LuaComponent when the answer is 0, the other one the _success script when it is 1.
// query: the cells of the box that are m1 or m2 (*total: all its cells), -1 when the box is not loaded
void material_area_step(const std::function<int(double x0, double y0, double x1, double y1, const std::string& m1, const std::string& m2, int* total)>& query);
void material_area_step(const std::function<int(double x0, double y0, double x1, double y1, const std::string& m1, const std::string& m2, int* total)>& query,
const std::function<bool(double x0, double y0, double x1, double y1)>& ready = {});
// TeleportComponent (with an enabled HitboxComponent): true when the player point is inside the hitbox of an enabled teleporter; gives the destination
// (target.x / target.y, absolute or relative to the teleporter) and the entity.
bool teleport_step(double player_x, double player_y, double& target_x, double& target_y, int* teleporter = nullptr);
@@ -193,6 +194,9 @@ public:
void remove_component(int comp) override;
void set_component_enabled(int comp, bool enabled) override;
bool component_enabled(int comp) override;
// Item emitter context tags apply to the item itself: a card inside the
// held wand is stored, not held. Untagged/non-item effects keep their state.
bool item_emitter_enabled(int comp, int held_item);
std::string component_type(int comp) override;
int component_entity(int comp) override;
bool comp_has_tag(int comp, const std::string& tag) override;
+1 -1
View File
@@ -20,7 +20,7 @@ public:
static constexpr size_t kTableBytes = (size_t)(kVerletBase + kMaxVerlet * kVerletStride) * 16;
static constexpr size_t kTexWords = 1u << 20;
static constexpr int kMaxNodes = 2048, kMaxSpecs = 4096, kMaxRequests = 256, kPersistent = 128; // the last 128 nodes and specs are permanent (creature attacks), the rest is a ring
static constexpr size_t kHostBytes = 16 + 1024 * 16 + 8192 * 16 + 16384 * 16;
static constexpr size_t kHostBytes = 16 + 1024 * 16 + 8192 * 16 + 16384 * 16 + 16 + 4096 * 4;
static constexpr int kMaxProj = 32768; // the projectile pool (shaders/fx_state.glsl FP_MAX); a new projectile takes a free record, a live one is overwritten only when the pool is full
static constexpr size_t kStateBytes = 16 + (size_t)kMaxProj * 96 + 16384 * 48 + 512 * 48; // (animations: 0..255 the GPU's ring, 256..511 the host's)
static constexpr size_t kLightBytes = 16 + 512 * 32;
+1
View File
@@ -208,6 +208,7 @@ const ProjDef* ProjectileSim::def_from(const std::string& file, const EntityDef&
d.dont_damage_shooter = attr(pc, "explosion_dont_damage_shooter", 0) != 0;
d.not_scaled_by_gamefx = attr(pc, "config_explosion.not_scaled_by_gamefx", 0) != 0;
d.hit_knockback_force = attr(pc, "knockback_force", 0.0f);
d.blood_count_multiplier = attr(pc, "blood_count_multiplier", 1.0f);
d.ragdoll_force_multiplier = attr(pc, "ragdoll_force_multiplier", 0.025f);
if (pc) { // (the packet's RAGDOLL_FX: 00c091f0 copies ragdoll_fx_on_collision)
static const char* fxNames[10] = {"NONE", "NORMAL", "BLOOD_EXPLOSION", "BLOOD_SPRAY", "FROZEN", "CONVERT_TO_MATERIAL", "CUSTOM_RAGDOLL_ENTITY", "DISINTEGRATED", "NO_RAGDOLL_FILE", "PLAYER_RAGDOLL_CAMERA"};
+1
View File
@@ -67,6 +67,7 @@ struct ProjDef {
float hit_knockback_force = 0.0f; // ProjectileComponent knockback_force (+5d0, default 0): a hit knocks back by knockback_force x speed x the projectile's VelocityComponent mass (00c091f0)
float ragdoll_explosion_mult = 1.0f; // config_explosion physics_multiplier_ragdoll_force (ConfigExplosion +0x108, default 1): scales the impulse an explosion gives the entities
int ragdoll_fx = 1; // ProjectileComponent ragdoll_fx_on_collision (+53c): RAGDOLL_FX NONE 0, NORMAL 1, BLOOD_EXPLOSION 2, BLOOD_SPRAY 3, FROZEN 4, CONVERT_TO_MATERIAL 5, CUSTOM_RAGDOLL_ENTITY 6, DISINTEGRATED 7, NO_RAGDOLL_FILE 8, PLAYER_RAGDOLL_CAMERA 9
float blood_count_multiplier = 1.0f; // ProjectileComponent +0x5dc
float ragdoll_force_multiplier = 0.025f; // ProjectileComponent (+5d4, default 0.025): scales the force vector of the hit
float velocity_mass = 0.05f; // VelocityComponent mass (+50, default 0.05)
float physics_min = 0, physics_max = 0.2f; // config_explosion.physics_explosion_power.min / .max: the force on rigid bodies (00657960)
+7 -1
View File
@@ -385,7 +385,13 @@ int n_StatsBiomeGetValue(LuaHost& h, lua_State* L) {
return 1;
}
int n_StatsResetBiome(LuaHost& h, lua_State*) { h.biome_stats_base = h.stats; return 0; }
int n_GlobalsSetValue(LuaHost& h, lua_State* L) { if (h.buffer_world_writes) { h.globals[str(L, 1)] = str(L, 2); h.wrote_world = true; return 0; } std::lock_guard<std::mutex> lk(LuaHost::shared_mutex); h.world_globals()[str(L, 1)] = str(L, 2); return 0; }
int n_GlobalsSetValue(LuaHost& h, lua_State* L) {
const auto key = str(L, 1), value = str(L, 2);
if (std::getenv("NG_EVENTLOG") && key.rfind("TEMPLE_", 0) == 0)
std::printf("temple global %s = %s%s\n", key.c_str(), value.c_str(), h.buffer_world_writes ? " (generation trial)" : "");
if (h.buffer_world_writes) { h.globals[key] = value; h.wrote_world = true; return 0; }
std::lock_guard<std::mutex> lk(LuaHost::shared_mutex); h.world_globals()[key] = value; return 0;
}
// session numbers (new game plus count etc.): a fresh run has everything at 0
int n_SessionNumbersGetValue(LuaHost&, lua_State* L) { lua_pushstring(L, "0"); return 1; }
int n_GameGetFrameNum(LuaHost&, lua_State* L) { lua_pushnumber(L, 0); return 1; }
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include <cmath>
#include <cstdint>
namespace ng::phys {
// 00720960: visit surviving body pixels, preserving their source color, and
// fill only empty grid cells. Rotated masks bridge one/two-cell horizontal gaps.
// get_material returns -1 for unloaded space; put receives the source ARGB.
template<class Get, class Put>
void convert_body_cells(const uint32_t* pixels, int width, int height, double anchor_x, double anchor_y,
double origin_x, double origin_y, double angle, int old_material, int target, Get get_material, Put put) {
const double cs = std::cos(angle), sn = std::sin(angle);
for (int py = 0; py < height; ++py) for (int px = 0; px < width; ++px) {
const uint32_t color = pixels[py * width + px];
if ((color >> 24) < 128) continue;
const double lx = px + .5 - anchor_x, ly = py + .5 - anchor_y;
const int x = (int)std::floor(origin_x + cs * lx - sn * ly), y = (int)std::floor(origin_y + sn * lx + cs * ly);
if (get_material(x, y) == 0) put(x, y, target, color);
if (angle == 0) continue;
const auto matches = [&](int m) { return m > 0 && (m == target || m == old_material); };
if (matches(get_material(x - 2, y))) {
if (get_material(x - 1, y) == 0) put(x - 1, y, target, color);
} else if (matches(get_material(x - 3, y))) {
if (get_material(x - 1, y) == 0) put(x - 1, y, target, color);
if (get_material(x - 2, y) == 0) put(x - 2, y, target, color);
}
}
}
// 00ba5f20 / 00ba61ab: the native lookup deliberately saturates at sector 31
// after a quarter turn; it is not a uniform division of a full turn into 32.
inline int loose_ground_sector(int x, int y) {
constexpr float pi = 3.1415927f, tau = 6.2831855f;
float angle = 0;
const float length = std::sqrt(float(x * x + y * y));
if (length != 0) angle = float(std::atan2(double(float(y) / length), double(float(x) / length)));
while (angle < 0) angle += tau;
angle = float(std::fmod(double(angle), double(tau)));
const int sector = int(((angle / pi) * 2.0f) * 32.0f);
return sector < 0 ? 0 : sector > 31 ? 31 : sector;
}
// 00ba5940: single-precision unit steps and truncation towards zero, including
// the negative coordinates on the left side of a Holy Mountain.
template<class Hit>
bool loose_ground_ray(float sx, float sy, float ex, float ey, int& hx, int& hy, Hit hit) {
const float dx = ex - sx, dy = ey - sy, squared = dx * dx + dy * dy;
const int limit = (int)(squared + .5f);
hx = (int)ex; hy = (int)ey;
if (limit < 1) return hit(hx, hy);
const float length = std::sqrt(squared), ux = dx / length, uy = dy / length;
float x = sx, y = sy;
for (int step = 0; step < 1024 && (x - sx) * (x - sx) + (y - sy) * (y - sy) < (float)limit; ++step) {
x += ux; y += uy; hx = (int)x; hy = (int)y;
if (hit(hx, hy)) return true;
}
return false;
}
} // namespace ng::phys
+266 -53
View File
@@ -12,6 +12,7 @@
#include "physics/rigid.h"
#include "recovered_units.h"
#include "shape_queries.h"
#include "contact_solver.h"
namespace ng::phys {
namespace {
@@ -19,7 +20,6 @@ namespace {
using noita::cpu::Vec2d;
constexpr double kDt = noita::cpu::original_body_step; // 009a48d0: the body world steps by 1/60 s
constexpr double kUnitsPerFrameFromCells = 6.0; // 1 cell per frame = 60 cells/s = 6 physics units/s
constexpr int kFreezeMargin = 16; // cells inside the window edge beyond which a body is frozen (it has no terrain around it)
uint64_t fnv(uint64_t h, uint8_t b) { return (h ^ b) * 1099511628211ull; }
@@ -27,13 +27,19 @@ uint64_t fnv(uint64_t h, uint8_t b) { return (h ^ b) * 1099511628211ull; }
PhysSystem::PhysSystem() {
world_.gravity = {0, 12}; // 00718740: gravity (0, 12) physics units per second squared
terrain_ = std::make_unique<TerrainPhysics>(world_);
terrain_static_ = std::make_unique<TerrainPhysics>(world_, kStaticOnlyCategory);
terrain_ = std::make_unique<TerrainPhysics>(world_, 4);
terrain_static_ = std::make_unique<TerrainPhysics>(world_, 1);
terrain_sticky_ = std::make_unique<TerrainPhysics>(world_, 1);
}
int PhysSystem::add_shape(const RigidShape& s) {
int PhysSystem::add_shape(const RigidShape& s, int reuse) {
ShapeData d;
d.polys = mask_polygons(s.mask.data(), s.w, s.h, 1.0f, 6);
d.bounds = {(double)s.w, (double)s.h, 0, 0};
for (const auto& poly : d.polys) for (const auto& p : poly) {
d.bounds[0] = std::min(d.bounds[0], (double)p.x); d.bounds[1] = std::min(d.bounds[1], (double)p.y);
d.bounds[2] = std::max(d.bounds[2], (double)p.x); d.bounds[3] = std::max(d.bounds[3], (double)p.y);
}
if (std::getenv("NG_SHAPEAREA")) { // (debug: the polygon area against the pixel count)
double a = 0; int px = 0;
for (const ConvexPoly& cp : d.polys) for (size_t k = 0; k < cp.size(); ++k) a += 0.5 * (cp[k].x * cp[(k + 1) % cp.size()].y - cp[(k + 1) % cp.size()].x * cp[k].y);
@@ -46,6 +52,7 @@ int PhysSystem::add_shape(const RigidShape& s) {
d.mass = s.mass;
d.radius = s.radius;
d.samples = s.samples;
if (reuse >= 0 && reuse < (int)shapes_.size()) { shapes_[reuse] = std::move(d); return reuse; }
shapes_.push_back(std::move(d));
return (int)shapes_.size() - 1;
}
@@ -54,6 +61,7 @@ void PhysSystem::add_body(int index, int shape, double ax, double ay, double ox,
if (index >= (int)specs_.size()) specs_.resize(index + 1);
Spec s;
s.shape = shape;
s.lifecycle.born = frame_;
s.ax = ax; s.ay = ay; s.ox = ox; s.oy = oy; if (std::getenv("NG_ALLSINK")) s.go_through_sand = true; // (test: every body goes through sand)
if (specs_[index].cpu >= 0) remove_body(index);
specs_[index] = s;
@@ -92,6 +100,12 @@ void PhysSystem::set_surface(int index, double friction, double restitution) {
for (int fi : world_.bodies[specs_[index].cpu].fixtures) { world_.fixtures[fi].friction = friction; world_.fixtures[fi].restitution = restitution; }
}
void PhysSystem::set_bullet(int index, bool on) {
if (index < 0 || index >= (int)specs_.size() || !specs_[index].live) return;
specs_[index].bullet = on;
if (specs_[index].cpu >= 0) world_.bodies[specs_[index].cpu].flags.bullet = on;
}
void PhysSystem::set_density(int index, double density) {
if (index < 0 || index >= (int)specs_.size() || !specs_[index].live) return;
specs_[index].density = density;
@@ -124,6 +138,7 @@ void PhysSystem::create_pending() {
BodyDefinition def;
def.type = 2;
def.position = {s.ox / kPixelsPerUnit, s.oy / kPixelsPerUnit};
def.angle = s.angle;
def.velocity = {s.vx * kUnitsPerFrameFromCells, s.vy * kUnitsPerFrameFromCells};
def.angular_velocity = s.omega * 60.0;
def.allow_sleep = s.allow_sleep; def.fixed_rotation = s.fixed_rotation; def.bullet = s.bullet;
@@ -135,7 +150,7 @@ void PhysSystem::create_pending() {
f.density = s.density;
f.friction = s.friction;
f.restitution = s.restitution;
f.filter = {2, (std::uint16_t)(s.go_through_sand ? 0xffff & ~4 : 0xffff & ~kStaticOnlyCategory), (std::int16_t)s.group}; // 006f8860: the default fixture category 2 (a body that goes through sand meets only the static terrain)
f.filter = {2, (std::uint16_t)(s.go_through_sand ? 0xffff & ~4 : 0xffff), (std::int16_t)s.group}; // 006f8860: the default fixture category 2 (a body that goes through sand meets only the static terrain)
ToiShape shape;
for (const FPoint& p : poly) shape.vertices.push_back({(p.x - s.ax) / kPixelsPerUnit, (p.y - s.ay) / kPixelsPerUnit});
shape.radius = 0.009999999776482582;
@@ -188,23 +203,35 @@ void PhysSystem::set_materials(std::vector<MaterialClass> table) {
for (size_t m = 0; m < materials_.size() && m < 65536; ++m) solid_lut_[m] = !materials_[m].solid ? 0 : (materials_[m].powder ? 2 : 1); // 1 static, 2 powder
}
void PhysSystem::update_terrain(const uint32_t* cells, int vx, int vy, int gw, int gh) {
// The solid mask of the window. A chunk gets its fixtures once it lies completely inside the window (with the one-cell margin the contours need) and again
// whenever a cell in or next to it changes; chunks that scroll out keep theirs (the terrain there is not simulated and does not change).
void PhysSystem::update_terrain(const uint32_t* cells, int vx, int vy, int gw, int gh, const uint32_t* terrain_age) {
// Terrain fixtures cover the loaded window, including clipped edge chunks.
// Rebuild changed cells and changed clipping; chunks outside retain their fixtures.
cur_.resize((size_t)gw * gh);
for (size_t i = 0; i < cur_.size(); ++i) cur_[i] = solid_lut_[cells[i] & 0xFFFFu];
for (size_t i = 0; i < cur_.size(); ++i) {
const uint32_t cell = cells[i], m = cell & 0xffffu;
uint8_t cls = solid_lut_[m];
if (cls == 2) {
const auto& mat = materials_[m];
const bool settled = terrain_age && (terrain_age[i] >> 16) == m && (terrain_age[i] & 255u) >= 20;
if (cell & (1u << 26)) cls = 3; // native static byte is tested before never_box2d
else if (mat.never_box2d) cls = 0;
else if (mat.sticks_to_ceiling && settled) cls = 3;
else cls = settled ? 2 : 0;
}
cur_[i] = cls;
}
auto floor_div = [](int a, int b) { return a >= 0 ? a / b : -((-a + b - 1) / b); };
std::unordered_set<int64_t> dirty, dirtyStatic, dirtySolid;
auto mark = [&](int x, int y, bool staticToo, bool solidToo) {
for (int dy = -1; dy <= 1; dy += 2) for (int dx = -1; dx <= 1; dx += 2) {
std::unordered_set<int64_t> dirty, dirtyStatic, dirtySticky;
auto mark = [&](int x, int y, unsigned changed) {
for (int dy = -8; dy <= 8; dy += 16) for (int dx = -8; dx <= 8; dx += 16) {
const int cx = floor_div(x + dx, kChunk), cy = floor_div(y + dy, kChunk);
dirty.insert(((int64_t)cx << 32) ^ (uint32_t)cy);
if (staticToo) dirtyStatic.insert(((int64_t)cx << 32) ^ (uint32_t)cy);
if (solidToo) dirtySolid.insert(((int64_t)cx << 32) ^ (uint32_t)cy);
if (changed & 1u) dirtyStatic.insert(((int64_t)cx << 32) ^ (uint32_t)cy);
if (changed & 2u) dirtySticky.insert(((int64_t)cx << 32) ^ (uint32_t)cy);
}
dirty.insert(((int64_t)floor_div(x, kChunk) << 32) ^ (uint32_t)floor_div(y, kChunk));
if (staticToo) dirtyStatic.insert(((int64_t)floor_div(x, kChunk) << 32) ^ (uint32_t)floor_div(y, kChunk));
if (solidToo) dirtySolid.insert(((int64_t)floor_div(x, kChunk) << 32) ^ (uint32_t)floor_div(y, kChunk));
if (changed & 1u) dirtyStatic.insert(((int64_t)floor_div(x, kChunk) << 32) ^ (uint32_t)floor_div(y, kChunk));
if (changed & 2u) dirtySticky.insert(((int64_t)floor_div(x, kChunk) << 32) ^ (uint32_t)floor_div(y, kChunk));
};
if (prev_.size() == cur_.size() && prev_w_ == gw && prev_h_ == gh) {
const int ox0 = std::max(vx, prev_vx_), ox1 = std::min(vx, prev_vx_) + gw, oy0 = std::max(vy, prev_vy_), oy1 = std::min(vy, prev_vy_) + gh;
@@ -213,36 +240,43 @@ void PhysSystem::update_terrain(const uint32_t* cells, int vx, int vy, int gw, i
const uint8_t* b = &prev_[(size_t)(y - prev_vy_) * gw + (ox0 - prev_vx_)];
const int n = ox1 - ox0;
if (n <= 0 || std::memcmp(a, b, (size_t)n) == 0) continue;
for (int x = 0; x < n; ++x) if (a[x] != b[x]) mark(ox0 + x, y, (a[x] == 1) != (b[x] == 1), a[x] != 2 && b[x] != 2); // (2: powder)
for (int x = 0; x < n; ++x) if (a[x] != b[x]) mark(ox0 + x, y, unsigned((a[x] == 1) != (b[x] == 1)) | (unsigned((a[x] == 3) != (b[x] == 3)) << 1)); // (2: powder)
}
}
prev_ = cur_; prev_vx_ = vx; prev_vy_ = vy; prev_w_ = gw; prev_h_ = gh;
SolidFn solid = [&](int x, int y) {
const int gx = x - vx, gy = y - vy;
return gx >= 0 && gy >= 0 && gx < gw && gy < gh && cur_[(size_t)gy * gw + gx] != 0;
return gx >= 0 && gy >= 0 && gx < gw && gy < gh && cur_[(size_t)gy * gw + gx] == 2;
};
SolidFn solidStatic = [&](int x, int y) {
const int gx = x - vx, gy = y - vy;
return gx >= 0 && gy >= 0 && gx < gw && gy < gh && cur_[(size_t)gy * gw + gx] == 1;
};
// Keep fixed terrain boundaries when settling/attached powder grows around
// a body. Sharing one contour removes the masonry surface below a body
// already inside mud, leaving it inside a collision shell down to the wall's
// underside. Both independent surfaces retain native category-1 filtering.
SolidFn solidSticky = [&](int x, int y) {
const int gx = x - vx, gy = y - vy;
return gx >= 0 && gy >= 0 && gx < gw && gy < gh && cur_[(size_t)gy * gw + gx] == 3;
};
CoefficientFn coeff = [&](int x, int y) {
const int gx = x - vx, gy = y - vy;
if (gx < 0 || gy < 0 || gx >= gw || gy >= gh) return TerrainCoefficients{};
const uint32_t m = cells[(size_t)gy * gw + gx] & 0xFFFFu;
return m < materials_.size() ? TerrainCoefficients{materials_[m].friction, materials_[m].restitution} : TerrainCoefficients{};
};
const int cx0 = floor_div(vx + 1 + kChunk - 1, kChunk), cx1 = floor_div(vx + gw - 1 - kChunk, kChunk); // chunks with a cell of margin on every side inside the window
const int cy0 = floor_div(vy + 1 + kChunk - 1, kChunk), cy1 = floor_div(vy + gh - 1 - kChunk, kChunk);
// A built chunk whose cells changed is rebuilt only while a body is near it (its box, the radius + 16 cells + two frames of its motion, touches the chunk):
// the fixtures matter to the bodies only, and moving powder (the snow of the Hiisi base) dirtied dozens of chunks every frame (4 ms, and the rebuild woke
// every body on them). The others stay stale until a body comes. A chunk where only powder moved (falling snow) waits 10 frames even so.
// A solid change waits 4 frames (the first change counts; powder only: 10): the snow cave's ice melts and freezes again cell by cell all the time, and a rebuild every
// frame near the bodies floating in its pools (which it also woke) was 2-3 ms of each physics step
for (int64_t k : dirty) if (built_.count(k)) { auto [it, fresh] = stale_.emplace(k, frame_); if (dirtySolid.count(k) && (fresh || it->second > frame_ - 6) && !std::getenv("NG_NOSOLIDDELAY")) it->second = std::min<std::remove_reference_t<decltype(it->second)>>(it->second, frame_ - 6); }
const int cx0 = floor_div(vx, kChunk), cx1 = floor_div(vx + gw - 1, kChunk);
const int cy0 = floor_div(vy, kChunk), cy1 = floor_div(vy + gh - 1, kChunk);
// Dirty terrain is rebuilt before stepping nearby bodies. Settling classification
// keeps moving powder out of this queue; no extra debounce delays removed supports.
// The eight-cell dirty halo also covers powder contours inset across chunk seams.
for (int64_t k : dirty) if (built_.count(k)) stale_.insert(k);
for (int64_t k : dirtyStatic) if (built_.count(k)) stale_static_.insert(k);
for (int64_t k : dirtySticky) if (built_.count(k)) stale_sticky_.insert(k);
std::vector<std::array<double, 4>> near; // the boxes of the active bodies, in cells
for (const Spec& s : specs_) {
if (s.cpu < 0 || s.frozen) continue;
if (s.cpu < 0) continue;
const auto& b = world_.bodies[s.cpu];
const double px = b.sweep.center_end.x * kPixelsPerUnit, py = b.sweep.center_end.y * kPixelsPerUnit;
const double r = (s.shape >= 0 ? shapes_[s.shape].radius : 8.0) + 16.0 + 2.0 * std::hypot(b.velocity.x, b.velocity.y) * kPixelsPerUnit / 60.0;
@@ -256,9 +290,13 @@ void PhysSystem::update_terrain(const uint32_t* cells, int vx, int vy, int gw, i
std::vector<std::pair<int, int>> rebuilt;
for (int cy = cy0; cy <= cy1; ++cy)
for (int cx = cx0; cx <= cx1; ++cx) {
if (cx * kChunk - 1 < vx || cy * kChunk - 1 < vy || cx * kChunk + kChunk + 1 > vx + gw || cy * kChunk + kChunk + 1 > vy + gh) continue;
const int64_t key = ((int64_t)cx << 32) ^ (uint32_t)cy;
const bool known = built_.count(key) != 0;
// Edge chunks are clipped to the loaded cells. Rebuild when scrolling
// changes that coverage, including when a clipped chunk becomes full.
const std::array<int, 4> coverage = {std::max(vx, cx * kChunk - 1), std::max(vy, cy * kChunk - 1),
std::min(vx + gw, (cx + 1) * kChunk + 1), std::min(vy + gh, (cy + 1) * kChunk + 1)};
if (known && terrain_coverage_[key] != coverage) { stale_.insert(key); stale_static_.insert(key); stale_sticky_.insert(key); }
if (!known) { // a chunk new in the window is built once it has been there 15 frames: after a teleport the pixel scenes reach the cells a few frames after the
// terrain, and a chunk built at once had the bare rock's outline (the holy mountain's statue fell 6 px onto it, then sat inside the brick's thin shell, shaking);
// the bodies over it stay frozen until it is built
@@ -267,18 +305,20 @@ void PhysSystem::update_terrain(const uint32_t* cells, int vx, int vy, int gw, i
}
if (known) {
const auto st = stale_.find(key);
if (st == stale_.end() || (st->second >= 0 && frame_ - st->second < 10) || !body_near(cx, cy)) continue;
if (st == stale_.end() || !body_near(cx, cy)) continue;
}
terrain_->rebuild_chunk(solid, coeff, cx, cy);
if (!known || stale_static_.count(key)) terrain_static_->rebuild_chunk(solidStatic, coeff, cx, cy);
built_.insert(key); stale_.erase(key); stale_static_.erase(key); first_seen_.erase(key);
if (!known || stale_sticky_.count(key)) terrain_sticky_->rebuild_chunk(solidSticky, coeff, cx, cy);
built_.insert(key); stale_.erase(key); stale_static_.erase(key); stale_sticky_.erase(key); first_seen_.erase(key);
terrain_coverage_[key] = coverage;
rebuilt.push_back({cx, cy});
}
if (std::getenv("NG_PHYSPROF")) std::printf(" chunks %d..%d x %d..%d, rebuilt %zu, built %zu, fixtures total %zu, contacts %zu%c", cx0, cx1, cy0, cy1, rebuilt.size(), built_.size(), world_.fixtures.size(), world_.contacts.size(), 10);
// chunks far from the window go away
for (auto it = built_.begin(); it != built_.end();) {
const int cx = (int)(*it >> 32), cy = (int)(int32_t)(*it & 0xFFFFFFFF);
if (cx < floor_div(vx, kChunk) - 3 || cx > floor_div(vx + gw, kChunk) + 3 || cy < floor_div(vy, kChunk) - 3 || cy > floor_div(vy + gh, kChunk) + 3) { terrain_->remove_chunk(cx, cy); terrain_static_->remove_chunk(cx, cy); stale_.erase(*it); stale_static_.erase(*it); it = built_.erase(it); }
if (cx < floor_div(vx, kChunk) - 3 || cx > floor_div(vx + gw, kChunk) + 3 || cy < floor_div(vy, kChunk) - 3 || cy > floor_div(vy + gh, kChunk) + 3) { terrain_->remove_chunk(cx, cy); terrain_static_->remove_chunk(cx, cy); terrain_sticky_->remove_chunk(cx, cy); stale_.erase(*it); stale_static_.erase(*it); stale_sticky_.erase(*it); terrain_coverage_.erase(*it); it = built_.erase(it); }
else ++it;
}
for (auto it = first_seen_.begin(); it != first_seen_.end();) { // (a chunk that left the window before it was built starts its wait again when it comes back)
@@ -379,7 +419,7 @@ void PhysSystem::set_go_through_sand(int index, bool on) {
Spec& s = specs_[index];
s.go_through_sand = on;
if (s.cpu >= 0)
for (int fi : world_.bodies[s.cpu].fixtures) noita::cpu::set_fixture_filter(world_, fi, {world_.fixtures[fi].filter.category, (std::uint16_t)(on ? 0xffff & ~4 : 0xffff & ~kStaticOnlyCategory), world_.fixtures[fi].filter.group});
for (int fi : world_.bodies[s.cpu].fixtures) noita::cpu::set_fixture_filter(world_, fi, {world_.fixtures[fi].filter.category, (std::uint16_t)(on ? 0xffff & ~4 : 0xffff), world_.fixtures[fi].filter.group});
}
void PhysSystem::shove(double x, double y, double radius, double power) {
@@ -457,7 +497,7 @@ void PhysSystem::explosion_force(double x, double y, double radius, double fmin,
pending_explosions_.push_back({x, y, radius, fmin, fmax});
}
void PhysSystem::step(const uint32_t* cells, int vx, int vy, int gw, int gh) {
void PhysSystem::step(const uint32_t* cells, int vx, int vy, int gw, int gh, const uint32_t* terrain_age) {
using namespace noita::cpu;
++frame_;
const bool prof = std::getenv("NG_PHYSPROF") != nullptr;
@@ -465,7 +505,7 @@ void PhysSystem::step(const uint32_t* cells, int vx, int vy, int gw, int gh) {
auto lap = [&](const char* what) { if (prof) { const auto t1 = std::chrono::steady_clock::now(); std::printf(" %s %.2f ms%c", what, std::chrono::duration<double, std::milli>(t1 - t0).count(), 10); t0 = t1; } };
create_pending();
lap("create");
update_terrain(cells, vx, vy, gw, gh);
update_terrain(cells, vx, vy, gw, gh, terrain_age);
lap("terrain");
// explosions: the force of 00657960 on every body in range that the blast can reach (line of sight through the cells as the stand-in of the 360-bin gate)
PhysicsUnits units;
@@ -519,17 +559,24 @@ void PhysSystem::step(const uint32_t* cells, int vx, int vy, int gw, int gh) {
// bodies outside the simulated window have no terrain around them: they are frozen (and wake up when the window comes back)
for (Spec& s : specs_) {
if (s.cpu < 0) continue;
const Vec2d p = world_.bodies[s.cpu].sweep.center_end; // (the centre of mass: the radius is measured from it; the transform position is the anchor, a corner of a chunk's image)
const double px = p.x * kPixelsPerUnit, py = p.y * kPixelsPerUnit;
const double rr = (s.shape >= 0 ? shapes_[s.shape].radius : 8.0) + 4.0;
const auto& body = world_.bodies[s.cpu];
const auto& bounds = shapes_[s.shape].bounds;
std::array<double, 4> box = {1e30, 1e30, -1e30, -1e30};
for (double x : {bounds[0], bounds[2]}) for (double y : {bounds[1], bounds[3]}) {
const double wx = body.transform.position.x * kPixelsPerUnit + body.transform.cosine * (x - s.ax) - body.transform.sine * (y - s.ay);
const double wy = body.transform.position.y * kPixelsPerUnit + body.transform.sine * (x - s.ax) + body.transform.cosine * (y - s.ay);
box[0] = std::min(box[0], wx); box[1] = std::min(box[1], wy);
box[2] = std::max(box[2], wx); box[3] = std::max(box[3], wy);
}
bool covered = true; // every chunk under the body has its terrain fixtures
for (double ex : {-rr, rr}) for (double ey : {-rr, rr}) {
const int ccx = (int)std::floor((px + ex) / kChunk), ccy = (int)std::floor((py + ey) / kChunk);
for (double x : {box[0], box[2]}) for (double y : {box[1], box[3]}) {
const int ccx = (int)std::floor(x / kChunk), ccy = (int)std::floor(y / kChunk);
covered = covered && chunk_built(ccx, ccy);
}
const bool inside = covered && px >= vx + kFreezeMargin && py >= vy + kFreezeMargin && px <= vx + gw - kFreezeMargin && py <= vy + gh - kFreezeMargin;
const double mx = 1 + std::fabs(body.velocity.x) * kPixelsPerUnit / 60.0, my = 1 + std::fabs(body.velocity.y) * kPixelsPerUnit / 60.0;
const bool inside = covered && box[0] >= vx + mx && box[1] >= vy + my && box[2] <= vx + gw - mx && box[3] <= vy + gh - my;
// a frozen body is deactivated (no proxies, no contacts: neighbours cannot wake it) and reactivated when its terrain exists again
if (!inside && !s.frozen) { s.frozen = true; set_body_awake(world_, s.cpu, false); set_body_active(world_, s.cpu, false, [](WorldToi&, int) {}); }
if (!inside && !s.frozen) { s.awake_before_freeze = body.flags.awake; s.frozen = true; set_body_awake(world_, s.cpu, false); set_body_active(world_, s.cpu, false, [](WorldToi&, int) {}); }
else if (inside && s.frozen) { s.frozen = false; set_body_active(world_, s.cpu, true, [](WorldToi&, int) {}); set_body_awake(world_, s.cpu, true); }
}
if (std::getenv("NG_RELEASENAILS") && frame_ == 100) for (Spec& s : specs_) if (s.nail_on && s.cpu >= 0 && !s.frozen) { s.nail_on = false; set_world_body_type(world_, s.cpu, 2); } // debug
@@ -574,15 +621,16 @@ void PhysSystem::step(const uint32_t* cells, int vx, int vy, int gw, int gh) {
if (false && specs_.size() > 4 && specs_[4].cpu >= 0 && frame_ % 10 == 0) { const Spec& s4 = specs_[4]; const auto& b4 = world_.bodies[s4.cpu]; std::printf("spec4 frozen %d awake %d type %d y %.1f contacts %zu%c", s4.frozen, b4.flags.awake, b4.flags.type, b4.transform.position.y * 10, b4.contacts.size(), 10); }
apply_liquids(cells, vx, vy, gw, gh);
ContactListeners listeners;
listeners.begin = [&](WorldToi& w, int contact) {
listeners.pre_solve = [&](WorldToi& w, int contact, const ContactManifold& old) {
const auto& c = w.contacts[contact];
if (c.manifold.count <= 0 || contact_events_.size() >= 256) return;
if (!material_contact_added(c.manifold, old) || contact_events_.size() >= 8192) return; // native queue: 4096 contacts, two material sides
const int ba = w.fixtures[c.fixture_a].body, bb = w.fixtures[c.fixture_b].body;
// the contact point: the manifold keeps it local to the body that does not own the reference face (type 1: body B, type 2: body A)
const auto& pt = c.manifold.points[0].point;
const int host = c.manifold.type == 2 ? ba : bb;
const auto& t = w.bodies[host].transform;
const double wx = t.position.x + t.cosine * pt.x - t.sine * pt.y, wy = t.position.y + t.sine * pt.x + t.cosine * pt.y;
const auto& a = w.bodies[ba]; const auto& b = w.bodies[bb];
const SolverBody bodyA{{a.sweep.center_end, a.velocity, a.sweep.angle_end, a.angular_velocity}, a.sweep.local_center, a.inverse_mass, a.inverse_inertia};
const SolverBody bodyB{{b.sweep.center_end, b.velocity, b.sweep.angle_end, b.angular_velocity}, b.sweep.local_center, b.inverse_mass, b.inverse_inertia};
const SolverContact geometry{0, 1, c.manifold, w.fixtures[c.fixture_a].children[c.child_a].radius, w.fixtures[c.fixture_b].children[c.child_b].radius};
const auto point = world_contact_manifold(geometry, bodyA, bodyB).points[0]; // 009b5b30: actual surface contact, including skin radii
const double wx = point.x, wy = point.y;
const auto sa = cpu_to_spec_.find(ba), sb = cpu_to_spec_.find(bb);
const double va_x = w.bodies[ba].velocity.x, va_y = w.bodies[ba].velocity.y, vb_x = w.bodies[bb].velocity.x, vb_y = w.bodies[bb].velocity.y;
for (int side = 0; side < 2; ++side) {
@@ -620,6 +668,53 @@ void PhysSystem::step(const uint32_t* cells, int vx, int vy, int gw, int gh) {
};
lap("pre");
step_rigid_world(world_, kDt, listeners);
// 006f7f00 reads the body's velocity when the queued contact is dispatched
// after the physics job (0071a6b0), not its pre-solve contact-point speed.
for (auto& event : contact_events_) {
const auto& s = specs_[event.body];
if (s.cpu < 0) continue;
const auto& b = world_.bodies[s.cpu];
event.vx = b.velocity.x * kPixelsPerUnit / 60.0;
event.vy = b.velocity.y * kPixelsPerUnit / 60.0;
}
// 007440a0: only dirty/awake bodies reach this check; retaining their
// fixtures and pixels is essential (this is not a body-to-cell bake).
for (int i = 0; i < (int)specs_.size(); ++i) {
auto& s = specs_[i];
if (s.cpu < 0 || s.frozen) continue;
const auto& b = world_.bodies[s.cpu];
if (s.lifecycle.settle_static && (s.raster_dirty || b.flags.awake) && b.flags.type != 0 &&
frame_ - s.lifecycle.born > 600 && std::hypot(b.velocity.x, b.velocity.y) < 1.0)
set_static(i, true);
s.raster_dirty = false;
}
if (std::getenv("NG_CONTACTPROBE") && frame_ % 60 == 0) {
double depth = 0, terrainDepth = 0; int pairA = -1, pairB = -1, awake = 0, terrainBody = -1, terrainMaterial = -1;
for (const auto& c : world_.contacts) if (c.live && c.enabled && c.touching && c.manifold.count > 0) {
const auto& fa = world_.fixtures[c.fixture_a]; const auto& fb = world_.fixtures[c.fixture_b];
const bool bodyA = cpu_to_spec_.count(fa.body) != 0, bodyB = cpu_to_spec_.count(fb.body) != 0;
if (!bodyA && !bodyB) continue;
const auto& a = world_.bodies[fa.body]; const auto& b = world_.bodies[fb.body];
if (!a.active || !b.active) continue;
const SolverBody sa{{a.sweep.center_end, a.velocity, a.sweep.angle_end, a.angular_velocity}, a.sweep.local_center, a.inverse_mass, a.inverse_inertia};
const SolverBody sb{{b.sweep.center_end, b.velocity, b.sweep.angle_end, b.angular_velocity}, b.sweep.local_center, b.inverse_mass, b.inverse_inertia};
const SolverContact contact{0, 1, c.manifold, fa.children[c.child_a].radius, fb.children[c.child_b].radius};
const auto manifold = world_contact_manifold(contact, sa, sb);
for (int p = 0; p < c.manifold.count; ++p) {
const double d = -manifold.separations[p] * kPixelsPerUnit;
if (bodyA && bodyB && d > depth) {
depth = d; pairA = cpu_to_spec_.at(fa.body); pairB = cpu_to_spec_.at(fb.body);
awake = int(a.flags.awake) | (int(b.flags.awake) << 1);
} else if (bodyA != bodyB && d > terrainDepth) {
terrainDepth = d; terrainBody = cpu_to_spec_.at(bodyA ? fa.body : fb.body);
const int x = (int)std::floor(manifold.points[p].x * kPixelsPerUnit) - vx;
const int y = (int)std::floor(manifold.points[p].y * kPixelsPerUnit) - vy;
terrainMaterial = x >= 0 && y >= 0 && x < gw && y < gh ? int(cells[y * gw + x] & 0xFFFFu) : -1;
}
}
}
std::printf("contact probe %u: maximum body penetration %.3f px, bodies %d / %d, awake %d; terrain %.3f px, body %d, material %d\n", frame_, depth, pairA, pairB, awake, terrainDepth, terrainBody, terrainMaterial);
}
lap("box2d step");
if (prof) { int aw = 0, act = 0, dyn = 0; for (const auto& b : world_.bodies) { if (!b.live || b.flags.type != 2) continue; ++dyn; act += b.active; aw += b.active && b.flags.awake; } std::printf(" bodies: %d dynamic, %d active, %d awake%c", dyn, act, aw, 10); }
break_world_joints(world_, kPixelsPerUnit);
@@ -665,15 +760,30 @@ bool PhysSystem::body_state(int index, double* o) const {
}
void PhysSystem::set_body_state(int index, const double* v, int n) { // (b2Body::SetTransform: the origin follows the centre, the sweep starts there)
if (index < 0 || index >= (int)specs_.size() || !specs_[index].live || specs_[index].cpu < 0) return;
if (index < 0 || index >= (int)specs_.size() || !specs_[index].live) return;
Spec& s = specs_[index];
if (s.cpu < 0) {
// Fractures restore the pose before create_pending builds the replacement body.
if (n >= 3) s.angle = v[2];
if (n >= 2 && s.shape >= 0 && s.shape < (int)shapes_.size()) {
const auto& shape = shapes_[s.shape];
const double x = shape.com_x - s.ax, y = shape.com_y - s.ay;
const double c = std::cos(s.angle), sn = std::sin(s.angle);
s.ox = v[0] * kPixelsPerUnit - (c * x - sn * y);
s.oy = v[1] * kPixelsPerUnit - (sn * x + c * y);
}
if (n >= 5) { s.vx = v[3] / kUnitsPerFrameFromCells; s.vy = v[4] / kUnitsPerFrameFromCells; }
if (n >= 6) s.omega = v[5] / 60.0;
return;
}
auto& b = world_.bodies[specs_[index].cpu];
if (n >= 2) {
const double ang = n >= 3 ? v[2] : b.sweep.angle_end, c = std::cos(ang), sn = std::sin(ang);
const Vec2d lc = b.sweep.local_center;
b.transform.cosine = c; b.transform.sine = sn;
b.transform.position = Vec2d{v[0] - (c * lc.x - sn * lc.y), v[1] - (sn * lc.x + c * lc.y)};
b.sweep.center_start = b.sweep.center_end = Vec2d{v[0], v[1]};
b.sweep.angle_start = b.sweep.angle_end = ang;
const Vec2d origin{v[0] - (c * lc.x - sn * lc.y), v[1] - (sn * lc.x + c * lc.y)};
// SetTransform also moves the fixture proxies. Updating only the sweep
// leaves collision queries at the previous position after a script move.
noita::cpu::set_body_transform(world_, specs_[index].cpu, origin, ang);
}
if (n >= 5) b.velocity = Vec2d{v[3], v[4]};
if (n >= 6) b.angular_velocity = v[5];
@@ -719,6 +829,39 @@ void PhysSystem::set_static(int index, bool st) {
if (specs_[index].cpu >= 0) noita::cpu::set_world_body_type(world_, specs_[index].cpu, st ? 0 : 2);
}
void PhysSystem::set_material_lifecycle(int index, int material, bool convert, int target) {
if (index < 0 || index >= (int)specs_.size() || !specs_[index].live) return;
auto& state = specs_[index].lifecycle;
state.material = material; state.sleep_convert = convert; state.target = target;
}
void PhysSystem::set_lifecycle(int index, Lifecycle state) {
if (index >= 0 && index < (int)specs_.size() && specs_[index].live) specs_[index].lifecycle = state;
}
PhysSystem::Lifecycle PhysSystem::lifecycle(int index) const {
return index >= 0 && index < (int)specs_.size() ? specs_[index].lifecycle : Lifecycle{};
}
void PhysSystem::set_settle_static(int index, bool on) {
if (index >= 0 && index < (int)specs_.size() && specs_[index].live) specs_[index].lifecycle.settle_static = on;
}
std::vector<int> PhysSystem::terrain_conversion_candidates(int vx, int vy, int width, int height) {
const double x = vx + width * .5, y = vy + height * .5;
const double dx = x - conversion_x_, dy = y - conversion_y_;
if (dx * dx + dy * dy <= 512.0 * 512.0) return {};
conversion_x_ = x; conversion_y_ = y;
std::vector<int> out;
for (int i = 0; i < (int)specs_.size(); ++i) {
const auto& s = specs_[i];
if (s.cpu < 0 || !s.lifecycle.sleep_convert) continue;
const auto& b = world_.bodies[s.cpu];
if (b.flags.type == 0) continue;
const double bx = b.transform.position.x * kPixelsPerUnit - x, by = b.transform.position.y * kPixelsPerUnit - y;
const double distance2 = bx * bx + by * by;
const bool awake = s.frozen ? s.awake_before_freeze : b.flags.awake;
if (distance2 >= 350.0 * 350.0 && (!awake || distance2 >= 750.0 * 750.0)) out.push_back(i);
}
return out;
}
void PhysSystem::set_ai_force(int index, double fx, double fy, double torque) {
if (index < 0 || index >= (int)specs_.size() || !specs_[index].live) return;
Spec& s = specs_[index];
@@ -731,6 +874,76 @@ double PhysSystem::mass_of(int index) const {
return b.inverse_mass > 0 ? 1.0 / b.inverse_mass : 0.0;
}
std::vector<PhysSystem::DebugBody> PhysSystem::debug_bodies() const {
std::vector<DebugBody> out;
for (int i = 0; i < (int)specs_.size(); ++i) {
const auto& s = specs_[i];
if (!s.live || s.cpu < 0) continue;
const auto& b = world_.bodies[s.cpu];
out.push_back({i, s.cpu, s.shape, b.flags.type, s.group, b.flags.awake, b.active, s.frozen, s.bullet, s.nail_on,
b.sweep.center_end.x * kPixelsPerUnit, b.sweep.center_end.y * kPixelsPerUnit, b.sweep.angle_end,
b.velocity.x / kUnitsPerFrameFromCells, b.velocity.y / kUnitsPerFrameFromCells, b.angular_velocity / 60.0,
b.inverse_mass > 0 ? 1.0 / b.inverse_mass : 0});
}
return out;
}
std::vector<PhysSystem::DebugPolygon> PhysSystem::debug_polygons(const std::array<double, 4>* viewport) const {
std::vector<DebugPolygon> out;
for (int fi = 0; fi < (int)world_.fixtures.size(); ++fi) {
const auto& f = world_.fixtures[fi];
if (!f.live || f.body < 0 || !world_.bodies[f.body].live) continue;
const auto& t = world_.bodies[f.body].transform;
const auto it = cpu_to_spec_.find(f.body);
for (size_t ci = 0; ci < f.children.size(); ++ci) {
const auto& ch = f.children[ci];
// Synchronization already maintains conservative child bounds. Inactive
// bodies may lack current proxies, so leave those in the fallback path.
if (viewport && world_.bodies[f.body].active && ci < f.bounds.size()) {
const auto& box = f.bounds[ci];
if (box.upper.x * kPixelsPerUnit < (*viewport)[0] || box.lower.x * kPixelsPerUnit > (*viewport)[2] ||
box.upper.y * kPixelsPerUnit < (*viewport)[1] || box.lower.y * kPixelsPerUnit > (*viewport)[3]) continue;
}
if (viewport && (!world_.bodies[f.body].active || ci >= f.bounds.size())) {
// Frozen bodies have no active proxy. Compute a conservative box
// without allocating their vertex vectors or trusting stale bounds.
double x0 = 1e300, y0 = 1e300, x1 = -1e300, y1 = -1e300;
for (const auto& v : ch.vertices) {
const double x = (t.position.x + t.cosine * v.x - t.sine * v.y) * kPixelsPerUnit;
const double y = (t.position.y + t.sine * v.x + t.cosine * v.y) * kPixelsPerUnit;
x0 = std::min(x0, x); y0 = std::min(y0, y); x1 = std::max(x1, x); y1 = std::max(y1, y);
}
const double radius = ch.radius * kPixelsPerUnit;
if (x1 + radius < (*viewport)[0] || x0 - radius > (*viewport)[2] ||
y1 + radius < (*viewport)[1] || y0 - radius > (*viewport)[3]) continue;
}
DebugPolygon p{it == cpu_to_spec_.end() ? -1 : it->second, fi, f.filter.category, f.filter.mask, f.filter.group, f.sensor, ch.radius * kPixelsPerUnit, {}};
p.points.reserve(ch.vertices.size());
for (const auto& v : ch.vertices) p.points.push_back({(t.position.x + t.cosine * v.x - t.sine * v.y) * kPixelsPerUnit,
(t.position.y + t.sine * v.x + t.cosine * v.y) * kPixelsPerUnit});
out.push_back(std::move(p));
}
}
return out;
}
std::vector<PhysSystem::DebugContact> PhysSystem::debug_contacts() const {
using namespace noita::cpu;
std::vector<DebugContact> out;
for (const auto& c : world_.contacts) {
if (!c.live || !c.enabled || !c.touching || !c.manifold.count) continue;
const auto& fa = world_.fixtures[c.fixture_a]; const auto& fb = world_.fixtures[c.fixture_b];
const auto& a = world_.bodies[fa.body]; const auto& b = world_.bodies[fb.body];
if (!a.active || !b.active) continue;
const SolverBody sa{{a.sweep.center_end, a.velocity, a.sweep.angle_end, a.angular_velocity}, a.sweep.local_center, a.inverse_mass, a.inverse_inertia};
const SolverBody sb{{b.sweep.center_end, b.velocity, b.sweep.angle_end, b.angular_velocity}, b.sweep.local_center, b.inverse_mass, b.inverse_inertia};
const SolverContact sc{0, 1, c.manifold, fa.children[c.child_a].radius, fb.children[c.child_b].radius};
const auto m = world_contact_manifold(sc, sa, sb);
const auto ia = cpu_to_spec_.find(fa.body), ib = cpu_to_spec_.find(fb.body);
for (int k = 0; k < c.manifold.count; ++k) out.push_back({ia == cpu_to_spec_.end() ? -1 : ia->second, ib == cpu_to_spec_.end() ? -1 : ib->second,
m.points[k].x * kPixelsPerUnit, m.points[k].y * kPixelsPerUnit, m.separations[k] * kPixelsPerUnit});
}
return out;
}
Pose PhysSystem::pose(int index) const {
Pose p;
if (index < 0 || index >= (int)specs_.size()) return p;
+49 -11
View File
@@ -3,6 +3,7 @@
// of the props and the joints of the corpses. `GpuBodies` (src/physics/gpu_bodies.*) is the interface the game uses; it forwards everything here and mirrors the
// poses into the GPU buffers that the cell, entity and fire passes read.
#include <cstdint>
#include <array>
#include <functional>
#include <memory>
#include <unordered_map>
@@ -22,9 +23,10 @@ namespace ng::phys {
struct MaterialClass {
bool solid = false; // terrain for the bodies (static cells)
bool powder = false; // sand and other powders: they hold a nail but are not terrain
bool powder = false; // dynamic powder: terrain eligibility depends on its live settling state
bool liquid = false; // a liquid that is not a powder (the single probe of the water force of the bodies)
float friction = 0.3f, restitution = 0.2f; // solid_friction / solid_restitution (CellData defaults 0.3 / 0.2)
bool never_box2d = false, sticks_to_ceiling = false;
};
struct Pose {
@@ -36,7 +38,14 @@ struct Pose {
// An impact the game reacts to (fragile props): the speed change of the body at a contact, in cells per frame.
struct Impact { int body = -1; double speed = 0; };
// A new contact of a body (the contact listener's begin, 006f8c20): where, the body's linear velocity (cells per frame) and the closing speed along the normal (cells per frame).
// 006f8c20 -> 009b6070: effects use the first manifold point's added state,
// including a new feature on a persistent contact. Other newly added points do not trigger it.
inline bool material_contact_added(const noita::cpu::ContactManifold& current, const noita::cpu::ContactManifold& previous) {
if (current.count <= 0) return false;
for (int i = 0; i < previous.count; ++i) if (previous.points[i].feature_id == current.points[0].feature_id) return false;
return true;
}
// A newly added first contact point: where, post-step linear velocity and pre-solve closing/point speed.
struct ContactEvent { int body = -1, other = -1; double x = 0, y = 0, vx = 0, vy = 0, normal_speed = 0, mass = 0, point_speed = 0; }; // point_speed: the speed of the body at the contact point (linear + angular), pixels per second // mass: physics units (density x area in unit^2)
class PhysSystem {
@@ -44,11 +53,12 @@ public:
PhysSystem();
void set_materials(std::vector<MaterialClass> table);
int add_shape(const RigidShape& s); // same numbering as GpuBodies::add_shape
int add_shape(const RigidShape& s, int reuse = -1); // same numbering as GpuBodies::add_shape
// `index` is the slot of the GpuBodies mirror. The Box2D body is created at the next step, after the setters below have run (the mass depends on the density).
void add_body(int index, int shape, double anchor_x, double anchor_y, double origin_x, double origin_y);
void remove_body(int index);
void set_motion(int index, double vx, double vy, double omega); // cells and radians per frame
void set_bullet(int index, bool on); // continuous collision with other dynamic debris
void set_surface(int index, double friction, double restitution);
void set_density(int index, double density);
void set_group(int index, double group);
@@ -80,8 +90,9 @@ public:
// b2Body::ApplyLinearImpulse(impulse, point, wake) on body `index`: impulse in kg units / s, the point in pixels (a projectile's hit, 00c073b0)
void impulse_at(int index, double x, double y, double ix, double iy);
// One frame. `cells` is the cell grid of the simulated window (material in the low 16 bits), `view` its world origin.
void step(const uint32_t* cells, int view_x, int view_y, int grid_w, int grid_h);
// One frame. `terrain_age` holds the GPU material stamp (high 16) / rest age (low 8).
// Without live state, dynamic powder is excluded; static cells still collide.
void step(const uint32_t* cells, int view_x, int view_y, int grid_w, int grid_h, const uint32_t* terrain_age = nullptr);
std::unordered_map<int64_t, int> first_seen_; // (update_terrain: the frame a chunk not yet built was first inside the window)
Pose pose(int index) const;
// PhysicsAIComponent (00bce0a0): a force (physics units: mass x units / s^2) and a torque added to the body in the next step, set every frame by the host
@@ -98,21 +109,42 @@ public:
bool gravity_scale(int index, double& g) const;
void set_gravity_scale(int index, double g);
void set_static(int index, bool is_static);
// Native body record +0x29/+0x31/+0x34: distant cell conversion and old-debris settling are separate paths.
struct Lifecycle { int material = -1, target = -1; bool sleep_convert = false, settle_static = false; uint32_t born = 0; };
void set_material_lifecycle(int index, int material, bool sleep_convert, int target);
void set_lifecycle(int index, Lifecycle state);
Lifecycle lifecycle(int index) const;
void set_settle_static(int index, bool on);
// Returns candidates without removing them: the host first persists their remaining pixels.
std::vector<int> terrain_conversion_candidates(int view_x, int view_y, int width, int height);
double mass_of(int index) const; // physics units (0: none or static)
bool nailed(int index) const { return index >= 0 && index < (int)specs_.size() && specs_[index].nail_on; }
std::vector<ContactEvent>& contacts() { return contact_events_; } // new contacts since the last clear
std::vector<Impact>& impacts() { return impacts_; } // since the last call to clear
int terrain_fixtures() const { return terrain_fixtures_; }
// Diagnostic copies use the solver's actual fixture transforms, in world pixels.
struct DebugBody {
int index, cpu, shape, type, group;
bool awake, active, frozen, bullet, nailed;
double x, y, angle, vx, vy, omega, mass;
};
struct DebugPolygon { int body, fixture, category, mask, group; bool sensor; double radius; std::vector<std::array<double, 2>> points; };
struct DebugContact { int a, b; double x, y, separation; };
std::vector<DebugBody> debug_bodies() const;
// Optional pixel-space viewport for drawing; null retains the complete dump.
std::vector<DebugPolygon> debug_polygons(const std::array<double, 4>* viewport = nullptr) const;
std::vector<DebugContact> debug_contacts() const;
private:
struct ShapeData {
std::vector<ConvexPoly> polys; // image pixels
double com_x = 0, com_y = 0, mass = 1, radius = 1;
std::array<double, 4> bounds{}; // min x/y, max x/y in image pixels
std::vector<std::pair<double, double>> samples; // outline samples relative to the pixel centre of mass
};
struct Spec {
int shape = -1;
double ax = 0, ay = 0, ox = 0, oy = 0;
double ax = 0, ay = 0, ox = 0, oy = 0, angle = 0;
double vx = 0, vy = 0, omega = 0;
double friction = 0.3, restitution = 0.2, density = 1.0;
int group = 0;
@@ -126,27 +158,32 @@ private:
bool nail_surface = false, nail_anchored = false; double nail_rx = 0, nail_ry = -10; int nail_ax = 0, nail_ay = 0;
int cpu = -1;
bool live = false, frozen = false;
bool awake_before_freeze = true, raster_dirty = true;
Lifecycle lifecycle;
double ai_fx = 0, ai_fy = 0, ai_torque = 0; uint32_t ai_frame = 0; bool ai_set = false;
double sf_x = 0, sf_y = 0; // forces of scripts (PhysicsApplyForce) for the next step
struct Joint { int other; double ix, iy, force; int kind; int tag = -1; };
std::vector<Joint> joints;
};
void create_pending();
void update_terrain(const uint32_t* cells, int vx, int vy, int gw, int gh);
void update_terrain(const uint32_t* cells, int vx, int vy, int gw, int gh, const uint32_t* terrain_age);
bool chunk_built(int cx, int cy) const { return built_.count(((int64_t)cx << 32) ^ (uint32_t)cy) != 0; }
std::unordered_map<int64_t, std::array<int, 4>> terrain_coverage_;
void apply_liquids(const uint32_t* cells, int vx, int vy, int gw, int gh);
std::vector<MaterialClass> materials_;
std::vector<ShapeData> shapes_;
std::vector<JointBreak> joint_breaks_;
std::vector<Spec> specs_;
noita::cpu::WorldToi world_;
std::unique_ptr<TerrainPhysics> terrain_; // everything solid for the bodies (static cells and powders)
std::unique_ptr<TerrainPhysics> terrain_static_; // the static cells alone: the terrain of the bodies that go through sand
std::vector<uint8_t> solid_lut_; // material id -> 1 when the cell is terrain for the bodies
std::unique_ptr<TerrainPhysics> terrain_; // native class 2: settled loose powder (category 4)
std::unique_ptr<TerrainPhysics> terrain_static_; // fixed class-1 terrain (category 1)
std::unique_ptr<TerrainPhysics> terrain_sticky_; // mobile/attached class-1 powder, separate from fixed terrain
std::vector<uint8_t> solid_lut_; // material id -> 0 excluded, 1 static, 2 live powder classification required
std::vector<uint8_t> cur_, prev_; // solid mask of the window now and at the last step
int prev_vx_ = 0, prev_vy_ = 0, prev_w_ = 0, prev_h_ = 0;
std::unordered_set<int64_t> built_; // chunks that have terrain fixtures
std::unordered_map<int64_t, int> stale_; // built chunks whose cells changed, not rebuilt yet: the frame of the first powder-only change (-1: a solid one)
std::unordered_set<int64_t> stale_; // changed chunks, rebuilt before a nearby body steps
std::unordered_set<int64_t> stale_sticky_; // changed attached/settled class-1 powder
std::unordered_set<int64_t> stale_static_; // (and their static fixtures too)
struct ExplosionRequest { double x, y, radius, fmin, fmax; };
std::vector<ExplosionRequest> pending_explosions_;
@@ -157,6 +194,7 @@ private:
std::unordered_map<int, int> joint_tags_; // add_joint tag -> world joint
int terrain_fixtures_ = 0;
uint32_t frame_ = 0;
double conversion_x_ = 0, conversion_y_ = 0;
};
} // namespace ng::phys
+46
View File
@@ -1,4 +1,5 @@
#include "phys/terrain_outline.h"
#include "clipper.hpp"
#include <algorithm>
#include <cmath>
@@ -165,6 +166,51 @@ ChunkOutline trace_chunk(const SolidFn& solid, int cx, int cy, float tolerance)
return out;
}
ChunkOutline trace_powder_chunk(const SolidFn& solid, int cx, int cy) {
// 008a9250 traces the chunk's matrix; 00810950 offsets each contour
// separately, retains the first result, and bridges its x seams explicitly.
const int x0 = cx * kChunk, y0 = cy * kChunk;
std::vector<uint8_t> mask(kChunk * kChunk);
for (int y = 0; y < kChunk; ++y) for (int x = 0; x < kChunk; ++x)
mask[y * kChunk + x] = solid(x0 + x, y0 + y);
ChunkOutline out;
for (const auto& loop : trace_mask(mask.data(), kChunk, kChunk, 1.5f)) {
if (loop.size() < 4) continue;
ClipperLib::Polygon polygon;
IPoint anchors[2]{};
bool found[2] = {}, used[2] = {};
for (size_t i = 0; i + 1 < loop.size(); ++i) {
const IPoint p{loop[i].x + x0, loop[i].y + y0};
polygon.emplace_back(p.x, p.y);
if ((p.x & 63) != 0) continue;
for (int side = 0; side < 2; ++side) {
const auto& a = anchors[side];
if (!found[side] || (side == 0 ? p.x < a.x : p.x > a.x) || (p.x == a.x && p.y < a.y)) {
anchors[side] = p; found[side] = true;
}
}
}
ClipperLib::Polygons inset;
ClipperLib::OffsetPolygons({polygon}, inset, -4.0, ClipperLib::jtSquare, 0.0, true);
if (inset.empty() || inset.front().empty()) continue;
std::vector<IPoint> line;
for (const auto& v : inset.front()) {
const IPoint p{(int)v.X, (int)v.Y};
for (int side = 0; side < 2; ++side) {
const auto& a = anchors[side];
if (found[side] && !used[side] && std::abs(p.x - a.x) <= 5 && std::abs(p.y - a.y) <= 5) {
out.connectors.push_back({{a.x, std::max(a.y, p.y)}, p});
used[side] = true;
}
}
line.push_back(p); // retain Clipper winding and unsnapped offset vertices
}
line.push_back(line.front());
out.lines.push_back(std::move(line));
}
return out;
}
std::vector<std::vector<IPoint>> trace_mask(const uint8_t* mask, int w, int h, float tolerance, std::vector<bool>* is_hole) {
SolidFn solid = [&](int x, int y) { return x >= 0 && y >= 0 && x < w && y < h && mask[(size_t)y * w + x] != 0; };
auto owned = [](int, int, int) { return true; };
+4
View File
@@ -20,12 +20,16 @@ constexpr int kChunk = 64; // `(x & 0x3f) == 0` in 00810950: the terrain physi
// of a world tile the contours without duplicates and neighbouring chunks meet at the same lattice points.
struct ChunkOutline {
std::vector<std::vector<IPoint>> lines;
std::vector<std::vector<IPoint>> connectors; // 00810950: separate two-point seam fixtures
};
// Traces the contours around chunk (cx, cy) (cells [cx*64, cx*64+64) x [cy*64, cy*64+64)). `tolerance` is the Douglas-Peucker distance in cells (1 as in the
// pixel-body contours, 0095bcf0).
ChunkOutline trace_chunk(const SolidFn& solid, int cx, int cy, float tolerance = 1.0f);
// 00810950: settled powder contours are inset four pixels with Clipper.
ChunkOutline trace_powder_chunk(const SolidFn& solid, int cx, int cy);
// Contours of a whole mask (tests and pixel bodies): the same tracing on the rectangle [0,w) x [0,h) of `mask` (non-zero = solid), without chunk splitting.
std::vector<std::vector<IPoint>> trace_mask(const uint8_t* mask, int w, int h, float tolerance, std::vector<bool>* is_hole = nullptr);
+14 -6
View File
@@ -14,12 +14,18 @@ using noita::cpu::Vec2d;
// (the solid side, n = (dy, -dx) / len) with no extension along the segment (the extension argument is 0 at both call sites in 008102c0). The thickness is
// 0.2 * 0.5 = 0.1 unit (one cell); segments shorter than 0.5 unit and segments in the bottom 0.4 unit of the chunk (local y >= 6.0) are drawn with
// 0.2 * 0.2 * 0.25 = 0.01 unit.
bool segment_quad(Vec2d p, Vec2d q, double chunk_bottom_local_y, std::vector<Vec2d>& out) {
bool segment_quad(Vec2d p, Vec2d q, double chunk_bottom_local_y, bool powder, bool connector, std::vector<Vec2d>& out) {
const double dx = q.x - p.x, dy = q.y - p.y, len2 = dx * dx + dy * dy;
if (len2 <= 0.0) return false;
const double len = std::sqrt(len2);
const bool thin = (p.y >= chunk_bottom_local_y && q.y >= chunk_bottom_local_y) || len2 < 0.25;
const double t = thin ? 0.01 : 0.1;
// 0080f9f0's class-2 flag accepts only upward-facing long surfaces.
// Short/bottom segments use 0080fc20, which has no angle filter.
if (powder && !thin && !connector) {
const double angle = std::atan2(-dx, dy);
if (angle < -3.1415 || angle >= 0.0) return false;
}
const double t = connector ? 0.05 : thin ? 0.01 : 0.1;
const double nx = dy / len * t, ny = -dx / len * t;
out = {p, q, {q.x + nx, q.y + ny}, {p.x + nx, p.y + ny}};
double area2 = 0;
@@ -44,7 +50,7 @@ void TerrainPhysics::remove_chunk(int cx, int cy) {
int TerrainPhysics::rebuild_chunk(const SolidFn& solid, const CoefficientFn& coeff, int cx, int cy) {
using namespace noita::cpu;
ChunkOutline outline = trace_chunk(solid, cx, cy);
ChunkOutline outline = category_ == 4 ? trace_powder_chunk(solid, cx, cy) : trace_chunk(solid, cx, cy, 1.5f);
int body = chunk_body(cx, cy);
if (body < 0) {
BodyDefinition def;
@@ -59,18 +65,19 @@ int TerrainPhysics::rebuild_chunk(const SolidFn& solid, const CoefficientFn& coe
}
const double ox = cx * (double)kChunk, oy = cy * (double)kChunk, bottom = (double)kChunk / kPixelsPerUnit - 0.4;
int made = 0;
for (const auto& line : outline.lines) {
for (int pass = 0; pass < 2; ++pass) {
for (const auto& line : pass == 0 ? outline.lines : outline.connectors) {
for (size_t i = 0; i + 1 < line.size(); ++i) {
const Vec2d p{(line[i].x - ox) / kPixelsPerUnit, (line[i].y - oy) / kPixelsPerUnit};
const Vec2d q{(line[i + 1].x - ox) / kPixelsPerUnit, (line[i + 1].y - oy) / kPixelsPerUnit};
std::vector<Vec2d> quad;
if (!segment_quad(p, q, bottom, quad)) continue;
if (!segment_quad(p, q, bottom, category_ == 4, pass == 1, quad)) continue;
// the cell on the solid side of the segment (left of the direction of travel, y down) gives the surface coefficients
const double ddx = line[i + 1].x - line[i].x, ddy = line[i + 1].y - line[i].y, dl = std::sqrt(ddx * ddx + ddy * ddy);
const int scx = (int)std::floor((line[i].x + line[i + 1].x) * 0.5 + 0.5 * ddy / dl), scy = (int)std::floor((line[i].y + line[i + 1].y) * 0.5 - 0.5 * ddx / dl);
const TerrainCoefficients c = coeff ? coeff(scx, scy) : TerrainCoefficients{};
WorldFixture f;
f.filter = {category_, 0xffdf, 0}; // 008102c0: category 4, mask 0xffdf
f.filter = {category_, (std::uint16_t)(category_ == 1 ? 0xffff : 0xffdf), 0}; // 008102c0: category 4, mask 0xffdf
f.friction = c.friction;
f.restitution = c.restitution;
f.density = 0;
@@ -81,6 +88,7 @@ int TerrainPhysics::rebuild_chunk(const SolidFn& solid, const CoefficientFn& coe
if (create_world_fixture(w_, body, std::move(f)) >= 0) ++made;
}
}
}
return made;
}
+3 -4
View File
@@ -10,7 +10,6 @@
namespace ng::phys {
constexpr std::uint16_t kStaticOnlyCategory = 0x4000; // (not an engine category: the second terrain of the static cells, see PhysSystem)
constexpr double kPixelsPerUnit = 10.0; // 00718740: one physics unit is ten cells (`mConverts.rate` = W / (W/10)) in both axes
struct TerrainCoefficients { float friction = 0.3f, restitution = 0.2f; }; // CellData defaults solid_friction / solid_restitution
@@ -18,8 +17,8 @@ using CoefficientFn = std::function<TerrainCoefficients(int cellX, int cellY)>;
class TerrainPhysics {
public:
// category: the fixture category of this terrain (4 = 008102c0's, everything solid for the bodies; kStaticOnlyCategory = the static cells alone, for the bodies that go through sand)
explicit TerrainPhysics(noita::cpu::WorldToi& world, std::uint16_t category = 4) : w_(world), category_(category) {}
// Native terrain classes: category 1 = static cells; category 4 = settled powder.
explicit TerrainPhysics(noita::cpu::WorldToi& world, std::uint16_t category = 1) : w_(world), category_(category) {}
// Rebuilds the fixtures of chunk (cx, cy) from the solid mask. Returns the number of segment fixtures.
int rebuild_chunk(const SolidFn& solid, const CoefficientFn& coeff, int cx, int cy);
void remove_chunk(int cx, int cy);
@@ -28,7 +27,7 @@ public:
private:
static int64_t key(int cx, int cy) { return ((int64_t)cx << 32) ^ (uint32_t)cy; }
noita::cpu::WorldToi& w_;
std::uint16_t category_ = 4;
std::uint16_t category_ = 1;
std::unordered_map<int64_t, int> bodies_;
};
+68 -5
View File
@@ -19,22 +19,46 @@ void GpuBodies::attach(void* bodyBuffer, uint32_t* shapeWords) {
words_ = shapeWords;
next_ = (size_t)kMaxShapes * kShapeHeaderWords;
shapes_ = 0;
shape_epoch_ = 0; shape_live_.clear();
info_.clear();
}
int GpuBodies::add_shape(const RigidShape& s) {
const size_t need = (size_t)s.w * s.h + s.samples.size() * 2;
if (shapes_ >= kMaxShapes || next_ + need > kShapeWords) return -1;
uint32_t* h = words_ + (size_t)shapes_ * kShapeHeaderWords;
int index = -1;
for (int k = 0; k < shapes_; ++k) if (!shape_live_[k]) { index = k; break; }
if ((index < 0 && shapes_ >= kMaxShapes) || next_ + need > kShapeWords) {
// Dead bodies recycled their body slots but permanently consumed shape
// slots and mask storage. Reclaim both, retaining every live/pending body.
std::vector<uint8_t> used(shapes_);
for (int k = 0; k < *count_; ++k) if (!(bodies_[k].i[2] & 2)) used[bodies_[k].i[0]] = 1;
std::vector<uint32_t> packed;
const size_t base = (size_t)kMaxShapes * kShapeHeaderWords;
for (int k = 0; k < shapes_; ++k) if (used[k]) {
uint32_t* h = words_ + (size_t)k * kShapeHeaderWords;
const size_t pixels = (size_t)h[0] * h[1], samples = (size_t)h[4] * 2;
const size_t mask = packed.size(); packed.insert(packed.end(), words_ + h[2], words_ + h[2] + pixels);
const size_t points = packed.size(); packed.insert(packed.end(), words_ + h[3], words_ + h[3] + samples);
h[2] = (uint32_t)(base + mask); h[3] = (uint32_t)(base + points);
} else std::memset(words_ + (size_t)k * kShapeHeaderWords, 0, kShapeHeaderWords * 4);
std::copy(packed.begin(), packed.end(), words_ + base);
next_ = base + packed.size(); shape_live_ = std::move(used); ++shape_epoch_;
index = -1;
for (int k = 0; k < shapes_; ++k) if (!shape_live_[k]) { index = k; break; }
}
if ((index < 0 && shapes_ >= kMaxShapes) || next_ + need > kShapeWords) return -1;
if (index < 0) { index = shapes_++; shape_live_.push_back(0); info_.push_back({}); }
shape_live_[index] = 1;
uint32_t* h = words_ + (size_t)index * kShapeHeaderWords;
const size_t maskOff = next_, sampOff = next_ + (size_t)s.w * s.h;
for (size_t k = 0; k < (size_t)s.w * s.h; ++k) words_[maskOff + k] = s.mask[k];
for (size_t k = 0; k < s.samples.size(); ++k) { words_[sampOff + 2 * k] = bits((float)s.samples[k].first); words_[sampOff + 2 * k + 1] = bits((float)s.samples[k].second); }
h[0] = (uint32_t)s.w; h[1] = (uint32_t)s.h; h[2] = (uint32_t)maskOff; h[3] = (uint32_t)sampOff; h[4] = (uint32_t)s.samples.size();
h[5] = bits((float)s.com_x); h[6] = bits((float)s.com_y); h[7] = bits((float)s.mass); h[8] = bits((float)s.inertia); h[9] = bits((float)s.radius);
next_ += need;
info_.push_back({(float)s.radius, (float)s.mass, (float)s.com_x, (float)s.com_y});
if (phys_) phys_->add_shape(s);
return shapes_++;
info_[index] = {(float)s.radius, (float)s.mass, (float)s.com_x, (float)s.com_y};
if (phys_) phys_->add_shape(s, index);
return index;
}
int GpuBodies::add_body(int shape, int slot, double ax, double ay, double ox, double oy) {
@@ -77,6 +101,27 @@ bool GpuBodies::add_joint(int a, int b, double px, double py, double break_force
return true;
}
bool GpuBodies::has_body_capacity() const {
if (*count_ < kMaxBodies) return true;
for (int k = 0; k < *count_; ++k) if (bodies_[k].i[2] & 2) return true;
return false;
}
bool GpuBodies::has_shape_capacity(int width, int height) const {
// make_shape keeps at most 127 outline samples. Count storage after the
// same live-shape compaction as add_shape, without modifying the buffers.
const size_t need = (size_t)width * height + 254;
if (shapes_ < kMaxShapes && next_ + need <= kShapeWords) return true;
std::vector<uint8_t> used(shapes_);
for (int k = 0; k < *count_; ++k) if (!(bodies_[k].i[2] & 2)) used[bodies_[k].i[0]] = 1;
int live = 0; size_t words = (size_t)kMaxShapes * kShapeHeaderWords;
for (int k = 0; k < shapes_; ++k) if (used[k]) {
const uint32_t* h = words_ + (size_t)k * kShapeHeaderWords;
words += (size_t)h[0] * h[1] + (size_t)h[4] * 2; ++live;
}
return live < kMaxShapes && words + need <= kShapeWords;
}
void GpuBodies::remove_slot(int slot) {
for (int k = 0; k < *count_; ++k) if (bodies_[k].i[1] == slot) { bodies_[k].i[2] |= 2; if (phys_) phys_->remove_body(k); }
}
@@ -171,15 +216,33 @@ void GpuBodies::set_surface(int body, double friction, double restitution) {
}
void GpuBodies::set_props(int body, double ld, double ad, bool sleep, bool fixed, bool bullet, bool stat) {
if (bullet) bodies_[body].i[2] |= kContinuousCollision; else bodies_[body].i[2] &= ~kContinuousCollision;
bodies_[body].d[1] = (float)ld; bodies_[body].d[2] = (float)ad;
if (phys_) phys_->set_props(body, ld, ad, sleep, fixed, bullet, stat);
}
void GpuBodies::set_bullet(int body, bool on) {
if (on) bodies_[body].i[2] |= kContinuousCollision; else bodies_[body].i[2] &= ~kContinuousCollision;
if (phys_) phys_->set_bullet(body, on);
}
void GpuBodies::set_density(int body, double density) {
bodies_[body].q[2] = (float)density;
if (phys_) phys_->set_density(body, density);
}
void GpuBodies::inherit_material(int body, const GpuBody& source) {
set_surface(body, source.v[3], source.v[2]);
set_density(body, source.q[2]);
set_group(body, source.m[0], source.m[1] > .5f);
bodies_[body].d[1] = source.d[1]; bodies_[body].d[2] = source.d[2];
bodies_[body].i[2] = (bodies_[body].i[2] & ~(512 | 1024 | 65536)) | (source.i[2] & (512 | 1024 | 65536));
if (phys_) {
phys_->set_go_through_sand(body, (source.i[2] & 512) != 0);
phys_->set_damping(body, source.d[1], source.d[2], true);
}
}
void GpuBodies::sync_from_physics() {
if (!phys_) return;
for (int k = 0; k < *count_; ++k) {
+9
View File
@@ -29,6 +29,7 @@ static_assert(sizeof(GpuBody) == 224, "GpuBody layout");
class GpuBodies {
public:
static constexpr int kMaxBodies = 1024, kMaxShapes = 512, kShapeHeaderWords = 16;
static constexpr int kContinuousCollision = 2048; // 1024 is reserved for character pass-through
static constexpr size_t kBodyBytes = 16 + sizeof(GpuBody) * kMaxBodies;
static constexpr size_t kShapeWords = 1u << 20;
@@ -41,13 +42,18 @@ public:
int add_shape(const RigidShape& s); // -1 when the shape table is full
int add_body(int shape, int slot, double anchor_x, double anchor_y, double ox, double oy); // body index or -1
int count() const { return *count_; }
bool has_body_capacity() const;
bool has_shape_capacity(int width, int height) const; // conservative preflight before extracting terrain
void remove_slot(int slot);
void set_group(int body, double group, bool passable);
void set_motion(int body, double vx, double vy, double omega);
void set_bullet(int body, bool on);
void inherit_material(int body, const GpuBody& source); // fracture: preserve density and collision response
void set_fragile(int body, double impact_speed) { bodies_[body].n[0] = (float)impact_speed; } // damaged by impacts faster than this (cells per frame)
// hangs on the wall at that point; break_force is the PhysicsJoint2Component multiplier (0 = only the loss of the surface lets it fall)
void set_nail(int body, double x, double y, double radius, double break_force = 0.0);
void set_surface_nail(int body, double x, double y, double ray_x, double ray_y, double break_force);
void set_material_reactions(int body, bool enabled) { if (enabled) bodies_[body].i[2] &= ~65536; else bodies_[body].i[2] |= 65536; }
void set_char_pass(int body) { bodies_[body].i[2] |= 1024; } // flag 1024: the material is no platform (platform_type not 1 / 2): the player and the creatures pass (rigid_raster.comp marks its cells)
void set_go_through_sand(int body); // flag 512: the body sinks through powders (meets the static terrain only, pushes no grains) // hangs on the wall at that point; break_force is the PhysicsJoint2Component multiplier (0 = only the loss of the surface lets it fall)
void wake(double x, double y, double radius);
@@ -64,6 +70,7 @@ public:
void set_burning(int body, double frames) { bodies_[body].m[2] = (float)frames; }
const uint32_t* shape_words() const { return words_; } // the mapped shape table (headers of kShapeHeaderWords words: w, h, mask offset, ..., pixel count at 7)
size_t shape_words_used() const { return next_; } // words of the shape table in use (header area + data), for incremental uploads
uint64_t shape_epoch() const { return shape_epoch_; } // cache invalidation / full GPU upload after compaction
private:
phys::PhysSystem* phys_ = nullptr;
@@ -72,6 +79,8 @@ private:
uint32_t* words_ = nullptr;
size_t next_ = 0;
int shapes_ = 0;
uint64_t shape_epoch_ = 0;
std::vector<uint8_t> shape_live_;
struct Info { float radius, mass, com_x, com_y; };
std::vector<Info> info_;
};
+37 -8
View File
@@ -18,6 +18,7 @@ std::vector<int> matching(const MaterialDb& db, const std::string& pat) {
if (std::find(m.tags.begin(), m.tags.end(), tag) != m.tags.end()) r.push_back(m.index);
} else if (const Material* m = db.find(pat)) {
r.push_back(m->index);
if (db.loose_twin(m->index) >= 0) r.push_back(db.loose_twin(m->index));
}
return r;
}
@@ -29,25 +30,50 @@ int output_of(const MaterialDb& db, const std::string& pat, int input) {
if (pat.front() == '[') {
const size_t close = pat.find(']');
if (close == std::string::npos || close + 1 >= pat.size()) return -1;
const Material* m = db.find(db.materials()[input].name + pat.substr(close + 1));
return m ? m->index : -1;
const Material* m = db.find(db.materials()[db.canonical(input)].name + pat.substr(close + 1));
return m ? (m->index == db.canonical(input) ? input : m->index) : -1;
}
const Material* m = db.find(pat);
return m ? m->index : -1;
return m ? (m->index == db.canonical(input) ? input : m->index) : -1;
}
} // namespace
std::vector<uint32_t> build_thermal_conversions(const MaterialDb& db) {
std::vector<uint32_t> result(db.materials().size() * 2u, 0u);
for (const Material& m : db.materials()) {
if (db.canonical(m.index) != m.index) continue;
auto target = [&](const char* key) { const auto it = m.attrs.find(key); const auto* t = it == m.attrs.end() ? nullptr : db.find(it->second); return t && t->index > 0 ? uint32_t(t->index + 1) : 0u; };
const uint32_t cold = target("cold_freezes_to_material");
result[2u * m.index] = cold | (uint32_t(std::clamp(attr_int(m, "cold_freezes_chance_rev", 100), 0, 32767)) << 16);
if (cold != 0u && attr_int(m, "cold_freezes_to_dont_do_reverse_reaction", 0) == 0)
result[2u * (cold - 1u) + 1u] = (result[2u * (cold - 1u) + 1u] & 0xFFFF0000u) | uint32_t(m.index + 1);
// The target may be finalized later; its explicit (including empty) warm
// target then overwrites the reverse assignment, just as in 00707990.
result[2u * m.index + 1u] = target("warmth_melts_to_material") | (uint32_t(std::clamp(attr_int(m, "warmth_melts_chance_rev", 100), 0, 32767)) << 16);
}
for (const Material& m : db.materials()) {
if (db.canonical(m.index) == m.index) continue;
result[2u * m.index] = result[2u * db.canonical(m.index)];
result[2u * m.index + 1u] = result[2u * db.canonical(m.index) + 1u];
}
return result;
}
ReactionTables build_reaction_tables(const MaterialDb& db) {
ReactionTables t;
t.n = (int)db.materials().size();
struct Entry { uint32_t outs, prob, third, blob, entity; }; // prob: probability in 1/65536 | direction code << 16 (0 any, 1 top, 2 bottom, 3 left, 4 right)
struct Entry { uint32_t outs, prob, third, blob, entity; }; // prob: XML percent * 100 | direction code << 16 (0 any, 1 top, 2 bottom, 3 left, 4 right)
const uint32_t stride = uint32_t(t.n) * (t.n + 1u);
std::map<uint32_t, std::vector<Entry>> lists; // key a * n + b
for (const Reaction& r : db.reactions()) {
auto life = r.attrs.find("req_lifetime");
const auto fast = r.attrs.find("fast_reaction");
const uint32_t base = fast != r.attrs.end() && std::atoi(fast->second.c_str()) != 0 ? stride : 0;
const std::vector<int> A = matching(db, r.in[0]), B = matching(db, r.in[1]);
std::vector<int> C = r.in[2].empty() ? std::vector<int>{-1} : matching(db, r.in[2]); // third input (the cell next to the first one), air included
if (r.in[2] == "air") C = {0};
C.erase(std::remove_if(C.begin(), C.end(), [&](int m) { return m >= 0 && db.canonical(m) != m; }), C.end());
if (A.empty() || B.empty() || C.empty()) continue;
if (!r.in[2].empty()) ++t.three_input;
uint32_t prob = (uint32_t)std::min(65535.0, std::max(0.0, std::floor(r.probability * 100.0 + 0.5))); // the native threshold: R = next15 % 10001 is accepted when R <= threshold (0070eaf0), the probability is in percent
@@ -75,15 +101,18 @@ ReactionTables build_reaction_tables(const MaterialDb& db) {
const int o1 = output_of(db, r.out[0], a), o2 = output_of(db, r.out[1], b), o3 = c < 0 ? -1 : output_of(db, r.out[2], c);
const uint32_t outA = o1 < 0 ? 0xFFFFu : (uint32_t)o1, outB = o2 < 0 ? 0xFFFFu : (uint32_t)o2, outC = o3 < 0 ? 0xFFFFu : (uint32_t)o3;
const uint32_t third = (c < 0 ? 0xFFFFu : (uint32_t)c) | outC << 16;
lists[(uint32_t)a * t.n + b].push_back({outA | outB << 16, prob | dir << 16, third, blob, entityIdx});
if (a != b) lists[(uint32_t)b * t.n + a].push_back({outB | outA << 16, prob | rdir << 16, third, rblob, entityIdx | 0x80000000u}); // bit 31 of the entity word: the record is the mirrored one (the first input is the second material)
lists[base + (uint32_t)a * t.n + b].push_back({outA | outB << 16, prob | dir << 16, third, blob, entityIdx});
if (a != b) lists[base + (uint32_t)b * t.n + a].push_back({outB | outA << 16, prob | rdir << 16, third, rblob, entityIdx | 0x80000000u}); // bit 31 of the entity word: the record is the mirrored one (the first input is the second material)
}
}
t.head.assign((size_t)t.n * t.n + t.n, 0u); // then per material the largest probability of a reaction it starts (CellData + 0x22c)
t.head.assign(2u * stride + t.n, 0u);
for (int m = 0; m < t.n; ++m) t.head[2u * stride + m] = uint32_t(db.canonical(m)); // then per material the largest probability of a reaction it starts (CellData + 0x22c)
for (auto& [key, v] : lists) {
if (v.size() > 255) v.resize(255);
t.head[key] = (uint32_t)(t.rec.size() / 5) << 8 | (uint32_t)v.size();
for (const Entry& e : v) t.head[(size_t)t.n * t.n + key / t.n] = std::max(t.head[(size_t)t.n * t.n + key / t.n], e.prob & 0xFFFFu);
const uint32_t base = key >= stride ? stride : 0;
const size_t threshold = base + (size_t)t.n * t.n + (key - base) / t.n;
for (const Entry& e : v) t.head[threshold] = std::max(t.head[threshold], e.prob & 0xFFFFu);
for (const Entry& e : v) { t.rec.push_back(e.outs); t.rec.push_back(e.prob); t.rec.push_back(e.third); t.rec.push_back(e.blob); t.rec.push_back(e.entity); }
t.entries += (int)v.size();
}
+6 -2
View File
@@ -15,10 +15,12 @@ namespace ng {
struct ReactionTables {
int n = 0; // number of materials
// head[a * n + b] = (offset << 8) | count into `rec` (count <= 255)
// Two blocks of n*n pair heads followed by n thresholds: combined, then fast.
// Finally n canonical material IDs (loose twins retain native CellData identity).
// head[base + a * n + b] = (offset << 8) | count into `rec` (count <= 255)
std::vector<uint32_t> head;
// five words per entry (the fourth: blob radius of side a | radius of side b << 8 | restrict a << 16 | restrict b << 17 | destroy_horizontally_lonely_pixels << 19 | req_lifetime << 20;
// the fifth: index + 1 into `entities` of the entity the reaction spawns at the first cell, 0 = none): (out_a | out_b << 16) with 0xFFFF = unchanged; the probability per attempt in 1/65536 | direction << 16;
// the fifth: index + 1 into `entities` of the entity the reaction spawns at the first cell, 0 = none): (out_a | out_b << 16) with 0xFFFF = unchanged; the native probability threshold (XML percent * 100) | direction << 16;
// the third input (0xFFFF = none) | out_c << 16 (0xFFFF = unchanged) | bit 31 = the third cell is looked up around the cell of the second slot
std::vector<uint32_t> rec;
std::vector<std::string> entities; // paths of the entities of the reactions (`entity=`)
@@ -26,5 +28,7 @@ struct ReactionTables {
};
ReactionTables build_reaction_tables(const MaterialDb& db);
// Material finalization order and reverse cold conversion (00707990).
std::vector<uint32_t> build_thermal_conversions(const MaterialDb& db);
} // namespace ng
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <cmath>
#include <map>
#include <string>
namespace ng {
// data/scripts/biomes/temple_shared.lua: temple_pos_to_id and
// temple_should_we_spawn_checkers. Deferred spawns must use the current run state.
inline bool temple_breach_check_retired(double x, double y, const std::map<std::string, std::string>& globals) {
const auto room = std::to_string((int)std::floor((x + 32 * 512) / (64 * 512))) + "_" +
std::to_string((int)std::floor(y / 512));
for (const char* prefix : {"TEMPLE_COLLAPSED_", "TEMPLE_LEAKED_"}) {
const auto value = globals.find(prefix + room);
if (value != globals.end() && value->second == "1") return true;
}
return false;
}
} // namespace ng
+23
View File
@@ -0,0 +1,23 @@
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+5
View File
@@ -0,0 +1,5 @@
Clipper 5.1.6, Angus Johnson, 18 May 2013.
Copied from the existing local `luapower-all-master/csrc/clipper` checkout.
The source and header are unchanged; see `LICENSE` for the Boost Software
License. Used for the native terrain's negative powder-contour offset.
+3680
View File
File diff suppressed because it is too large. Load diff
+349
View File
@@ -0,0 +1,349 @@
/*******************************************************************************
* *
* Author : Angus Johnson *
* Version : 5.1.6 *
* Date : 18 May 2013 *
* Website : http://www.angusj.com *
* Copyright : Angus Johnson 2010-2013 *
* *
* License: *
* Use, modification & distribution is subject to Boost Software License Ver 1. *
* http://www.boost.org/LICENSE_1_0.txt *
* *
* Attributions: *
* The code in this library is an extension of Bala Vatti's clipping algorithm: *
* "A generic solution to polygon clipping" *
* Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. *
* http://portal.acm.org/citation.cfm?id=129906 *
* *
* Computer graphics and geometric modeling: implementation and algorithms *
* By Max K. Agoston *
* Springer; 1 edition (January 4, 2005) *
* http://books.google.com/books?q=vatti+clipping+agoston *
* *
* See also: *
* "Polygon Offsetting by Computing Winding Numbers" *
* Paper no. DETC2005-85513 pp. 565-575 *
* ASME 2005 International Design Engineering Technical Conferences *
* and Computers and Information in Engineering Conference (IDETC/CIE2005) *
* September 24-28, 2005 , Long Beach, California, USA *
* http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf *
* *
*******************************************************************************/
#ifndef clipper_hpp
#define clipper_hpp
#include <vector>
#include <stdexcept>
#include <cstring>
#include <cstdlib>
#include <ostream>
namespace ClipperLib {
enum ClipType { ctIntersection, ctUnion, ctDifference, ctXor };
enum PolyType { ptSubject, ptClip };
//By far the most widely used winding rules for polygon filling are
//EvenOdd & NonZero (GDI, GDI+, XLib, OpenGL, Cairo, AGG, Quartz, SVG, Gr32)
//Others rules include Positive, Negative and ABS_GTR_EQ_TWO (only in OpenGL)
//see http://glprogramming.com/red/chapter11.html
enum PolyFillType { pftEvenOdd, pftNonZero, pftPositive, pftNegative };
typedef signed long long long64;
typedef unsigned long long ulong64;
struct IntPoint {
public:
long64 X;
long64 Y;
IntPoint(long64 x = 0, long64 y = 0): X(x), Y(y) {};
friend std::ostream& operator <<(std::ostream &s, IntPoint &p);
};
typedef std::vector< IntPoint > Polygon;
typedef std::vector< Polygon > Polygons;
std::ostream& operator <<(std::ostream &s, Polygon &p);
std::ostream& operator <<(std::ostream &s, Polygons &p);
class PolyNode;
typedef std::vector< PolyNode* > PolyNodes;
class PolyNode
{
public:
PolyNode();
Polygon Contour;
PolyNodes Childs;
PolyNode* Parent;
PolyNode* GetNext() const;
bool IsHole() const;
int ChildCount() const;
private:
PolyNode* GetNextSiblingUp() const;
unsigned Index; //node index in Parent.Childs
void AddChild(PolyNode& child);
friend class Clipper; //to access Index
};
class PolyTree: public PolyNode
{
public:
~PolyTree(){Clear();};
PolyNode* GetFirst() const;
void Clear();
int Total() const;
private:
PolyNodes AllNodes;
friend class Clipper; //to access AllNodes
};
enum JoinType { jtSquare, jtRound, jtMiter };
enum EndType { etClosed, etButt, etSquare, etRound};
bool Orientation(const Polygon &poly);
double Area(const Polygon &poly);
void OffsetPolygons(const Polygons &in_polys, Polygons &out_polys,
double delta, JoinType jointype = jtSquare, double limit = 0, bool autoFix = true);
void OffsetPolyLines(const Polygons &in_lines, Polygons &out_lines,
double delta, JoinType jointype = jtSquare, EndType endtype = etSquare, double limit = 0, bool autoFix = true);
void SimplifyPolygon(const Polygon &in_poly, Polygons &out_polys, PolyFillType fillType = pftEvenOdd);
void SimplifyPolygons(const Polygons &in_polys, Polygons &out_polys, PolyFillType fillType = pftEvenOdd);
void SimplifyPolygons(Polygons &polys, PolyFillType fillType = pftEvenOdd);
void CleanPolygon(const Polygon& in_poly, Polygon& out_poly, double distance = 1.415);
void CleanPolygons(const Polygons& in_polys, Polygons& out_polys, double distance = 1.415);
void PolyTreeToPolygons(const PolyTree& polytree, Polygons& polygons);
void ReversePolygon(Polygon& p);
void ReversePolygons(Polygons& p);
//used internally ...
enum EdgeSide { esLeft = 1, esRight = 2};
enum IntersectProtects { ipNone = 0, ipLeft = 1, ipRight = 2, ipBoth = 3 };
//inline IntersectProtects operator|(IntersectProtects a, IntersectProtects b)
//{return static_cast<IntersectProtects>(static_cast<int>(a) | static_cast<int>(b));}
struct TEdge {
long64 xbot;
long64 ybot;
long64 xcurr;
long64 ycurr;
long64 xtop;
long64 ytop;
double dx;
long64 deltaX;
long64 deltaY;
PolyType polyType;
EdgeSide side;
int windDelta; //1 or -1 depending on winding direction
int windCnt;
int windCnt2; //winding count of the opposite polytype
int outIdx;
TEdge *next;
TEdge *prev;
TEdge *nextInLML;
TEdge *nextInAEL;
TEdge *prevInAEL;
TEdge *nextInSEL;
TEdge *prevInSEL;
};
struct IntersectNode {
TEdge *edge1;
TEdge *edge2;
IntPoint pt;
IntersectNode *next;
};
struct LocalMinima {
long64 Y;
TEdge *leftBound;
TEdge *rightBound;
LocalMinima *next;
};
struct Scanbeam {
long64 Y;
Scanbeam *next;
};
struct OutPt; //forward declaration
struct OutRec {
int idx;
bool isHole;
OutRec *FirstLeft; //see comments in clipper.pas
PolyNode *polyNode;
OutPt *pts;
OutPt *bottomPt;
};
struct OutPt {
int idx;
IntPoint pt;
OutPt *next;
OutPt *prev;
};
struct JoinRec {
IntPoint pt1a;
IntPoint pt1b;
int poly1Idx;
IntPoint pt2a;
IntPoint pt2b;
int poly2Idx;
};
struct HorzJoinRec {
TEdge *edge;
int savedIdx;
};
struct IntRect { long64 left; long64 top; long64 right; long64 bottom; };
typedef std::vector < OutRec* > PolyOutList;
typedef std::vector < TEdge* > EdgeList;
typedef std::vector < JoinRec* > JoinList;
typedef std::vector < HorzJoinRec* > HorzJoinList;
//ClipperBase is the ancestor to the Clipper class. It should not be
//instantiated directly. This class simply abstracts the conversion of sets of
//polygon coordinates into edge objects that are stored in a LocalMinima list.
class ClipperBase
{
public:
ClipperBase();
virtual ~ClipperBase();
bool AddPolygon(const Polygon &pg, PolyType polyType);
bool AddPolygons( const Polygons &ppg, PolyType polyType);
virtual void Clear();
IntRect GetBounds();
protected:
void DisposeLocalMinimaList();
TEdge* AddBoundsToLML(TEdge *e);
void PopLocalMinima();
virtual void Reset();
void InsertLocalMinima(LocalMinima *newLm);
LocalMinima *m_CurrentLM;
LocalMinima *m_MinimaList;
bool m_UseFullRange;
EdgeList m_edges;
};
class Clipper : public virtual ClipperBase
{
public:
Clipper();
~Clipper();
bool Execute(ClipType clipType,
Polygons &solution,
PolyFillType subjFillType = pftEvenOdd,
PolyFillType clipFillType = pftEvenOdd);
bool Execute(ClipType clipType,
PolyTree &polytree,
PolyFillType subjFillType = pftEvenOdd,
PolyFillType clipFillType = pftEvenOdd);
void Clear();
bool ReverseSolution() {return m_ReverseOutput;};
void ReverseSolution(bool value) {m_ReverseOutput = value;};
bool ForceSimple() {return m_ForceSimple;};
void ForceSimple(bool value) {m_ForceSimple = value;};
protected:
void Reset();
virtual bool ExecuteInternal();
private:
PolyOutList m_PolyOuts;
JoinList m_Joins;
HorzJoinList m_HorizJoins;
ClipType m_ClipType;
Scanbeam *m_Scanbeam;
TEdge *m_ActiveEdges;
TEdge *m_SortedEdges;
IntersectNode *m_IntersectNodes;
bool m_ExecuteLocked;
PolyFillType m_ClipFillType;
PolyFillType m_SubjFillType;
bool m_ReverseOutput;
bool m_UsingPolyTree;
bool m_ForceSimple;
void DisposeScanbeamList();
void SetWindingCount(TEdge& edge);
bool IsEvenOddFillType(const TEdge& edge) const;
bool IsEvenOddAltFillType(const TEdge& edge) const;
void InsertScanbeam(const long64 Y);
long64 PopScanbeam();
void InsertLocalMinimaIntoAEL(const long64 botY);
void InsertEdgeIntoAEL(TEdge *edge);
void AddEdgeToSEL(TEdge *edge);
void CopyAELToSEL();
void DeleteFromSEL(TEdge *e);
void DeleteFromAEL(TEdge *e);
void UpdateEdgeIntoAEL(TEdge *&e);
void SwapPositionsInSEL(TEdge *edge1, TEdge *edge2);
bool IsContributing(const TEdge& edge) const;
bool IsTopHorz(const long64 XPos);
void SwapPositionsInAEL(TEdge *edge1, TEdge *edge2);
void DoMaxima(TEdge *e, long64 topY);
void ProcessHorizontals();
void ProcessHorizontal(TEdge *horzEdge);
void AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &pt);
void AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &pt);
OutRec* GetOutRec(int idx);
void AppendPolygon(TEdge *e1, TEdge *e2);
void IntersectEdges(TEdge *e1, TEdge *e2,
const IntPoint &pt, const IntersectProtects protects);
OutRec* CreateOutRec();
void AddOutPt(TEdge *e, const IntPoint &pt);
void DisposeAllPolyPts();
void DisposeOutRec(PolyOutList::size_type index);
bool ProcessIntersections(const long64 botY, const long64 topY);
void InsertIntersectNode(TEdge *e1, TEdge *e2, const IntPoint &pt);
void BuildIntersectList(const long64 botY, const long64 topY);
void ProcessIntersectList();
void ProcessEdgesAtTopOfScanbeam(const long64 topY);
void BuildResult(Polygons& polys);
void BuildResult2(PolyTree& polytree);
void SetHoleState(TEdge *e, OutRec *outrec);
void DisposeIntersectNodes();
bool FixupIntersectionOrder();
void FixupOutPolygon(OutRec &outrec);
bool IsHole(TEdge *e);
void FixHoleLinkage(OutRec &outrec);
void AddJoin(TEdge *e1, TEdge *e2, int e1OutIdx = -1, int e2OutIdx = -1);
void ClearJoins();
void AddHorzJoin(TEdge *e, int idx);
void ClearHorzJoins();
bool JoinPoints(const JoinRec *j, OutPt *&p1, OutPt *&p2);
void FixupJoinRecs(JoinRec *j, OutPt *pt, unsigned startIdx);
void JoinCommonEdges();
void DoSimplePolygons();
void FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec);
void FixupFirstLefts2(OutRec* OldOutRec, OutRec* NewOutRec);
};
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
class clipperException : public std::exception
{
public:
clipperException(const char* description): m_descr(description) {}
virtual ~clipperException() throw() {}
virtual const char* what() const throw() {return m_descr.c_str();}
private:
std::string m_descr;
};
//------------------------------------------------------------------------------
} //ClipperLib namespace
#endif //clipper_hpp