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:
1 parent
a34f42d53e
commit
00a9c1c26c
48 files changed
+5771
-407
No files matched your search
+2
-1
@@ -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)
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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"
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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"};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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; };
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Vendored
+23
@@ -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.
|
||||
Vendored
+5
@@ -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.
|
||||
Vendored
+3680
File diff suppressed because it is too large.
Load diff
Vendored
+349
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user