From 00a9c1c26c4fef3f7507c35845c38f8cdb29f7f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl?= Date: Wed, 21 Oct 2026 20:46:13 +0200 Subject: [PATCH] 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 --- CMakeLists.txt | 3 +- shaders/apply.comp | 2 + shaders/burn.comp | 14 - shaders/common.glsl | 20 + shaders/entities.comp | 78 +- shaders/ents_prev.comp | 2 +- shaders/fx_common.glsl | 31 +- shaders/fx_host.glsl | 2 + shaders/fx_particles.comp | 15 +- shaders/fx_state.glsl | 16 +- shaders/hit_liquid.glsl | 31 + shaders/hit_liquid_channels.glsl | 15 + shaders/intent.comp | 10 +- shaders/particle_alloc.glsl | 16 + shaders/proj_step.comp | 33 +- shaders/react.comp | 202 +- shaders/rigid_burn.comp | 72 + shaders/sprites.comp | 22 +- shaders/terrain.comp | 3 + src/app/hud.h | 21 + src/app/main.cpp | 675 ++++-- src/app/rigid_debug.h | 43 + src/core/materials.cpp | 4 + src/core/materials.h | 4 + src/entity/entity.cpp | 13 +- src/entity/hit_liquid.h | 54 + src/entity/runtime.cpp | 54 +- src/entity/runtime.h | 6 +- src/gun/gpu_fx.h | 2 +- src/gun/projectiles.cpp | 1 + src/gun/projectiles.h | 1 + src/lua/lua_host.cpp | 8 +- src/phys/body_cells.h | 59 + src/phys/phys_system.cpp | 319 ++- src/phys/phys_system.h | 60 +- src/phys/terrain_outline.cpp | 46 + src/phys/terrain_outline.h | 4 + src/phys/terrain_world.cpp | 54 +- src/phys/terrain_world.h | 7 +- src/physics/gpu_bodies.cpp | 73 +- src/physics/gpu_bodies.h | 9 + src/world/reactions.cpp | 45 +- src/world/reactions.h | 8 +- src/world/temple_state.h | 20 + third_party/clipper/LICENSE | 23 + third_party/clipper/README.md | 5 + third_party/clipper/clipper.cpp | 3680 ++++++++++++++++++++++++++++++ third_party/clipper/clipper.hpp | 349 +++ 48 files changed, 5799 insertions(+), 435 deletions(-) create mode 100644 shaders/hit_liquid.glsl create mode 100644 shaders/hit_liquid_channels.glsl create mode 100644 shaders/particle_alloc.glsl create mode 100644 src/app/rigid_debug.h create mode 100644 src/entity/hit_liquid.h create mode 100644 src/phys/body_cells.h create mode 100644 src/world/temple_state.h create mode 100644 third_party/clipper/LICENSE create mode 100644 third_party/clipper/README.md create mode 100644 third_party/clipper/clipper.cpp create mode 100644 third_party/clipper/clipper.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 433803d..9db43c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/shaders/apply.comp b/shaders/apply.comp index 3d22a73..f9a1539 100644 --- a/shaders/apply.comp +++ b/shaders/apply.comp @@ -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; diff --git a/shaders/burn.comp b/shaders/burn.comp index 38de02f..4a7061d 100644 --- a/shaders/burn.comp +++ b/shaders/burn.comp @@ -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; diff --git a/shaders/common.glsl b/shaders/common.glsl index 8649ea4..22ad8b4 100644 --- a/shaders/common.glsl +++ b/shaders/common.glsl @@ -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); } diff --git a/shaders/entities.comp b/shaders/entities.comp index 5caa76d..4b2e507 100644 --- a/shaders/entities.comp +++ b/shaders/entities.comp @@ -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); diff --git a/shaders/ents_prev.comp b/shaders/ents_prev.comp index 5bc8908..43320fc 100644 --- a/shaders/ents_prev.comp +++ b/shaders/ents_prev.comp @@ -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 } diff --git a/shaders/fx_common.glsl b/shaders/fx_common.glsl index 0ee6397..1560bfb 100644 --- a/shaders/fx_common.glsl +++ b/shaders/fx_common.glsl @@ -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" diff --git a/shaders/fx_host.glsl b/shaders/fx_host.glsl index c8c9ef7..cc9fe07 100644 --- a/shaders/fx_host.glsl +++ b/shaders/fx_host.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 }; diff --git a/shaders/fx_particles.comp b/shaders/fx_particles.comp index c6d5248..17c7d79 100644 --- a/shaders/fx_particles.comp +++ b/shaders/fx_particles.comp @@ -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); diff --git a/shaders/fx_state.glsl b/shaders/fx_state.glsl index bf47361..b1dcef9 100644 --- a/shaders/fx_state.glsl +++ b/shaders/fx_state.glsl @@ -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; diff --git a/shaders/hit_liquid.glsl b/shaders/hit_liquid.glsl new file mode 100644 index 0000000..9d9e2b4 --- /dev/null +++ b/shaders/hit_liquid.glsl @@ -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); + } +} diff --git a/shaders/hit_liquid_channels.glsl b/shaders/hit_liquid_channels.glsl new file mode 100644 index 0000000..0344f86 --- /dev/null +++ b/shaders/hit_liquid_channels.glsl @@ -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)); + } +} diff --git a/shaders/intent.comp b/shaders/intent.comp index 1a82b4d..914104d 100644 --- a/shaders/intent.comp +++ b/shaders/intent.comp @@ -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); diff --git a/shaders/particle_alloc.glsl b/shaders/particle_alloc.glsl new file mode 100644 index 0000000..ea1648e --- /dev/null +++ b/shaders/particle_alloc.glsl @@ -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); +} diff --git a/shaders/proj_step.comp b/shaders/proj_step.comp index 0351812..32a178f 100644 --- a/shaders/proj_step.comp +++ b/shaders/proj_step.comp @@ -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 diff --git a/shaders/react.comp b/shaders/react.comp index d1d9ffc..284c397 100644 --- a/shaders/react.comp +++ b/shaders/react.comp @@ -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); - 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; +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; + 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); +} diff --git a/shaders/rigid_burn.comp b/shaders/rigid_burn.comp index 4a8c3b0..7f17b51 100644 --- a/shaders/rigid_burn.comp +++ b/shaders/rigid_burn.comp @@ -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; diff --git a/shaders/sprites.comp b/shaders/sprites.comp index e2dd7a5..cf18d92 100644 --- a/shaders/sprites.comp +++ b/shaders/sprites.comp @@ -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) diff --git a/shaders/terrain.comp b/shaders/terrain.comp index d6747ed..5a06979 100644 --- a/shaders/terrain.comp +++ b/shaders/terrain.comp @@ -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]; diff --git a/src/app/hud.h b/src/app/hud.h index 426139e..c5003ea 100644 --- a/src/app/hud.h +++ b/src/app/hud.h @@ -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; diff --git a/src/app/main.cpp b/src/app/main.cpp index 23b4573..5bd42a0 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -25,6 +25,7 @@ #include #include #include "entity/material_mix.h" +#include "entity/hit_liquid.h" #include #include #include @@ -37,11 +38,13 @@ #include "app/perf.h" #include "app/gif_editor.h" #include "app/hud.h" +#include "app/rigid_debug.h" #include "entity/runtime.h" #include "lua/gpu_lua.h" #include "lua/gpu_lua_profiles.h" #include "lua/mod_loader.h" #include "world/biomes.h" +#include "world/temple_state.h" #include #include "entity/electricity.h" #include "entity/herds.h" @@ -85,6 +88,7 @@ void fly_waypoint(FlyPath& fp, float px, float py, float gx, float gy, int frame #include "gun/projectiles.h" #include "physics/gpu_bodies.h" #include "phys/phys_system.h" +#include "phys/body_cells.h" #include "world/sky.h" #include "world/reactions.h" #include "world/area_setup.h" @@ -134,7 +138,7 @@ const ng::BiomeModifierSet& tileMods(const ng::BiomeMap& bm, double x, double y) return tx >= 0 && ty >= 0 && tx < bm.width() && ty < bm.height() ? tiles[(size_t)ty * bm.width() + tx] : none; } -constexpr uint32_t kBindings = 65; // (64: compiled GPU Lua tasks) (63: the chains of the VERLET projectiles, proj_step.comp) (61 / 62: the far blocks and their map, far.glsl) (60: the window picture without the HUD, for the replay recorder - the game records no UI) (59: the picture at one pixel per cell, render.comp world pass) (58: the half-size frame for the replay recorder, clip_half.comp) (57: the light of the entities per cell, light_cells.comp) cells, materials, pixels, ext, claims, intent, wang field, wang materials, noise tables, material params, previous cells, previous ext, pixel scenes, texture texels, texture table, background tile, scene visuals, scene backgrounds, store override, entity texels, entity instances, entity layer, entity states, entity control, projectile types, projectile pool, HUD overlay, explosion edits, reaction heads, reaction entries, rigid shapes, rigid bodies, rigid bodies (next), fx tables, fx sprites, fx host data, fx state, explosion events, lights, entity positions before the last tick, cells covered by rigid bodies, burn state of the pixels of the rigid bodies, birth frame of every cell, loaded biome areas, the sky, the lights of the entities, the glow of the materials, the glow (collected, blurred) x2, the explored map of the fog of war, the particle requests, the ages before a scroll, the cell probe (terrain.comp probe mode), the depth of the entity layer (sprites.comp), the cut-through-world rules (terrain.comp), the particle glow (summed per glow texel) +constexpr uint32_t kBindings = 68; // (64: compiled GPU Lua tasks) (63: the chains of the VERLET projectiles, proj_step.comp) (61 / 62: the far blocks and their map, far.glsl) (60: the window picture without the HUD, for the replay recorder - the game records no UI) (59: the picture at one pixel per cell, render.comp world pass) (58: the half-size frame for the replay recorder, clip_half.comp) (57: the light of the entities per cell, light_cells.comp) cells, materials, pixels, ext, claims, intent, wang field, wang materials, noise tables, material params, previous cells, previous ext, pixel scenes, texture texels, texture table, background tile, scene visuals, scene backgrounds, store override, entity texels, entity instances, entity layer, entity states, entity control, projectile types, projectile pool, HUD overlay, explosion edits, reaction heads, reaction entries, rigid shapes, rigid bodies, rigid bodies (next), fx tables, fx sprites, fx host data, fx state, explosion events, lights, entity positions before the last tick, cells covered by rigid bodies, burn state of the pixels of the rigid bodies, birth frame of every cell, loaded biome areas, the sky, the lights of the entities, the glow of the materials, the glow (collected, blurred) x2, the explored map of the fog of war, the particle requests, the ages before a scroll, the cell probe (terrain.comp probe mode), the depth of the entity layer (sprites.comp), the cut-through-world rules (terrain.comp), the particle glow (summed per glow texel) enum : uint32_t { F_STATIC = 1, F_LIQUID = 2, F_GAS = 4, F_SAND = 8, F_AIR = 16, F_FIRE = 32 }; @@ -255,6 +259,7 @@ struct Input { bool inv_toggle = false, drop_item = false; // Tab / I: the inventory, Q: drop the item in the hand int speed_step = 0; // numpad + / - : doubles / halves the simulation speed bool pause_toggle = false, debug_toggle = false, help_toggle = false, brush_toggle = false; // P, F3, F1, B + bool collider_toggle = false, rigid_dump = false; bool mouse_in = false; int cw = 0, ch = 0; // client size of the window (WM_SIZE) int move_x = 0, move_y = 0; // view movement requested by the arrow keys / WASD (world mode) @@ -292,6 +297,10 @@ LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) { if (w == VK_F10) { g_in.perf_dump = true; return 0; } break; case WM_KEYDOWN: + if ((GetKeyState(VK_F3) & 0x8000) && (w == 'C' || w == 'L')) { + if (!(l & (1LL << 30))) { if (w == 'C') g_in.collider_toggle = true; else g_in.rigid_dump = true; } + return 0; + } if (w == VK_F11) { g_in.gif_toggle = true; return 0; } if (g_gifOpen) { if (w == VK_F9) g_in.clip_dump = true; else g_in.gif_keys.push_back((int)w); return 0; } if (w == VK_ESCAPE) { if (g_modal) g_in.escape = true; else g_in.quit = true; } @@ -305,7 +314,7 @@ LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) { if (w == 'E') g_in.interact = true; if (w == VK_ADD || w == VK_OEM_PLUS) g_in.speed_step = 1; if (w == VK_SUBTRACT || w == VK_OEM_MINUS) g_in.speed_step = -1; - if (w == VK_F3) g_in.debug_toggle = true; + if (w == VK_F3 && !(l & (1LL << 30))) g_in.debug_toggle = true; if (w == VK_F1) g_in.help_toggle = true; if (w == 'B') g_in.brush_toggle = true; if (w == VK_TAB || w == 'I') g_in.inv_toggle = true; @@ -393,7 +402,7 @@ struct Gpu { VkPipeline pipeProjLua{}, pipeLightCells{}, pipeClipHalf{}, pipePartReq{}, pipeGlow{}, pipeFog{}, pipeSkyCols{}, pipeIntent{}, pipeApply{}, pipePaint{}, pipeRender{}, pipeTerrain{}, pipeSprites{}, pipeEntities{}, pipeProj{}, pipeEdits{}, pipeReact{}, pipeBurn{}, pipeRigidSolve{}, pipeRigidCommit{}, pipeRigidFrame{}, pipeRigidEvents{}, pipeProjSpawn{}, pipeProjStep{}, pipeFxParticles{}, pipeFxAnims{}, pipeEntsPrev{}, pipeFxHand{}, pipeDisplace{}, pipeVines{}, pipeRigidRaster{}, pipeExplodeRays{}, pipeRigidBurn{}; Buffer fprojRead; // the GPU projectiles read back every frame (the twins of the host follow them) - Buffer luaRead, luaTasks, verletPts, farCells, farMap, farRead, bodyRead, ctrlR, clipRaw, litCells, clipHalf, worldCells, partGlow, cutRules, probe, partReq, glowTab, glowA, glowB, fog, fogVis, lit, sky, entDepth, cells, mats, pixels, ext, claims, intent, cellsOld, extOld, birthOld, scene, texData, texInfo, bgTile, sceneVis, sceneBg, ovr, strip, entTex, entInst, entLayer, ents, ctrl, projTypes, projPool, overlay, overlayStage, edits, reactHead, reactRec, rigidShapes, rigidShapesDev, rigidBodies, rigidHost, rigidNext, fxTables, fxTex, fxHost, fxState, fxXev, fxLights, entPrev, bodyGrid, burnPx, birth, areaTab, entRead, cellsRead, field, matplane, tables, mparams, staging, stagingFrame, readback, clipBuf[3]; + Buffer hitLiquid, luaRead, luaTasks, verletPts, farCells, farMap, farRead, bodyRead, terrainAge, terrainAgeOld, ctrlR, clipRaw, litCells, clipHalf, worldCells, partGlow, cutRules, probe, partReq, glowTab, glowA, glowB, fog, fogVis, lit, sky, entDepth, cells, mats, pixels, ext, claims, intent, cellsOld, extOld, birthOld, scene, texData, texInfo, bgTile, sceneVis, sceneBg, ovr, strip, entTex, entInst, entLayer, ents, ctrl, projTypes, projPool, overlay, overlayStage, edits, reactHead, reactRec, rigidShapes, rigidShapesDev, rigidBodies, rigidHost, rigidNext, fxTables, fxTex, fxHost, fxState, fxXev, fxLights, entPrev, bodyGrid, burnPx, birth, areaTab, entRead, cellsRead, field, matplane, tables, mparams, staging, stagingFrame, readback, clipBuf[3]; uint32_t find_mem(uint32_t bits, VkMemoryPropertyFlags want) const { for (uint32_t i = 0; i < memProps.memoryTypeCount; ++i) @@ -701,7 +710,7 @@ void init_vulkan(Gpu& g, HWND hwnd, const std::string& shaderDir, const std::vec } void bind_buffers(Gpu& g) { - const Buffer* bs[kBindings] = {&g.cells, &g.mats, &g.pixels, &g.ext, &g.claims, &g.intent, &g.field, &g.matplane, &g.tables, &g.mparams, &g.cellsOld, &g.extOld, &g.scene, &g.texData, &g.texInfo, &g.bgTile, &g.sceneVis, &g.sceneBg, &g.ovr, &g.entTex, &g.entInst, &g.entLayer, &g.ents, &g.ctrl, &g.projTypes, &g.projPool, &g.overlay, &g.edits, &g.reactHead, &g.reactRec, &g.rigidShapesDev, &g.rigidBodies, &g.rigidNext, &g.fxTables, &g.fxTex, &g.fxHost, &g.fxState, &g.fxXev, &g.fxLights, &g.entPrev, &g.bodyGrid, &g.burnPx, &g.birth, &g.areaTab, &g.sky, &g.lit, &g.glowTab, &g.glowA, &g.glowB, &g.fog, &g.fogVis, &g.partReq, &g.birthOld, &g.probe, &g.entDepth, &g.cutRules, &g.partGlow, &g.litCells, &g.clipHalf, &g.worldCells, &g.clipRaw, &g.farCells, &g.farMap, &g.verletPts, &g.luaTasks}; + const Buffer* bs[kBindings] = {&g.cells, &g.mats, &g.pixels, &g.ext, &g.claims, &g.intent, &g.field, &g.matplane, &g.tables, &g.mparams, &g.cellsOld, &g.extOld, &g.scene, &g.texData, &g.texInfo, &g.bgTile, &g.sceneVis, &g.sceneBg, &g.ovr, &g.entTex, &g.entInst, &g.entLayer, &g.ents, &g.ctrl, &g.projTypes, &g.projPool, &g.overlay, &g.edits, &g.reactHead, &g.reactRec, &g.rigidShapesDev, &g.rigidBodies, &g.rigidNext, &g.fxTables, &g.fxTex, &g.fxHost, &g.fxState, &g.fxXev, &g.fxLights, &g.entPrev, &g.bodyGrid, &g.burnPx, &g.birth, &g.areaTab, &g.sky, &g.lit, &g.glowTab, &g.glowA, &g.glowB, &g.fog, &g.fogVis, &g.partReq, &g.birthOld, &g.probe, &g.entDepth, &g.cutRules, &g.partGlow, &g.litCells, &g.clipHalf, &g.worldCells, &g.clipRaw, &g.farCells, &g.farMap, &g.verletPts, &g.luaTasks, &g.terrainAge, &g.terrainAgeOld, &g.hitLiquid}; VkDescriptorBufferInfo infos[kBindings]; VkWriteDescriptorSet w[kBindings]{}; for (uint32_t i = 0; i < kBindings; ++i) { @@ -1169,11 +1178,8 @@ int main(int argc, char** argv) { for (const auto& m : db.materials()) tablesV[base + 132 + m.index] = (uint32_t)std::clamp(ng::attr_int(m, "fire_colors_index", 0), 0, 5); { // 0070e8c0: cold_freezes_to_material + 1 | cold_freezes_chance_rev << 16, warmth_melts_to_material + 1 | warmth_melts_chance_rev << 16 (constructor default 100); used with the biome modifiers' reaction chances const size_t cb = base + 132 + db.materials().size() + 2 + 8192 + 1 + 64 * 6 + 1 + 16 * 3 + 8 * 3272 + 8192 * 4; - for (const auto& m : db.materials()) { - auto conv = [&](const char* to, const char* rev) -> uint32_t { const auto it = m.attrs.find(to); const ng::Material* t = it == m.attrs.end() || it->second.empty() ? nullptr : db.find(it->second); return t ? (uint32_t)(t->index + 1) | ((uint32_t)std::clamp(ng::attr_int(m, rev, 100), 0, 32767) << 16) : 0u; }; - tablesV[cb + 2 * m.index] = conv("cold_freezes_to_material", "cold_freezes_chance_rev"); - tablesV[cb + 2 * m.index + 1] = conv("warmth_melts_to_material", "warmth_melts_chance_rev"); - } + const auto conversions = ng::build_thermal_conversions(db); + std::copy(conversions.begin(), conversions.end(), tablesV.begin() + cb); } } // ---- the game's material textures (graphics texture_file), tiled in world space ---- @@ -1617,6 +1623,9 @@ int main(int argc, char** argv) { g.birth = g.make_buffer(nCells * 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, devLocal); g.birthOld = g.make_buffer(nCells * 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, devLocal); g.fill_zero(g.birth, 0, VK_WHOLE_SIZE); + g.terrainAge = g.make_buffer(nCells * 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, devLocal); + g.terrainAgeOld = g.make_buffer(nCells * 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, devLocal); + g.fill_zero(g.terrainAge, 0, VK_WHOLE_SIZE); uint32_t* fxTexPtr = nullptr; // asks the GPU for a particle this frame: mode 0 a cosmetic droplet that stains where it ends and leaves `material` there, mode 1 a real particle of the material auto emitPart = [&](double x, double y, double vx, double vy, double life, double grav, uint32_t rgb, int mode, int material, bool collide = true, bool force = true, uint32_t alpha = 255, bool glow = false, bool fade = true, double airForce = 0.0, double airScale = 0.0) { @@ -1645,8 +1654,8 @@ int main(int argc, char** argv) { gfx.attach((uint32_t*)tp, (uint32_t*)sp, hp); } - try { g.cellsRead = g.make_buffer((VkDeviceSize)kGridW * kGridH * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, hostVis | VK_MEMORY_PROPERTY_HOST_CACHED_BIT); } // cached: the physics reads the whole grid every tick - catch (...) { g.cellsRead = g.make_buffer((VkDeviceSize)kGridW * kGridH * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, hostVis); } + try { g.cellsRead = g.make_buffer((VkDeviceSize)kGridW * kGridH * 8, VK_BUFFER_USAGE_TRANSFER_DST_BIT, hostVis | VK_MEMORY_PROPERTY_HOST_CACHED_BIT); } // cached: the physics reads the whole grid every tick + catch (...) { g.cellsRead = g.make_buffer((VkDeviceSize)kGridW * kGridH * 8, VK_BUFFER_USAGE_TRANSFER_DST_BIT, hostVis); } const uint32_t* cellsReadPtr = nullptr; try { g.bodyRead = g.make_buffer((VkDeviceSize)kGridW * kGridH * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, hostVis | VK_MEMORY_PROPERTY_HOST_CACHED_BIT); } // the cells covered by rigid bodies, read back every 5th tick (the path grid; cached: the grid build reads it all) catch (...) { g.bodyRead = g.make_buffer((VkDeviceSize)kGridW * kGridH * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, hostVis); } @@ -1709,6 +1718,8 @@ int main(int argc, char** argv) { } // The rigid bodies are simulated on the CPU by the recovered Box2D of the original (src/phys); NG_GPU_RIGID=1 selects the old GPU solver. ng::phys::PhysSystem physSys; + ng::RigidDebug rigidDebug; + bool showColliders = std::getenv("NG_COLLIDERS") != nullptr; const bool cpuPhys = !std::getenv("NG_GPU_RIGID"); if (cpuPhys) { std::vector mc; @@ -1718,6 +1729,8 @@ int main(int argc, char** argv) { c.solid = m.type != ng::CellType::Air && (sp.is_static || (m.type == ng::CellType::Liquid && sp.sand)); // static cells and powders: bodies rest on sand and soil (PhysicsBody2Component go_through_sand "if 1, will go through sand": by default they do not) c.liquid = m.type == ng::CellType::Liquid && !sp.sand; // (a static liquid is both: solid terrain for the fixtures and liquid for the immersion) c.powder = m.type == ng::CellType::Liquid && sp.sand && !sp.is_static; // (a moving powder: terrain for the bodies except those that go through sand) + c.never_box2d = ng::attr_int(m, "liquid_sand_never_box2d", 0) != 0; + c.sticks_to_ceiling = ng::attr_int(m, "liquid_sticks_to_ceiling", 0) != 0; c.friction = ng::attr_float(m, "solid_friction", 0.3f); c.restitution = ng::attr_float(m, "solid_restitution", 0.2f); mc.push_back(c); @@ -1727,6 +1740,10 @@ int main(int argc, char** argv) { } g.upload(g.ents, entStateV.data(), entStateV.size() * sizeof(ng::EntityState)); struct CtrlBlock { int32_t inp[4]; int32_t inp2[4]; int32_t inp3[4]; float outp[4]; float outp2[4]; float view[4]; float hand0[4]; int32_t hand1[4]; float hand2[4]; int32_t burnN[4]; int32_t burnEv[1024]; int32_t reactN[4]; int32_t reactEv[256]; }; // burnN / burnEv: pixels burnt away this frame (rigid_burn.comp); view: camera shift of the last tick, interpolation alpha, gravity scale; hand: the wand in the hand + g.hitLiquid = g.make_buffer(sizeof(ng::HitLiquidTable), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, hostVis); + ng::HitLiquidTable* hitLiquidPtr = nullptr; + VK(vkMapMemory(g.dev, g.hitLiquid.mem, 0, VK_WHOLE_SIZE, 0, (void**)&hitLiquidPtr)); + *hitLiquidPtr = {}; g.ctrl = g.make_buffer(sizeof(CtrlBlock), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, hostVis); g.ctrlR = g.make_buffer(sizeof(CtrlBlock), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, devLocal); // (the render half's ctrl, see Gpu::dsetR) CtrlBlock* ctrlPtr = nullptr; @@ -1781,11 +1798,10 @@ int main(int argc, char** argv) { if (n == "radioactive_liquid" || n == "cloud_radioactive") return 32; return 0; }; - struct CollExplode { double power = 0, rmin = 0, rmax = 150, vmin = 0, prob = 0; std::map cfg; }; // the ExplosionConfig of a material with solid_on_collision_explode + struct CollExplode { double power = 0, rmin = 5, rmax = 150, vmin = 0, prob = 0; std::map cfg; }; // the ExplosionConfig of a material with solid_on_collision_explode std::map collExplode; // by body index struct PendExplosion { double x, y, r; std::map cfg; }; std::vector pendingExplosions; // the explosions of impacts, run with the others in the game logic block - std::vector collExplodeAt(kMaxEnts, -1000); // frame of the last explosion of a body std::vector forceExplode(kMaxEnts, 0); // ExplodeOnDamageComponent: the explosion was rolled (damage or modified body), the death explodes at once std::vector origSolid(kMaxEnts, 0); // opaque pixels of a body when it was made (the destroyed fraction of physics_body_destruction_required) std::vector propHpSeen(kMaxEnts, -1.0f); // hp of the destructible props at the last tick (explode_on_damage_percent) @@ -1799,10 +1815,28 @@ int main(int argc, char** argv) { std::vector cracks; std::map> crackedPx; // sprite image (texel offset of the body's idle frame) -> its cracked pixels (cell hp below the material's) std::map bodyMaterial; // rigid body -> its material (PhysicsImageShapeComponent material, the ragdoll material): the durability against explosions - auto setCollAudio = [&](int body, const ng::Material* bm, int cells) { + auto setBodyMaterial = [&](int body, const ng::Material* bm, int cells) { collAudio.erase(body); if (bm) bodyMaterial[body] = bm->index; else bodyMaterial.erase(body); + collExplode.erase(body); collFx.erase(body); + const auto* target = bm ? db.find(bm->attrs.count("solid_break_to_type") ? bm->attrs.at("solid_break_to_type") : "") : nullptr; + const auto* fallback = db.find("rotten_meat"); + physSys.set_material_lifecycle(body, bm ? bm->index : -1, bm && ng::attr_int(*bm, "solid_on_sleep_convert", 0) != 0, + target && target->index > 0 ? target->index : fallback ? fallback->index : -1); if (!bm) return; + if (bm && ng::attr_int(*bm, "solid_on_collision_explode", 0) != 0) { // meteorites, sulphur, collapsed concrete: the impact explodes (006f7f00) + CollExplode ce; + ce.power = ng::attr_float(*bm, "explosion.cell_explosion_power", 1.0f); ce.rmin = ng::attr_float(*bm, "explosion.cell_explosion_radius_min", 5.0f); + ce.rmax = ng::attr_float(*bm, "explosion.cell_explosion_radius_max", 150.0f); ce.vmin = ng::attr_float(*bm, "explosion.cell_explosion_velocity_min", 0.0f); + ce.prob = ng::attr_float(*bm, "explosion.cell_explosion_probability", 0.0f); + for (const auto& [k, v] : bm->attrs) if (k.rfind("explosion.", 0) == 0) ce.cfg["config_explosion." + k.substr(10)] = v; + collExplode[body] = ce; + } + const auto splash = bm->attrs.find("solid_on_collision_material"); + const auto* splashMaterial = splash == bm->attrs.end() ? nullptr : db.find(splash->second); + const float splashPower = ng::attr_float(*bm, "solid_on_collision_splash_power", 0.0f); + if (splashMaterial && splashPower > 0) collFx[body] = {splashPower, splashMaterial->index, cells}; + if (ng::attr_int(*bm, "solid_go_through_sand", 0) != 0) rigid.set_go_through_sand(body); CollAudio ca; auto a = bm->attrs.find("audio_physics_material_solid"); if (a != bm->attrs.end()) ca.mat = a->second; @@ -1854,16 +1888,12 @@ int main(int argc, char** argv) { std::vector deathDone(kMaxEnts, 0); std::deque> corpses; // slots of the parts of every corpse, oldest first std::vector freeBodyOrigin(kMaxEnts, 0); // the bodies without an entity: 1 cut loose ground, 2 a burnt / broken piece, 3 an explosion chunk (NG_BODYHIST) - // Free bodies (no entity: cut loose ground, broken pieces, explosion chunks), oldest first, with the serial of their slot's use: past kMaxFreeBodies the oldest is - // removed. (Ours, not the engine's: in the snow cave the ice melts and freezes again all the time, every melted cell cuts loose a few crumbs of ice_glass, and - // after a minute hundreds of them bobbed in the pools awake - most of the physics step.) - constexpr size_t kMaxFreeBodies = 200; - std::deque> freeBodies; - std::vector freeBodySerial(kMaxEnts, 0); - auto noteFreeBody = [&](int slot, uint8_t origin) { freeBodyOrigin[slot] = origin; freeBodies.push_back({slot, ++freeBodySerial[slot]}); }; + // Anonymous bodies keep their lifetime: only native material conversion, + // destruction or explicit removal releases them, never a FIFO count cap. + auto noteFreeBody = [&](int slot, uint8_t origin) { freeBodyOrigin[slot] = origin; }; size_t corpsePartCount = 0; std::vector freeSlots; // released ragdoll slots - // TODO(to-do #8): kicker immunity (+0x74 / +0x78), telekinesis (+0x48), the meaning of +0x31 + // TODO(to-do #8): kicker immunity (+0x74 / +0x78), telekinesis (+0x48), telekinesis release state std::vector hurtAnySlot(kMaxEnts, 0); // per slot: its body hurts every entity (the body record's +0x30, 00ab6010's second gate; set only by the LooseGround chunks, 00ba6330) int corpseGroup = 0; std::function mapRanged; // creatures' ranged attacks become nodes of the GPU projectile system @@ -2368,11 +2398,126 @@ int main(int argc, char** argv) { auto hideSlot = [&](int slot) { // dead flag: the entity is no longer simulated or drawn if (slot < 0 || slot >= kMaxEnts) return; hurtAnySlot[(size_t)slot] = 0; - freeBodyOrigin[(size_t)slot] = 0; ++freeBodySerial[(size_t)slot]; // (the slot's free body, if any, is gone: a later user of the slot is not removed by the cap) + freeBodyOrigin[(size_t)slot] = 0; const int32_t flags = entReadPtr[slot].st[3] | ng::kEntDead; entReadPtr[slot].st[3] = flags; patchEntity(slot, offsetof(ng::EntityState, st) + 12, &flags, 4); }; + std::set frozenLiquidEntities; // Shared by every hit policy; no per-entity children scan. + int goreOverrideFrame = -1000; + float goreOverrideValue = 1.0f, currentGore = 1.0f; + auto updateHitLiquidWorld = [&] { + frozenLiquidEntities.clear(); + float base = 1.0f; + if (rt) { + for (int c : rt->components(rt->world_state_entity(), "WorldStateComponent", "", false)) { + const auto& a = rt->comp(c)->attrs; + auto it = a.find("gore_multiplier"); + if (it != a.end()) base = std::atof(it->second.c_str()); + } + float multiplied = base; + bool overrideActive = false; + for (const auto& effect : rt->all_game_effects()) { + if (effect.effect == "GLOBAL_GORE") { multiplied *= 2.0f; overrideActive = true; } + if (effect.effect == "FROZEN") { + // has_game_effect checks the entity itself and its direct children. + frozenLiquidEntities.insert(effect.owner); + if (const auto* component = rt->comp(effect.comp)) frozenLiquidEntities.insert(component->entity); + } + } + if (overrideActive) { goreOverrideFrame = rt->frame(); goreOverrideValue = multiplied; } + } + const bool recent = rt && rt->frame() - goreOverrideFrame < 2; + currentGore = recent ? goreOverrideValue : base; + hitLiquidPtr->settings[0] = int(currentGore); + hitLiquidPtr->settings[1] = recent ? 1 : 0; + }; + auto hitLiquidPolicy = [&](int id) { + ng::HitLiquidPolicy policy; + if (!rt || !id || !rt->alive(id)) return policy; + if (frozenLiquidEntities.count(id)) policy.flags |= 2u; + const auto cs = rt->components(id, "DamageModelComponent", "", false); + if (cs.empty()) return policy; + policy.flags |= 1u; + if (const auto* entity = rt->ent(id)) { policy.inventory[2] = float(entity->sx); policy.inventory[3] = float(entity->sy); } + const auto hitboxes = rt->components(id, "HitboxComponent", "", false); + if (!hitboxes.empty()) { + policy.flags |= 4u; + const auto& a = rt->comp(hitboxes.front())->attrs; + const char* keys[4] = {"aabb_min_x", "aabb_max_x", "aabb_min_y", "aabb_max_y"}; + for (int i = 0; i < 4; ++i) { auto it = a.find(keys[i]); policy.bounds[i] = it == a.end() ? (i % 2 ? 5.0f : -5.0f) : float(std::atof(it->second.c_str())); } + } + const auto inventories = rt->components(id, "MaterialInventoryComponent", "", false); + if (!inventories.empty()) { + const auto& a = rt->comp(inventories.front())->attrs; + auto num = [&](const char* key, float fallback) { auto it = a.find(key); return it == a.end() ? fallback : float(std::atof(it->second.c_str())); }; + policy.inventory[0] = num("leak_on_damage_percent", 0); + policy.inventory[1] = num("min_damage_to_leak", 0.09f); + policy.pressure[0] = num("leak_pressure_min", 0.7f); policy.pressure[1] = num("leak_pressure_max", 1.1f); + if (num("leak_gently", 1) != 0) policy.flags |= 8u; + const auto emitters = rt->components(id, "ParticleEmitterComponent", "", false); + if (!emitters.empty()) { + policy.flags |= 16u; + const auto& emitter = rt->comp(emitters.front())->attrs; + auto it = emitter.find("is_trail"); if (it != emitter.end() && it->second == "1") policy.flags |= 32u; + it = emitter.find("trail_gap"); if (it != emitter.end()) policy.pressure[2] = float(std::atof(it->second.c_str())); + } + } + + const auto& a = rt->comp(cs.front())->attrs; + auto material = [&](const char* key, const char* fallback, uint32_t& colour) { + const auto it = a.find(key); + const std::string name = it == a.end() ? fallback : it->second; + const ng::Material* m = name.empty() ? nullptr : db.find(name); + if (!m) return 0u; + colour = (m->graphics_color ? m->graphics_color : m->wang_color) & 0xFFFFFFu; + return uint32_t(m->index + 1); + }; + policy.materials = material("blood_material", "blood_fading", policy.baseColour); + policy.materials |= material("blood_spray_material", "", policy.sprayColour) << 16; + auto it = a.find("blood_multiplier"); + const float multiplier = it == a.end() ? 1.0f : float(std::atof(it->second.c_str())); + std::memcpy(&policy.multiplier, &multiplier, 4); + return policy; + }; + auto hitLiquidHost = [&](int slot, float damage, float x, float y, float vx, float vy, int fx, float incoming = 1.0f, float speedScale = 1.0f) { + updateHitLiquidWorld(); // Script hits can add/remove effects within the frame. + const auto p = hitLiquidPolicy(slot == 0 ? rt->player() : slotToId[slot]); + ng::EntityState& entity = entReadPtr[slot]; + if (entity.st[0] == 4 && entity.ex7[1] > 0 && damage > p.inventory[1] && damage < entity.hp[0] && p.inventory[0] > 0) { + auto random15 = [&] { return float(fxRng() & 32767u) / 32767.0f; }; + if (ng::inventory_hit_liquid_enabled(damage, entity.hp[0], p.inventory[0], p.inventory[1], random15(), (p.flags & 16u) != 0)) { + float lx = x - entity.pos[0], ly = y - entity.pos[1]; + ng::hit_liquid_local_point(lx, ly, p.inventory[2], p.inventory[3], entity.vel[0], (p.flags & 4u) != 0, p.bounds); + const float length = std::hypot(lx, ly), dx = length > 0 ? lx / length : 0, dy = length > 0 ? ly / length : 0; + const float ux = random15(), uy = random15(), pressure = random15(); + const float speed = 100 * (p.pressure[0] + (p.pressure[1] - p.pressure[0]) * pressure); + const float vx = dx * (0.85f + 0.29999995f * ux) * speed, vy = dy * (0.85f + 0.29999995f * uy) * speed; + const float a = random15(), b = random15(), c = random15(), d = random15(); + entity.ex[0] = vx * (0.6f + 0.39999998f * a); entity.ex[1] = vx * (1 + 0.39999998f * b); + entity.ex[2] = vy * (0.6f + 0.39999998f * c); entity.ex[3] = vy * (1 + 0.39999998f * d); + entity.ex2[1] = 2; + entity.ex8[0] = int(lx * 1000); entity.ex8[1] = int(ly * 1000); + patchEntity(slot, offsetof(ng::EntityState, ex), entity.ex, 16); + patchEntity(slot, offsetof(ng::EntityState, ex2) + 4, &entity.ex2[1], 4); + patchEntity(slot, offsetof(ng::EntityState, ex8), entity.ex8, 8); + } + } + if (!ng::hit_liquid_enabled(damage, incoming, fx, (p.flags & 2u) != 0)) return; + unsigned material = p.materials & 65535u; + uint32_t colour = p.baseColour; + if (hitLiquidPtr->settings[1] && (p.materials >> 16)) { material = p.materials >> 16; colour = p.sprayColour; } + if (!material) return; + float multiplier; std::memcpy(&multiplier, &p.multiplier, 4); + auto random15 = [&] { return float(fxRng() & 32767u) / 32767.0f; }; + const int count = ng::hit_liquid_count(random15(), multiplier, incoming, hitLiquidPtr->settings[0]); + for (int k = 0; k < count; ++k) { + const float su = random15(), au = random15(), yu = random15(), xu = random15(); + const auto drop = ng::hit_liquid_drop(x, y, vx, vy, speedScale, su, au, yu, xu); + if (drop.x < viewX || drop.y < viewY || drop.x >= viewX + kGridW || drop.y >= viewY + kGridH) continue; + emitPart(drop.x, drop.y, drop.vx, drop.vy, 70.0f + au * 40.0f, 100.0 / 3600.0, colour, 1, material - 1); + } + }; if (rt) { // Retain the runtime's cadence/counters for required twins and enqueue // their reviewed native program; accepted chunks do not execute CPU Lua. @@ -3064,7 +3209,7 @@ int main(int argc, char** argv) { // (00ab9a90): the amount times the victim's multiplier of the type goes into the GPU accumulator (milli-hp; negative heals), the responsible entity is stamped as the // attacker (script_damage_received, kill credit), curse damage keeps the "sound_damage_curse" loop going, and a knockback_force of at least minimum_knockback_force kicks // a surviving character along the impulse (008970f0 normalised), always upward, over its mass (00ab9910: the pending kick, as the explosions' one) - rt->on_inflict_damage = [&](int id, double amount, const std::string& dt, const std::string& desc, const std::string&, double ix, double iy, int who, double wx, double wy, double kf) { + rt->on_inflict_damage = [&](int id, double amount, const std::string& dt, const std::string& desc, const std::string& ragdollFx, double ix, double iy, int who, double wx, double wy, double kf) { const int slot = id == rt->player() ? 0 : [&] { const ng::RtEntity* re = rt->ent(id); return re && re->gpu >= 0 && re->gpu < entN ? re->gpu : -1; }(); if (slot < 0 || !entReadPtr || rt->components(id, "DamageModelComponent", "", false).empty()) return; ng::EntityState& e = entReadPtr[slot]; @@ -3077,6 +3222,8 @@ int main(int argc, char** argv) { } const double dmg = raw * damageTypeMult(e, dt); if (std::getenv("NG_SCRIPTLOG")) std::printf("frame %d: EntityInflictDamage %d (slot %d) %.3f %s x%.2f by %d \"%s\"%c", rt->frame(), id, slot, amount, dt.c_str(), dmg / (amount != 0.0 ? amount : 1.0), who, desc.c_str(), 10); + hitLiquidHost(slot, dt == "DAMAGE_HEALING" ? -float(std::abs(dmg)) : float(dmg), float(wx == 0.0 && wy == 0.0 ? e.pos[0] : wx), + float(wx == 0.0 && wy == 0.0 ? e.pos[1] : wy), float(ix), float(iy), ragdollFx == "NONE" ? 0 : 1); if (e.ex8[0] & kAboutToBeScript) hpTake(slot, dmg); else if (dmg != 0.0) { const int32_t milli = e.x[0] + (int32_t)std::lround(dmg * 1000.0); e.x[0] = milli; patchEntity(slot, offsetof(ng::EntityState, x), &milli, 4); } if (dmg > 0.0) { dmgMsg[slot] = desc; dmgMsgFrame[slot] = rt->frame(); } // 00ab9a90 passes the description on as the message of Message_DamageReceived @@ -3636,6 +3783,7 @@ int main(int argc, char** argv) { }; std::map shapeOfTex; // sprite texel offset -> rigid shape size_t shapesUploaded = 0; + uint64_t shapeCacheEpoch = 0, shapeUploadEpoch = 0; bool makingPart = false; // Which body pixels the player and the creatures collide with: the player's sweep 00bfa680 -> 00bfa290 -> 00640270 tests each cell with 0063ef70, which takes a cell only // when its virtual +0x2c (CSolidCell 004adb40: +0x58, the material's platform_type set by 0071bdb0; unset is -1) is exactly 1: wood_prop, metal_prop, steel... - crates, @@ -3647,7 +3795,7 @@ int main(int argc, char** argv) { if (!worldMode || slot < 0 || slot >= kMaxEnts) return; const ng::EntityLayer& L = entBuilder.layer(); if (std::getenv("NG_EMBEDCHECK")) std::printf("frame %d: makeRigid slot %d id %d at (%.1f, %.1f)%c", (int)ctrlPtr->inp2[0], slot, slotToId[slot], L.ents[slot].pos[0], L.ents[slot].pos[1], 10); - if (!makingPart && L.body_part_slots.count(slot)) return; // the other bodies of a prop (a wheel) are made with the first + if (!makingPart && L.body_part_slots.count(slot)) { if (freeBodyOrigin[slot]) std::printf("rigid skipped reused body-part slot %d (origin %d)\n", slot, (int)freeBodyOrigin[slot]); return; } // the other bodies of a prop (a wheel) are made with the first if (slot >= (int)L.ents.size() || L.ents[slot].st[0] != 4) { if (std::getenv("NG_RIGIDDBG") && slot < (int)L.ents.size() && slotToId[slot] && rt->ent(slotToId[slot]) && rt->ent(slotToId[slot])->file.find("lantern") != std::string::npos) std::printf("rigid: slot %d kind %d not a body (%s)%c", slot, L.ents[slot].st[0], rt->ent(slotToId[slot])->file.c_str(), 10); return; } const ng::SpriteInstance* si = nullptr; for (const ng::SpriteInstance& c : L.inst) if (c.ent == slot) { si = &c; break; } @@ -3669,22 +3817,26 @@ int main(int argc, char** argv) { const ng::EntityDef* def0 = re0 ? entBuilder.db().get(re0->file) : nullptr; if (def0 && def0->all("PhysicsBodyComponent").empty() && def0->all("PhysicsBody2Component").empty()) return; } + if (shapeCacheEpoch != rigid.shape_epoch()) { shapeOfTex.clear(); shapeCacheEpoch = rigid.shape_epoch(); } auto it = shapeOfTex.find((int)si->idle.tex); if (it == shapeOfTex.end()) { int idx = -1; if (auto sh = ng::make_shape(&L.texels[si->idle.tex], si->idle.w, si->idle.h)) idx = rigid.add_shape(*sh); + if (shapeCacheEpoch != rigid.shape_epoch()) { shapeOfTex.clear(); shapeCacheEpoch = rigid.shape_epoch(); } + if (idx < 0) { std::printf("rigid shape allocation failed: slot %d, %dx%d, %zu mask words used\n", slot, si->idle.w, si->idle.h, rigid.shape_words_used()); return; } it = shapeOfTex.emplace((int)si->idle.tex, idx).first; } if (it->second < 0) return; - if (rigid.shape_words_used() > shapesUploaded) { // new shape: the device copy of the table follows + if (shapeUploadEpoch != rigid.shape_epoch() || rigid.shape_words_used() > shapesUploaded) { // new shape / compacted masks: the device copy follows g.upload_at(g.rigidShapesDev, 0, rigidShapeWords, (size_t)ng::GpuBodies::kMaxShapes * ng::GpuBodies::kShapeHeaderWords * 4); - const size_t from = std::max(shapesUploaded, (size_t)ng::GpuBodies::kMaxShapes * ng::GpuBodies::kShapeHeaderWords); + const size_t from = shapeUploadEpoch != rigid.shape_epoch() ? (size_t)ng::GpuBodies::kMaxShapes * ng::GpuBodies::kShapeHeaderWords : std::max(shapesUploaded, (size_t)ng::GpuBodies::kMaxShapes * ng::GpuBodies::kShapeHeaderWords); g.upload_at(g.rigidShapesDev, from * 4, rigidShapeWords + from, (rigid.shape_words_used() - from) * 4); shapesUploaded = rigid.shape_words_used(); + shapeUploadEpoch = rigid.shape_epoch(); } const ng::EntityState& es = L.ents[slot]; const int bodyIdx = rigid.add_body(it->second, slot, si->ox, si->oy, es.pos[0], es.pos[1]); - if (bodyIdx < 0) return; + if (bodyIdx < 0) { std::printf("rigid body allocation failed: slot %d shape %d\n", slot, it->second); return; } g.fill_zero(g.burnPx, (VkDeviceSize)bodyIdx * 16384, 16384); // no pixel is burning collFx.erase(bodyIdx); collAudio.erase(bodyIdx); bodyMaterial.erase(bodyIdx); rigid.bodies()[bodyIdx].i[2] &= ~512; @@ -3703,23 +3855,10 @@ int main(int argc, char** argv) { const ng::Material* bm = db.find(sc->get("material")); if (bm && !isPlatform(*bm)) rigid.set_char_pass(bodyIdx); // (the lanterns: glass_box2d; the chains: metal_chain_nohit - the player walks through; crates are platforms) if (bm && ng::attr_int(*bm, "solid_go_through_sand", 0) != 0) rigid.set_go_through_sand(bodyIdx); // heavy bodies (meteorites) sink through sand without pushing it - if (bm && ng::attr_int(*bm, "solid_on_collision_explode", 0) != 0) { // meteorites, sulphur, collapsed concrete: the impact explodes (006f7f00) - CollExplode ce; - ce.power = ng::attr_float(*bm, "explosion.cell_explosion_power", 1.0f); ce.rmin = ng::attr_float(*bm, "explosion.cell_explosion_radius_min", 0.0f); - ce.rmax = ng::attr_float(*bm, "explosion.cell_explosion_radius_max", 150.0f); ce.vmin = ng::attr_float(*bm, "explosion.cell_explosion_velocity_min", 0.0f); - ce.prob = ng::attr_float(*bm, "explosion.cell_explosion_probability", 0.0f); - for (const auto& [k, v] : bm->attrs) if (k.rfind("explosion.", 0) == 0) ce.cfg["config_explosion." + k.substr(10)] = v; - collExplode[bodyIdx] = ce; - } - if (bm && ng::attr_float(*bm, "solid_on_collision_splash_power", 0.0f) > 0.0f) { // meat and the like splash on impact (006f8070 / 006f82f0) - const auto itm = bm->attrs.find("solid_on_collision_material"); - const ng::Material* sm = itm == bm->attrs.end() ? nullptr : db.find(itm->second); - if (sm) collFx[bodyIdx] = {ng::attr_float(*bm, "solid_on_collision_splash_power", 0.0f), sm->index, 40}; - } if (bm) { rigid.set_surface(bodyIdx, ng::attr_float(*bm, "solid_friction", 0.3f), ng::attr_float(*bm, "solid_restitution", 0.2f)); rigid.set_density(bodyIdx, ng::attr_float(*bm, "density", 1.0f)); } - setCollAudio(bodyIdx, bm, origSolid[slot]); - if (bm && ng::attr_int(*bm, "burnable", 0) != 0) { - const float hpv = ng::attr_float(*bm, "fire_hp", 0.0f); + setBodyMaterial(bodyIdx, bm, origSolid[slot]); + if (bm) { + const float hpv = ng::attr_int(*bm, "burnable", 0) != 0 ? ng::attr_float(*bm, "fire_hp", 0.0f) : -1.0f; float fireVals[3] = {hpv < 0 ? 1.0e9f : hpv + 1.0f, ng::attr_float(*bm, "autoignition_temperature", 100.0f), (float)(bm->index + 1)}; if (std::getenv("NG_ENTLIST")) std::printf("burnable body slot %d material %s fire_hp %.0f autoignition %.0f%c", slot, bm->name.c_str(), hpv, fireVals[1], 10); std::memcpy(entReadPtr[slot].ex5, fireVals, sizeof fireVals); @@ -3760,6 +3899,8 @@ int main(int argc, char** argv) { const ng::Material* lm = parts.size() == 1 ? db.find(parts[0].mat) : nullptr; if (lm && total > 0) { ng::EntityState& ls = entReadPtr[slot]; + ls.ex10[0] = ls.ex10[2] = ls.pos[0]; ls.ex10[1] = ls.ex10[3] = ls.pos[1]; + patchEntity(slot, offsetof(ng::EntityState, ex10), ls.ex10, 16); ls.ex7[1] = (float)total; ls.ex7[2] = (float)(lm->index + 1); ls.ex3[0] = lm->index + 1; ls.ex3[1] = (int32_t)((lm->graphics_color ? lm->graphics_color : lm->wang_color) & 0xFFFFFFu) | 0x04000000; patchEntity(slot, offsetof(ng::EntityState, ex7), ls.ex7, 16); @@ -3875,8 +4016,7 @@ int main(int argc, char** argv) { double hx = es.aux[2] * 0.001, hy = es.aux[3] * 0.001; const double hl = std::hypot(hx, hy); if (hl > 0.01) { hx /= hl; hy /= hl; } else { hx = 0; hy = -0.3; } - const bool slice = (types & 8) != 0; - const double power = gib ? 2.4 : (slice ? 1.6 : 0.9); + const double power = gib ? 2.4 : 0.9; ++corpseGroup; struct PartInfo { int body; int slot; std::vector> px; }; std::vector made; @@ -3890,6 +4030,16 @@ int main(int argc, char** argv) { const ng::Material* ragMat = db.find(convMat.empty() ? std::string("meat") : convMat); const double ragDensity = ragMat ? ng::attr_float(*ragMat, "density", 1.0f) : 1.0; const double bloodAbs = dm ? std::atof(dm->get("ragdoll_blood_amount_absolute", "-1").c_str()) : -1.0; + updateHitLiquidWorld(); + int bloodMaterial = es.ex3[0] - 1; + uint32_t bloodColour = uint32_t(es.ex3[1]); + const std::string sprayName = dm ? dm->get("blood_spray_material") : std::string(); + if (!sprayName.empty()) { + const ng::Material* spray = db.find(sprayName); + bloodMaterial = spray ? spray->index : -1; + bloodColour = spray ? (spray->graphics_color ? spray->graphics_color : spray->wang_color) : 0; + } + int totalOpaque = 0; if (bloodAbs >= 0.0) for (const std::string& pf : parts) { auto itI = ragdollIcons.find(pf); if (itI == ragdollIcons.end()) itI = ragdollIcons.emplace(pf, hud::load_icon(wak, pf)).first; if (itI->second.ok()) for (uint32_t c : itI->second.px) totalOpaque += (c >> 24) >= 128; } for (const std::string& pf : parts) { @@ -3918,19 +4068,20 @@ int main(int argc, char** argv) { int instIdx = -1; const int ns = entBuilder.add_body_entity(tex, ic.w, ic.h, ox, si0.oy, es.pos[0], es.pos[1], reuse, &instIdx); slotToId[ns] = 0; deathDone[ns] = 0; propDone[ns] = 0; - if (es.ex3[0] > 0 && partsBleed) { // 00abb7d0: the blood of the creature becomes a finite reserve of this part, emitted when the part is damaged - const int amount = bloodAbs >= 0.0 && totalOpaque > 0 ? (int)((double)opaque / totalOpaque * bloodAbs) : (int)(opaque / 100.0 * ragDensity * 1000.0); - L.ents[ns].ex7[1] = (float)(int)((0.8 + 0.40000004 * frand()) * (double)(2 * amount)); - L.ents[ns].ex7[2] = (float)es.ex3[0]; + L.ents[ns].ex10[0] = L.ents[ns].ex10[2] = es.pos[0]; L.ents[ns].ex10[1] = L.ents[ns].ex10[3] = es.pos[1]; + const double partMass = opaque / 100.0 * ragDensity; + if (es.ex3[0] > 0 && partsBleed && partMass >= 0.009999999776) { // 00abb7d0: the blood of the creature becomes a finite reserve of this part, emitted when the part is damaged + L.ents[ns].ex7[1] = float(ng::ragdoll_hit_liquid_reserve(float(frand()), currentGore, 2.0f, partMass, totalOpaque / 100.0 * ragDensity, int(bloodAbs))); + L.ents[ns].ex7[2] = float(bloodMaterial + 1); L.ents[ns].ex7[3] = 0; - L.ents[ns].ex3[1] = es.ex3[1]; + L.ents[ns].ex3[1] = int32_t(bloodColour); { // the emitter of the piece (00abb7d0): the direction is the part's centre relative to the creature's origin, the speed scales with the part's mass double sx = 0, sy = 0; int n = 0; for (int yy = 0; yy < ic.h; ++yy) for (int xx = 0; xx < ic.w; ++xx) if ((px[(size_t)yy * ic.w + xx] >> 24) >= 128) { sx += xx; sy += yy; ++n; } double dx = n ? sx / n - ox : 0.0, dy = n ? sy / n - si0.oy : 0.0; const double dlen = std::hypot(dx, dy); if (dlen > 1e-4) { dx /= dlen; dy /= dlen; } else { dx = dy = 0.0; } - const double mass = amount * 0.001; + const double mass = partMass; const double u1 = frand(), u2 = frand(), u3 = frand(), u4 = frand(), u5 = frand(), u6 = frand(), u7 = frand(), u8 = frand(); dx *= 0.85 + 0.29999995 * u1; dy *= 0.85 + 0.29999995 * u2; const double base = (360.0 + 0.7 * 1200.0 * u3) * mass; @@ -3957,12 +4108,6 @@ int main(int argc, char** argv) { ng::GpuBody& b = rigid.bodies()[k]; if (b.i[1] != ns || (b.i[2] & 2)) continue; rigid.set_group(k, corpseGroup, true); - { // the material of the corpse (ragdoll_material, meat by default) splashes its own blood on impact - const ng::Material* rm = ragMat; - const auto itm = rm ? rm->attrs.find("solid_on_collision_material") : std::map::const_iterator(); - const ng::Material* sm = rm && itm != rm->attrs.end() ? db.find(itm->second) : nullptr; - if (rm && sm && ng::attr_float(*rm, "solid_on_collision_splash_power", 0.0f) > 0.0f) collFx[k] = {ng::attr_float(*rm, "solid_on_collision_splash_power", 0.0f), sm->index, 0}; - } if (ragMat && ng::attr_int(*ragMat, "burnable", 0) != 0) { // the parts are of the ragdoll material: burnable meat burns pixel by pixel (rigid_burn.comp) like any burnable body const float hpv = ng::attr_float(*ragMat, "fire_hp", 0.0f); const float fireVals[3] = {hpv < 0 ? 1.0e9f : hpv + 1.0f, ng::attr_float(*ragMat, "autoignition_temperature", 100.0f), (float)(ragMat->index + 1)}; @@ -3977,8 +4122,7 @@ int main(int argc, char** argv) { } PartInfo pi{k, ns, {}}; for (int yy = 0; yy < ic.h; ++yy) for (int xx = 0; xx < ic.w; ++xx) if ((px[(size_t)yy * ic.w + xx] >> 24) != 0) pi.px.push_back({xx, yy}); - if (collFx.count(k)) collFx[k].cells = (int)pi.px.size(); - setCollAudio(k, ragMat, (int)pi.px.size()); + setBodyMaterial(k, ragMat, (int)pi.px.size()); made.push_back(std::move(pi)); rigid.set_motion(k, hx * power * (0.5 + frand()) + (frand() - 0.5) * 0.7, hy * power * (0.5 + frand()) - 0.4 - frand() * 0.6, (frand() - 0.5) * (gib ? 0.5 : 0.3)); } @@ -4030,7 +4174,7 @@ int main(int argc, char** argv) { if (!(br > 0.0)) return; const int roughRot = (int)(frand() * 181.0) % 181; // the random rotation of the roughness table (as explode_rays.comp) if (std::getenv("NG_COLLDBG")) std::printf("bodyBlast at (%.0f, %.0f) r %.1f max durability %.0f energy %.0f%c", bx, by, br, maxDur, rayEnergy, 10); - struct BPx { int k, idx; float hp, dur; uint32_t rgb; }; + struct BPx { int k, idx; float hp, dur; uint32_t rgb; bool stainable; }; std::unordered_map> at; auto key = [](int x, int y) { return (int64_t)(((uint64_t)(uint32_t)x << 32) | (uint64_t)(uint32_t)y); }; for (int k = 0; k < rigid.count(); ++k) { @@ -4046,6 +4190,7 @@ int main(int argc, char** argv) { const auto bmi = bodyMaterial.find(k); const int bm = bmi != bodyMaterial.end() ? bmi->second : (int)std::lround(es.ex5[2]) - 1; const ng::Material* bmat = bm >= 0 && bm < (int)db.materials().size() ? &db.materials()[bm] : nullptr; + const bool stainable = !bmat || ng::attr_int(*bmat, "stainable", 1) != 0; const float hp = bmat ? (float)(int)ng::attr_float(*bmat, "hp", 100.0f) : 100.0f, dur = bmat ? ng::attr_float(*bmat, "durability", 0.0f) : 0.0f; const ng::SpriteInstance& si = entBuilder.layer().inst[ii]; const int w = si.idle.w, h = si.idle.h; @@ -4056,7 +4201,8 @@ int main(int argc, char** argv) { const double lx = px + 0.5 - si.ox, ly = py + 0.5 - si.oy; const double wx = es.pos[0] + cs * lx - sn * ly, wy = es.pos[1] + sn * lx + cs * ly; if (std::hypot(wx - bx, wy - by) > br + (stainsR < 0.0 ? 0.0 : (stainsR > 0.0 ? stainsR : 0.4 * br)) + 2.0) continue; - at[key((int)std::floor(wx), (int)std::floor(wy))].push_back({k, py * w + px, hp, dur, tx & 0xFFFFFFu}); + at[key((int)std::floor(wx), (int)std::floor(wy))].push_back({k, py * w + px, hp, dur, tx & 0xFFFFFFu, + stainable}); } } if (at.empty()) return; @@ -4124,6 +4270,9 @@ int main(int argc, char** argv) { const double sv = (0.5 + 0.2 * frand()) * rough * fall * fall; if (sv <= 0.0) continue; for (const BPx& q : list) { + // gold_box2d inherits stainable=0 from gem_box2d. Keep + // cosmetic scorching separate from explosion damage. + if (!q.stainable) continue; const ng::GpuBody& b = rigid.bodies()[q.k]; const int ii = entBuilder.first_instance(b.i[1]); if (ii < 0) continue; @@ -4162,12 +4311,12 @@ int main(int argc, char** argv) { } int looseImages = 0; auto looseBody = [&](const std::vector>& cells, int convMat) -> bool { - if (cells.empty() || !rt) return false; + if (cells.empty() || !rt || !rigid.has_body_capacity()) return false; int x0 = INT_MAX, y0 = INT_MAX, x1 = INT_MIN, y1 = INT_MIN; for (const auto& [cx, cy] : cells) { x0 = std::min(x0, cx); y0 = std::min(y0, cy); x1 = std::max(x1, cx); y1 = std::max(y1, cy); } const int w = x1 - x0 + 1, h = y1 - y0 + 1; - if (w > 128 || h > 128) return false; // (our bodies are at most 128 x 128 pixels: a larger region stays in the grid) - if (cells.size() <= 4) { // a crumb (4 cells or fewer) crumbles: its cells go, a few grains of its colour fall (ours: no body for it, see kMaxFreeBodies) + if (w > 128 || h > 128 || !rigid.has_shape_capacity(w, h)) return false; // (our bodies are at most 128 x 128 pixels: a larger region stays in the grid) + if (cells.size() <= 4) { // a crumb (4 cells or fewer) crumbles: its cells go, a few grains of its colour fall (ours: no body for it) for (const auto& [cx, cy] : cells) { const int ri = (cy - cellsViewY) * kGridW + (cx - cellsViewX); const uint32_t m = ri >= 0 && ri < kGridW * kGridH ? cellsReadPtr[ri] & 0xFFFFu : 0u; @@ -4216,19 +4365,18 @@ int main(int argc, char** argv) { bodyMaterial[kb] = convMat; rigid.set_surface(kb, ng::attr_float(bm, "solid_friction", 0.3f), ng::attr_float(bm, "solid_restitution", 0.2f)); rigid.set_density(kb, ng::attr_float(bm, "density", 1.0f)); + setBodyMaterial(kb, &bm, (int)cells.size()); } if (std::getenv("NG_LOOSELOG")) std::printf("loose cells: %zu cells (%d x %d at %d, %d) -> a body of %s, slot %d%c", cells.size(), w, h, x0, y0, bm.name.c_str(), ns, 10); return true; }; struct ConvWatch { int x, y, m; }; std::vector convWatch; - int convWatchVX = INT_MIN, convWatchVY = INT_MIN, convWatchFrame = -1000; auto looseTick = [&](int frame) { // the removed cells of convertible materials (the watch: the window's cells of those materials), then their neighbours' regions if (!anyConvertible || !cellsReadPtr || !rt || cellsViewX != viewX || cellsViewY != viewY) return; auto matAt = [&](int x, int y) -> int { const int gx = x - cellsViewX, gy = y - cellsViewY; if (gx < 0 || gy < 0 || gx >= kGridW || gy >= kGridH) return -1; return (int)(cellsReadPtr[gy * kGridW + gx] & 0xFFFFu); }; std::vector removed; for (ConvWatch& cw : convWatch) { if (cw.m < 0) continue; const int m = matAt(cw.x, cw.y); if (m >= 0 && m != cw.m) { removed.push_back(cw); cw.m = -1; } } - bool converted = false; // the cells the fills of this tick reached (a stamp per window cell): a seed already inside a region of this tick is not filled again. Each removed cell // used to fill its regions afresh (a 147 KB visited map each): a worm eating through a convertible material removed dozens of cells a frame, each refilling // the same large region (tens of ms), and two seeds of one cut region made two bodies of the same cells (the grid read here is only updated next frame) @@ -4268,25 +4416,18 @@ int main(int argc, char** argv) { const int m = matAt(h[0], h[1]); if (m <= 0 || m == rc.m || (size_t)m >= db.materials().size()) continue; const uint32_t f = matTable[(size_t)m * 12 + 1]; - if ((f & F_STATIC) && !(f & (F_LIQUID | F_SAND | F_GAS | F_FIRE))) { held = true; break; } + // Rock and snow are static liquid/powder cells too. + // They support ice just as they support rigid-body fixtures. + if ((f & F_STATIC) && !(f & (F_GAS | F_FIRE))) { held = true; break; } } if (held) break; } if (std::getenv("NG_LOOSELOG") && held) std::printf("loose cells: %zu cells stay, held by the ground%c", region.size(), 10); - if (!held && looseBody(region, conv)) converted = true; + if (!held) looseBody(region, conv); } } - while (freeBodies.size() > kMaxFreeBodies) { // the oldest free body goes (when its slot still holds it: not reused, not broken or burnt away since) - const auto [os, ser] = freeBodies.front(); - freeBodies.pop_front(); - if (freeBodySerial[os] != ser || slotToId[os] || freeBodyOrigin[os] == 0) continue; - bool live = false; - for (int kb = 0; kb < rigid.count() && !live; ++kb) live = rigid.bodies()[kb].i[1] == os && !(rigid.bodies()[kb].i[2] & 2); - if (!live) continue; - rigid.remove_slot(os); hideSlot(os); freeSlots.push_back(os); freeBodyOrigin[os] = 0; - } - if (converted || frame - convWatchFrame >= 30 || convWatchVX != viewX || convWatchVY != viewY) { // the watch follows the window - convWatch.clear(); convWatchFrame = frame; convWatchVX = viewX; convWatchVY = viewY; + { // Track new ice every tick: it can form and lose a pixel before the old 30-tick refresh. + convWatch.clear(); for (int i = 0; i < kGridW * kGridH; ++i) { const uint32_t m = cellsReadPtr[i] & 0xFFFFu; if (m && m < convertTo.size() && convertTo[m]) convWatch.push_back({cellsViewX + i % kGridW, cellsViewY + i / kGridW, (int)m}); } } }; @@ -4313,7 +4454,13 @@ int main(int argc, char** argv) { if (k > 1) break; } if (shotFrames >= 0 && std::getenv("NG_BURNLOG") && (nev > 0 || ctrlPtr->inp2[0] % 100 == 0)) std::printf("frame %d: %d burnt pixels reported%c", ctrlPtr->inp2[0], ctrlPtr->burnN[0], 10); - for (int e = 0; e < nev; ++e) { const int bk = ctrlPtr->burnEv[e * 4]; burntPending[bk].insert(ctrlPtr->burnEv[e * 4 + 1]); burntSince.emplace(bk, ctrlPtr->inp2[0]); } + for (int e = 0; e < nev; ++e) { + const int bk = ctrlPtr->burnEv[e * 4]; + if (bk < 0 || bk >= rigid.count()) continue; + burntPending[bk].insert(ctrlPtr->burnEv[e * 4 + 1]); + burntSince.emplace(bk, ctrlPtr->inp2[0]); + if (ctrlPtr->burnEv[e * 4 + 2] != 0) burntSince[bk] = -1000; // material change: rebuild immediately + } ctrlPtr->burnN[0] = 0; std::vector> bodyModified; // entities whose body lost pixels (burnt, blasted): Message_PhysicsBodyModified -> script_physics_body_modified, after the loop for (auto itp = burntPending.begin(); itp != burntPending.end();) { @@ -4397,8 +4544,12 @@ int main(int argc, char** argv) { const double ang = es.vel[0], ox = es.pos[0], oy = es.pos[1]; const double vx = rb.v[0], vy = rb.v[1], om = rb.p[3], oldX = rb.p[0], oldY = rb.p[1]; const float burnLeft = rb.m[2]; + const bool continuous = (rb.i[2] & ng::GpuBodies::kContinuousCollision) != 0; + const auto oldMaterial = bodyMaterial.find(k); + const int fractureMaterial = oldMaterial != bodyMaterial.end() ? oldMaterial->second : -1; const int pid = slotToId[slot]; g.copy_buf(g.burnPx, (VkDeviceSize)k * 16384, g.burnPx, (VkDeviceSize)ng::GpuBodies::kMaxBodies * 16384, 16384); // the old state, kept while the body index may be reused + const auto fractureLifecycle = physSys.lifecycle(k); rigid.remove_slot(slot); // the entity stays with the largest island (009bc960: the retained body keeps its entity, its components and its slot; the other islands are new bodies // without an entity, here tagged with it: they go when it dies). The main body of a prop of several bodies (the floating fungus, the cart) keeps its parts @@ -4451,6 +4602,14 @@ int main(int argc, char** argv) { entReadPtr[ns] = L.ents[ns]; makeRigid(ns); } + // makeRigid clears material state while constructing a fresh body. + // Fragments retain the source material and contact response. + std::memcpy(L.ents[ns].ex5, es.ex5, 12); + std::memcpy(entReadPtr[ns].ex5, es.ex5, 12); + patchEntity(ns, offsetof(ng::EntityState, ex5), es.ex5, 12); + const float fragmentAngle = (float)ang; + L.ents[ns].vel[0] = fragmentAngle; entReadPtr[ns].vel[0] = fragmentAngle; + patchEntity(ns, offsetof(ng::EntityState, vel), &fragmentAngle, sizeof fragmentAngle); for (int kb = 0; kb < rigid.count(); ++kb) { ng::GpuBody& nb = rigid.bodies()[kb]; if (nb.i[1] != ns || (nb.i[2] & 2)) continue; @@ -4460,8 +4619,16 @@ int main(int argc, char** argv) { nb.p[0] = (float)(ox - (cs * cx0 - sn * cy0)); nb.p[1] = (float)(oy - (sn * cx0 + cs * cy0)); nb.p[2] = (float)ang; nb.l[0] = nb.p[0]; nb.l[1] = nb.p[1]; nb.l[2] = nb.p[2]; double fvx = vx, fvy = vy; - if (ci == largest) { fvx += -om * (nb.p[1] - oldY); fvy += om * (nb.p[0] - oldX); } // v += omega x (new centre - old centre) + fvx += -om * (nb.p[1] - oldY); fvy += om * (nb.p[0] - oldX); // every fragment inherits v + omega x (new centre - old centre) + rigid.inherit_material(kb, rb); + if (fractureMaterial >= 0) setBodyMaterial(kb, &db.materials()[fractureMaterial], (int)isl.size()); + physSys.set_lifecycle(kb, fractureLifecycle); rigid.set_motion(kb, fvx, fvy, om); + rigid.set_bullet(kb, continuous); + if (cpuPhys) { + const double state[] = {nb.p[0] / 10.0, nb.p[1] / 10.0, ang, fvx * 6.0, fvy * 6.0, om * 60.0}; + physSys.set_body_state(kb, state, 6); + } rigid.set_burning(kb, burnLeft); if (keepEnt && ci != largest) nb.d[3] = (float)(slot + 1); // (a piece of the entity: it goes with it) g.copy_buf(g.burnPx, (VkDeviceSize)ng::GpuBodies::kMaxBodies * 16384, g.burnPx, (VkDeviceSize)kb * 16384, 16384); @@ -4981,23 +5148,26 @@ int main(int argc, char** argv) { const int W = aTabV.size() >= 8 ? (int)aTabV[2] : 0, H = aTabV.size() >= 8 ? (int)aTabV[3] : 0; return W && tx >= 0 && ty >= 0 && tx < W && ty < H && aTabV[aTabV[1] + (size_t)ty * W + tx] != 0; }; - auto pendingSceneNear = [&](int ix, int iy) { // 008833e0: a pixel scene still waiting for its chunks (one of the tiles it covers is not loaded) overlaps x +- 15, y +- 15 + auto pendingSceneBox = [&](double x0, double y0, double x1, double y1) { + // 008833e0: pending pixel-scene rectangles are tested against the + // entire queried AABB, not just its origin or current scan column. if (!areaStore) return false; - const int ttx = (int)std::floor(ix / 512.0) + 35, tty = (int)std::floor(iy / 512.0) + 14; - for (int dy = -1; dy <= 1; ++dy) - for (int dx = -1; dx <= 1; ++dx) { - if (!tileLoaded(ttx + dx, tty + dy)) continue; - const ng::AreaSetup* as = areaStore->area(ttx + dx, tty + dy); - if (!as) continue; - for (const auto& r : as->scene_rects) { - if (r[2] * r[3] == 0 || !(r[0] <= ix + 15 && r[1] <= iy + 15 && ix - 15 <= r[0] + r[2] && iy - 15 <= r[1] + r[3])) continue; - for (int sy = (int)std::floor(r[1] / 512.0); sy <= (int)std::floor((r[1] + r[3] - 1) / 512.0); ++sy) - for (int sx = (int)std::floor(r[0] / 512.0); sx <= (int)std::floor((r[0] + r[2] - 1) / 512.0); ++sx) - if (!tileLoaded(sx + 35, sy + 14)) return true; - } + const int tx0 = (int)std::floor(x0 / 512.0) + 35, tx1 = (int)std::floor(x1 / 512.0) + 35; + const int ty0 = (int)std::floor(y0 / 512.0) + 14, ty1 = (int)std::floor(y1 / 512.0) + 14; + for (int ty = ty0 - 1; ty <= ty1 + 1; ++ty) for (int tx = tx0 - 1; tx <= tx1 + 1; ++tx) { + if (!tileLoaded(tx, ty)) continue; + const ng::AreaSetup* as = areaStore->area(tx, ty); + if (!as) continue; + for (const auto& r : as->scene_rects) { + if (r[2] * r[3] == 0 || r[0] > x1 || r[1] > y1 || x0 > r[0] + r[2] || y0 > r[1] + r[3]) continue; + for (int sy = (int)std::floor(r[1] / 512.0); sy <= (int)std::floor((r[1] + r[3] - 1) / 512.0); ++sy) + for (int sx = (int)std::floor(r[0] / 512.0); sx <= (int)std::floor((r[0] + r[2] - 1) / 512.0); ++sx) + if (!tileLoaded(sx + 35, sy + 14)) return true; } + } return false; }; + auto pendingSceneNear = [&](int ix, int iy) { return pendingSceneBox(ix - 15, iy - 15, ix + 15, iy + 15); }; auto holyMountain = [&](const ng::BiomeDef* bd) { // the biome's name has "holymountain" in it ($biome_holymountain: the temple_altar* and solid_wall_temple biomes) return bd && (bd->name.rfind("temple_altar", 0) == 0 || bd->name == "solid_wall_temple"); }; @@ -5590,8 +5760,10 @@ int main(int argc, char** argv) { if (ownHerd >= 0 && e.ex8[1] - 1 == ownHerd) continue; const float m = damageTypeMult(e, dt); double amt = dpf; - if (e.ex8[0] & kAboutToBeScript) { int crit = 0; amt = rt->run_damage_about_to_be_received(vid, dpf, e.pos[0], e.pos[1], 0, crit); hpTake(i, amt * m); } // (the scripts first, on the damage before the multiplier) - else { const int32_t milli = entReadPtr[i].x[0] + (int32_t)std::lround(dpf * m * 1000.0); entReadPtr[i].x[0] = milli; patchEntity(i, offsetof(ng::EntityState, x), &milli, 4); } + if (e.ex8[0] & kAboutToBeScript) { int crit = 0; amt = rt->run_damage_about_to_be_received(vid, dpf, e.pos[0], e.pos[1], 0, crit); } // Scripts precede the type multiplier and predicted-fatality check. + hitLiquidHost(i, dt == "DAMAGE_HEALING" ? -float(std::abs(amt * m)) : float(amt * m), e.pos[0], e.pos[1], 0, 0, 1); + if (e.ex8[0] & kAboutToBeScript) hpTake(i, amt * m); + else { const int32_t milli = entReadPtr[i].x[0] + (int32_t)std::lround(amt * m * 1000.0); entReadPtr[i].x[0] = milli; patchEntity(i, offsetof(ng::EntityState, x), &milli, 4); } if (dt == "DAMAGE_CURSE" && amt * m > 0.0) for (int lc : rt->components(vid, "AudioLoopComponent", "sound_damage_curse", false)) audioLoopPlay(lc, 1.0f, 1.0f); // 00ab9a90: curse damage keeps the victim's "sound_damage_curse" loop going } } @@ -5832,6 +6004,11 @@ int main(int argc, char** argv) { const float mult[12] = {e.ex9[3], e.ex12[0], e.ex9[2], e.ex12[3], e.ex12[2], e.ex12[1], e.ex13[2], e.ex13[3], e.ex13[0], e.ex14[1], e.ex14[2], e.ex9[1]}; double dmg = f[4] * e.ex9[0]; for (int c = 0; c < 12; ++c) dmg += pd->dmg_type[c] * mult[c]; + auto liquid = [&](float amount) { hitLiquidHost(i, amount, e.pos[0], e.pos[1], f[2] * 60.0f * pd->ragdoll_force_multiplier, + f[3] * 60.0f * pd->ragdoll_force_multiplier, pd->ragdoll_fx, pd->blood_count_multiplier, + ng::projectile_hit_liquid_speed(pd->ragdoll_force_multiplier)); }; + liquid(f[4] * e.ex9[0]); + for (int c : {1, 11, 2, 0, 5, 4, 3, 6, 7, 9, 10}) liquid(pd->dmg_type[c] * mult[c]); if (dmg != 0.0) { const int32_t milli = entReadPtr[i].x[0] + (int32_t)std::lround(dmg * 1000.0); entReadPtr[i].x[0] = milli; patchEntity(i, offsetof(ng::EntityState, x), &milli, 4); } } hostHits.push_back({(float)i, (float)sslot, -1.0f, (float)(d[0] + 1)}); @@ -6343,6 +6520,20 @@ int main(int argc, char** argv) { } int popHover = -1; // the slot box under the mouse in the new-wand choice (-2: Cancel) int popClickSeen = 0; + auto captureRigidDebug = [&](int sampleFrame) { + std::vector rows; + for (const auto& b : physSys.debug_bodies()) { + ng::RigidDebug::Row row; row.b = b; row.lifecycle = physSys.lifecycle(b.index); + if (b.index < rigid.count()) { + const auto& gb = rigid.bodies()[b.index]; row.slot = gb.i[1]; row.flags = gb.i[2]; + if (auto it = bodyMaterial.find(b.index); it != bodyMaterial.end()) row.material = it->second; + if (row.slot >= 0 && row.slot < kMaxEnts) row.entity = slotToId[row.slot]; + if (rt) if (const auto* e = rt->ent(row.entity)) row.file = e->file; + } + rows.push_back(std::move(row)); + } + rigidDebug.append(sampleFrame, std::move(rows)); + }; while (!g_in.quit) { perf.begin(frame); PERFMARK("window messages"); @@ -6571,6 +6762,7 @@ int main(int argc, char** argv) { if (doTick && !scripted && frame > 0) tickAcc = std::min(tickAcc - tickSec, tickSec); // at most one tick of backlog: a slow machine slows the game down const bool simOff = paused || !doTick || popWand != 0; // (the new-wand choice freezes the game; the clip editor too, through doTick) alphaI = scripted ? 1.0 : std::clamp(tickAcc / tickSec, 0.0, 1.0); // how far we are between the last tick and the next + if (const char* renderAlpha = std::getenv("NG_RENDER_ALPHA")) alphaI = std::clamp(std::atof(renderAlpha), 0.0, 1.0); // reproduce live interpolation in frame-limited tests if (runSeconds > 0 && std::chrono::duration(tickNow - runT0).count() > runSeconds) { std::printf("%.1f s: %d frames drawn, %d simulation ticks (%.1f fps, %.1f ticks/s)%c", runSeconds, totalFrames, frame, totalFrames / runSeconds, frame / runSeconds, 10); if (profile && profN) { @@ -6582,6 +6774,58 @@ int main(int argc, char** argv) { runShotNow = true; // (--shot: the last frame of a real-time run) } if (g_in.debug_toggle) { showDebug = !showDebug; g_in.debug_toggle = false; } + if (g_in.collider_toggle) { showColliders = !showColliders; showDebug = true; g_in.collider_toggle = false; } + if (const char* rd = std::getenv("NG_RIGID_DUMP_AT")) if (frame == std::atoi(rd)) g_in.rigid_dump = true; + if (g_in.rigid_dump) { + g_in.rigid_dump = false; showDebug = true; + if (rigidDebug.history.empty() || rigidDebug.history.back().frame != frame - 1) captureRigidDebug(std::max(0, frame - 1)); + const auto stamp = std::chrono::system_clock::now().time_since_epoch(); + const auto dir = std::filesystem::absolute(argv[0]).parent_path() / "debug" / ("rigid_" + std::to_string(std::chrono::duration_cast(stamp).count()) + "_f" + std::to_string(frame)); + const std::string context = "seed=" + std::to_string(worldSeed) + "\nframe=" + std::to_string(frame) + "\nphysics_frame=" + std::to_string(rigidDebug.history.back().frame) + "\nview=" + std::to_string(viewX) + "," + std::to_string(viewY) + "\nfree_camera=" + std::to_string(freeCam) + "\nplayer=" + std::to_string(ctrlPtr->outp[0]) + "," + std::to_string(ctrlPtr->outp[1]); + bool ok = rigidDebug.save(dir, physSys, context); + if (ok) { + std::ofstream f(dir / "allocation.txt"); + f << "shape_epoch=" << rigid.shape_epoch() << "\nshape_words=" << rigid.shape_words_used() << "\nshape_words_capacity=" << ng::GpuBodies::kShapeWords << "\nbody_slots=" << rigid.count() << '\n'; + std::ofstream sprites(dir / "sprites.csv"); + sprites << "slot,entity,origin,kind,flags,x,y,angle,width,height,texture,gpu_body,cpu_body\n"; + std::unordered_map gpuBody, cpuBody; + for (int k = 0; k < rigid.count(); ++k) if (!(rigid.bodies()[k].i[2] & 2)) gpuBody[rigid.bodies()[k].i[1]] = k; + for (const auto& b : physSys.debug_bodies()) cpuBody[b.index] = b.cpu; + if (entReadPtr) for (const auto& si : entBuilder.layer().inst) { + const int s = si.ent; + if (s < 0 || s >= entN || entReadPtr[s].st[0] == 0 || (entReadPtr[s].st[3] & ng::kEntDead)) continue; + const auto& es = entReadPtr[s]; + const int b = gpuBody.count(s) ? gpuBody.at(s) : -1; + sprites << s << ',' << slotToId[s] << ',' << (int)freeBodyOrigin[s] << ',' << es.st[0] << ',' << es.st[3] << ',' << es.pos[0] << ',' << es.pos[1] << ',' << es.vel[0] << ',' << si.idle.w << ',' << si.idle.h << ',' << si.idle.tex << ',' << b << ',' << (cpuBody.count(b) ? cpuBody.at(b) : -1) << '\n'; + } + ok = bool(f && sprites); + } + if (ok && rt) { + std::ofstream f(dir / "entities.csv"); + f << std::setprecision(12) << "entity,file,alive,parked,parent,slot,x,y,angle,gpu_x,gpu_y,gpu_angle\n"; + for (const auto& [id, e] : rt->entities()) { + f << id << ',' << std::quoted(e.file) << ',' << e.alive << ',' << e.parked << ',' << e.parent << ',' << e.gpu << ',' << e.x << ',' << e.y << ',' << e.rot; + if (entReadPtr && e.gpu >= 0 && e.gpu < entN) { const auto& es = entReadPtr[e.gpu]; f << ',' << es.pos[0] << ',' << es.pos[1] << ',' << es.vel[0]; } else f << ",,,"; + f << '\n'; + } + ok = bool(f); + } + if (ok && cellsReadPtr) { + // Include the actual source grid: a contour alone cannot tell + // a stale collider from a real static pixel left by a cut. + std::ofstream f(dir / "cells.bin", std::ios::binary); + const int32_t header[6] = {0x31434252, cellsViewX, cellsViewY, kGridW, kGridH, frame}; // RBC1, little endian + f.write((const char*)header, sizeof header); f.write((const char*)cellsReadPtr, (std::streamsize)kGridW * kGridH * 4); + std::ofstream ages(dir / "terrain_age.bin", std::ios::binary); + ages.write((const char*)header, sizeof header); + ages.write((const char*)(cellsReadPtr + (size_t)kGridW * kGridH), (std::streamsize)kGridW * kGridH * 4); + std::ofstream mats(dir / "materials.csv"); mats << "id,name\n"; + for (const auto& m : db.materials()) mats << m.index << ',' << m.name << '\n'; + ok = bool(f && ages && mats); + } + logLines.push_back(ok ? "Physics dump saved: " + dir.string() : "Physics dump failed: " + dir.string()); logAge.push_back(0); + std::printf("physics dump %s: %s\n", ok ? "saved" : "FAILED", dir.string().c_str()); + } if (g_in.help_toggle) { showHelp = !showHelp; g_in.help_toggle = false; } if (g_in.brush_toggle) { brushMode = !brushMode; g_in.brush_toggle = false; } if (g_in.inv_toggle) { g_in.inv_toggle = false; if (worldMode && hasPlayer && !freeCam && !playerPoly.active) { invOpen = !invOpen; pressItem = 0; invDragging = false; } } @@ -6971,6 +7215,13 @@ int main(int argc, char** argv) { } PERFMARK("logic: portals, test hooks"); if (hasPlayer && frame > 10) { // portals: the material under them decides whether they are open (MaterialAreaChecker), the player steps in (TeleportComponent) + // The exit script removes loaded checkers. Streaming can still + // instantiate an older deferred record afterward: retire it using + // the same run-state condition as the biome's spawn script. + if (gameHost) for (int id : rt->with_tag("temple_areachecker")) { + const auto* checker = rt->ent(id); + if (checker && ng::temple_breach_check_retired(checker->x, checker->y, gameHost->globals)) rt->kill(id); + } rt->material_area_step([&](double x0, double y0, double x1, double y1, const std::string& m1, const std::string& m2, int* total) -> int { const ng::Material* ma = m1.empty() ? nullptr : db.find(m1); const ng::Material* mb2 = m2.empty() ? nullptr : db.find(m2); @@ -6991,6 +7242,8 @@ int main(int argc, char** argv) { } if (!seen) return -1; return match; + }, [&](double x0, double y0, double x1, double y1) { + return cellsReadPtr && !forceRegen && regenWait == 0 && !pendingSceneBox(x0, y0, x1, y1); }); if (teleportCooldown > 0) --teleportCooldown; if (shotFrames >= 0 && std::getenv("NG_PORTALS") && frame % 60 == 0) @@ -7343,7 +7596,7 @@ int main(int argc, char** argv) { // With chunk_probability (while mChunkCount < chunk_count, -1: no limit) a ray within chunk_max_angle (00ba5940: 1 px steps to the first cell a chunk may take, // 00ba58b0: a static powder or a static solid, durability <= max_durability) places a random collapse image centred on the hit; its cells leave the grid and // become one rigid body of chunk_material (0: concrete_collapsed) with their colours (00ba6330), a body that hurts every entity (record +0x30, see the physics hits on - // creatures). (Not done: 00ba6a00 on the chunk's edges.) + // creatures). 00ba6a00 removes the four-neighbour border outside the collapse image so the cut body clears the surrounding terrain. static std::map collapseImgs; auto cellIdx = [&](int wx, int wy) -> int { const int gx = wx - viewX, gy = wy - viewY; return gx < 0 || gy < 0 || gx >= kGridW || gy >= kGridH ? -1 : gy * kGridW + gx; }; auto chunkable = [&](uint32_t m, double maxDur) { // 00ba58b0 @@ -7384,8 +7637,7 @@ int main(int argc, char** argv) { if (twin >= 0 && ng::attr_float(db.materials()[hm], "durability", 0.0f) <= maxDur) for (int oy = -rmax; oy < rmax; ++oy) for (int ox = -rmax; ox < rmax; ++ox) { const int d2 = ox * ox + oy * oy; - double an = std::atan2((double)oy, (double)ox); if (an < 0) an += 6.283185307179586; - const int sec = std::clamp((int)(an / 6.283185307179586 * 32.0), 0, 31); + const int sec = ng::phys::loose_ground_sector(ox, oy); if (d2 > radii[sec]) continue; const int ix = cellIdx(hx + ox, hy + oy); if (ix < 0 || (cellsReadPtr[ix] & 0xFFFFu) != hm) continue; @@ -7394,15 +7646,12 @@ int main(int argc, char** argv) { } } const int cc = (int)num("chunk_count", -1), made = (int)num("mChunkCount", 0); - if ((cc == -1 || made < cc) && frand() < num("chunk_probability", 0)) { // a chunk + if (rigid.has_body_capacity() && (cc == -1 || made < cc) && frand() < num("chunk_probability", 0)) { // a chunk const double a = (frand() * 2.0 - 1.0) * num("chunk_max_angle", 0.7); const double dx = dist * std::sin(a), dy = -dist * std::cos(a); int hx = 0, hy = 0; bool hit = false; - for (double t = 1.0; t <= dist && !hit; t += 1.0) { - const int wx = (int)std::floor(le->x + dx * t / dist), wy = (int)std::floor(le->y + dy * t / dist), ix = cellIdx(wx, wy); - if (ix < 0) break; - if (chunkable(cellsReadPtr[ix] & 0xFFFFu, maxDur)) { hit = true; hx = wx; hy = wy; } - } + hit = ng::phys::loose_ground_ray((float)le->x, (float)le->y, (float)(le->x + dx), (float)(le->y + dy), hx, hy, + [&](int wx, int wy) { const int ix = cellIdx(wx, wy); return ix >= 0 && chunkable(cellsReadPtr[ix] & 0xFFFFu, maxDur); }); if (!hit) continue; std::string pat = at["collapse_images"]; // "dir/$[0-14].png": a random number in the range const size_t b0 = pat.find("$["), b1 = b0 == std::string::npos ? b0 : pat.find(']', b0); @@ -7416,6 +7665,8 @@ int main(int argc, char** argv) { if (!img.ok() || img.w > 64 || img.h > 64) { if (std::getenv("NG_SCRIPTLOG")) std::printf("loose ground: image %s %dx%d not usable%c", pat.c_str(), img.w, img.h, 10); continue; } std::string cmn = at["chunk_material"]; const ng::Material* cm = (cmn.empty() || cmn == "0") ? db.find("concrete_collapsed") : (std::isdigit((unsigned char)cmn[0]) ? (std::atoi(cmn.c_str()) < (int)db.materials().size() ? &db.materials()[std::atoi(cmn.c_str())] : nullptr) : db.find(cmn)); + if (!rigid.has_shape_capacity(img.w, img.h) || entBuilder.layer().texels.size() + (size_t)img.w * img.h > entTexCapacity || + (freeSlots.empty() && (entBuilder.layer().ents.size() >= (size_t)kMaxEnts || entBuilder.layer().inst.size() >= (size_t)kMaxInst))) continue; const int x0 = hx - img.w / 2, y0 = hy - img.h / 2; std::vector tex((size_t)img.w * img.h, 0u); int taken = 0; @@ -7434,6 +7685,27 @@ int main(int argc, char** argv) { ++taken; } if (taken < 2) continue; + // 00ba6330 / 00ba6a00: loosen each occupied image pixel's + // neighbours outside the image mask, including past its bounds. + // Without this clearance the new polygon starts against the + // remaining wall and can become wedged inside its contour. + for (int py = 0; py < img.h; ++py) for (int px = 0; px < img.w; ++px) { + if (!(img.px[(size_t)py * img.w + px] >> 24)) continue; + const int neighbours[4][2] = {{px - 1, py}, {px + 1, py}, {px, py - 1}, {px, py + 1}}; + for (const auto& n : neighbours) { + if (n[0] >= 0 && n[1] >= 0 && n[0] < img.w && n[1] < img.h && + (img.px[(size_t)n[1] * img.w + n[0]] >> 24)) continue; + const int wx = x0 + n[0], wy = y0 + n[1], ix = cellIdx(wx, wy); + if (ix < 0) continue; + const uint32_t m = cellsReadPtr[ix] & 0xFFFFu; + if (!m || m >= db.materials().size()) continue; + setCell(ix, 0u); + const ng::Material& edgeMat = db.materials()[m]; + emitPart(wx + 0.5, wy + 0.5, (frand() * 80.0 - 40.0) / 60.0, (frand() * 80.0 - 40.0) / 60.0, + 300.0 + frand() * 300.0, 100.0 / 3600.0, + (edgeMat.graphics_color ? edgeMat.graphics_color : edgeMat.wang_color) & 0xFFFFFFu, 4, (int)m, true, true); + } + } ng::EntityLayer& L = entBuilder.layer(); int reuse = -1; if (!freeSlots.empty()) { reuse = freeSlots.back(); freeSlots.pop_back(); } @@ -7455,7 +7727,12 @@ int main(int argc, char** argv) { makeRigid(ns); for (int kb = 0; kb < rigid.count(); ++kb) { if (rigid.bodies()[kb].i[1] != ns || (rigid.bodies()[kb].i[2] & 2)) continue; - if (cm) { rigid.set_surface(kb, ng::attr_float(*cm, "solid_friction", 0.3f), ng::attr_float(*cm, "solid_restitution", 0.2f)); rigid.set_density(kb, ng::attr_float(*cm, "density", 1.0f)); setCollAudio(kb, cm, taken); } + // Thin collapse fragments can cross another moving body in + // one discrete step; include dynamic pairs in the TOI sweep. + rigid.set_bullet(kb, !std::getenv("NG_DISCRETE_DEBRIS")); + physSys.set_settle_static(kb, true); + rigid.set_material_reactions(kb, false); + if (cm) { rigid.set_surface(kb, ng::attr_float(*cm, "solid_friction", 0.3f), ng::attr_float(*cm, "solid_restitution", 0.2f)); rigid.set_density(kb, ng::attr_float(*cm, "density", 1.0f)); setBodyMaterial(kb, cm, taken); } } at["mChunkCount"] = std::to_string(made + 1); hurtAnySlot[(size_t)ns] = 1; // 00ba6330: the chunk's body record gets +0x2e = 0, +0x30 = +0x31 = 1 - it hurts every entity, the player too (00ab6010) @@ -7581,6 +7858,7 @@ int main(int argc, char** argv) { if ((e2.pos[0] - nx) * (e2.pos[0] - nx) + (e2.pos[1] - ny) * (e2.pos[1] - ny) >= kr * kr) continue; const int vid = i == 0 ? rt->player() : slotToId[i]; if (!vid || !rt->has_tag(vid, "mortal")) continue; + hitLiquidHost(i, float(dn("bite_damage", 2) * damageTypeMult(e2, "DAMAGE_MELEE")), entReadPtr[i].pos[0], entReadPtr[i].pos[1], 0, 0, 1); const int32_t milli = entReadPtr[i].x[0] + (int32_t)std::lround(dn("bite_damage", 2) * damageTypeMult(e2, "DAMAGE_MELEE") * 1000.0); entReadPtr[i].x[0] = milli; patchEntity(i, offsetof(ng::EntityState, x), &milli, 4); bit = true; @@ -7753,6 +8031,7 @@ int main(int argc, char** argv) { const int vid = i == 0 ? rt->player() : slotToId[i]; if (!vid || !rt->has_tag(vid, "mortal")) continue; const float m = damageTypeMult(e2, "DAMAGE_MELEE"); + hitLiquidHost(i, float(wn("bite_damage", 1) * m), e2.pos[0], e2.pos[1], 0, 0, 1); const int32_t milli = entReadPtr[i].x[0] + (int32_t)std::lround(wn("bite_damage", 1) * m * 1000.0); entReadPtr[i].x[0] = milli; patchEntity(i, offsetof(ng::EntityState, x), &milli, 4); const int32_t ty = 8; patchEntity(i, offsetof(ng::EntityState, ex3) + 8, &ty, 4); // (the melee type of the hit: its sounds) @@ -8554,6 +8833,7 @@ int main(int argc, char** argv) { if (c.meleeHitAt >= 0 && frame >= c.meleeHitAt) { if (c.prey >= 0 && !(entReadPtr[c.prey].st[3] & ng::kEntDead) && preyDist() <= reach) { const ng::EntityState& t = entReadPtr[c.prey]; + hitLiquidHost(c.prey, c.meleeDmg * damageTypeMult(t, "DAMAGE_MELEE"), t.pos[0], t.pos[1], 0, 0, 1); const int32_t milli = t.x[0] + (int32_t)std::lround(c.meleeDmg * damageTypeMult(t, "DAMAGE_MELEE") * 1000.0); entReadPtr[c.prey].x[0] = milli; patchEntity(c.prey, offsetof(ng::EntityState, x), &milli, 4); const int32_t ty = 8; patchEntity(c.prey, offsetof(ng::EntityState, ex3) + 8, &ty, 4); @@ -8680,6 +8960,7 @@ int main(int argc, char** argv) { } } if (frame < c.dashUntil && !c.dashHit && td < (float)an("attack_melee_max_distance", 20)) { // (in the dash state the prey within melee reach takes the melee: 00a26090) + hitLiquidHost(c.prey, float(an("attack_dash_damage", 0.25) * damageTypeMult(t, "DAMAGE_MELEE")), entReadPtr[c.prey].pos[0], entReadPtr[c.prey].pos[1], 0, 0, 1); const int32_t milli = entReadPtr[c.prey].x[0] + (int32_t)std::lround(an("attack_dash_damage", 0.25) * damageTypeMult(t, "DAMAGE_MELEE") * 1000.0); entReadPtr[c.prey].x[0] = milli; patchEntity(c.prey, offsetof(ng::EntityState, x), &milli, 4); const int32_t ty = 8; patchEntity(c.prey, offsetof(ng::EntityState, ex3) + 8, &ty, 4); @@ -8707,6 +8988,7 @@ int main(int argc, char** argv) { } } if (frame < c.dashUntil && !c.dashHit && td < (float)an("attack_melee_max_distance", 20)) { + hitLiquidHost(c.prey, float(an("attack_dash_damage", 0.25) * damageTypeMult(t, "DAMAGE_MELEE")), entReadPtr[c.prey].pos[0], entReadPtr[c.prey].pos[1], 0, 0, 1); const int32_t milli = entReadPtr[c.prey].x[0] + (int32_t)std::lround(an("attack_dash_damage", 0.25) * damageTypeMult(t, "DAMAGE_MELEE") * 1000.0); entReadPtr[c.prey].x[0] = milli; patchEntity(c.prey, offsetof(ng::EntityState, x), &milli, 4); const int32_t ty = 8; patchEntity(c.prey, offsetof(ng::EntityState, ex3) + 8, &ty, 4); @@ -9246,6 +9528,7 @@ int main(int argc, char** argv) { if (std::hypot(hx - L.ex, hy - L.ey) > 3.5f) continue; const int vid = i == 0 ? rt->player() : slotToId[i]; if (!vid || !rt->has_tag(vid, "mortal")) continue; + hitLiquidHost(i, float(0.5 * damageTypeMult(e2, "DAMAGE_MELEE")), entReadPtr[i].pos[0], entReadPtr[i].pos[1], 0, 0, 1); const int32_t milli = entReadPtr[i].x[0] + (int32_t)std::lround(0.5 * damageTypeMult(e2, "DAMAGE_MELEE") * 1000.0); entReadPtr[i].x[0] = milli; patchEntity(i, offsetof(ng::EntityState, x), &milli, 4); const int32_t ty = 8; patchEntity(i, offsetof(ng::EntityState, ex3) + 8, &ty, 4); @@ -9784,6 +10067,8 @@ int main(int argc, char** argv) { if (kv.first == "parent_hotspot_tag" && !kv.second.empty()) hotspotWorld(owner, kv.second, bx, by); } for (int pc : rt->components(effEnt, "ParticleEmitterComponent", "", false)) { + const int held = invp ? (activeItem >= 0 ? invp->items[activeItem] : invp->wands[activeWand]) : 0; + if (!rt->item_emitter_enabled(pc, held)) continue; auto& pa = rt->comp(pc)->attrs; auto num = [&](const char* k, double d) { auto it = pa.find(k); return it == pa.end() || it->second.empty() ? d : std::atof(it->second.c_str()); }; if (pa["is_emitting"] == "0" || pa["is_emitting"] == "false") continue; @@ -10192,6 +10477,20 @@ int main(int argc, char** argv) { const int pid2 = rt->spawn(tf ? tf : "data/entities/projectiles/ice.xml", sx0, sy0); if (pid2) rt->on_shoot(rt->player(), sx0, sy0, sx0 + tdx, sy0 + tdy, pid2, true); } + // EntityLoad alone also creates a live projectile (chest bombs do not call + // GameShootProjectile). Explicit shots have already left this deferred set. + if (psim && !deferredProjectileSlots.empty()) { + const auto deferred = deferredProjectileSlots; + for (int id : deferred) if (const auto* e = rt->ent(id); e && e->alive && e->gpu < 0 && !findTwin(id) && !rt->components(id, "ProjectileComponent", "", false).empty()) { + double vx = 0, vy = 0; + for (int cid : rt->components(id, "VelocityComponent", "", false)) { + vx = std::atof(rt->comp(cid)->attrs["mVelocity.x"].c_str()); + vy = std::atof(rt->comp(cid)->attrs["mVelocity.y"].c_str()); + break; + } + rt->on_shoot(0, e->x, e->y, e->x + vx, e->y + vy, id, false); + } + } for (const ScriptShot& ss : pendingShots) { // the shots of scripts: speed from the aim vector (the scripts pass the target as position + velocity, in pixels per second), the shooter is credited with the hits if (ss.entity && !rt->alive(ss.entity)) continue; std::string file = ss.file; @@ -10825,6 +11124,7 @@ int main(int argc, char** argv) { return m != 0 && (matTable[(size_t)m * 12 + 1] & (F_STATIC | F_SAND)) != 0; }; auto damageSlot = [&](int slot, double dmg) { + hitLiquidHost(slot, float(dmg), entReadPtr[slot].pos[0], entReadPtr[slot].pos[1], 0, 0, 1); const int32_t milli = entReadPtr[slot].x[0] + (int32_t)std::lround(dmg * 1000.0); entReadPtr[slot].x[0] = milli; patchEntity(slot, offsetof(ng::EntityState, x), &milli, 4); @@ -10911,33 +11211,6 @@ int main(int argc, char** argv) { if (std::getenv("NG_DMGLOG")) std::printf("frame %d: BLOCK on entity %d (slot %d)%c", frame, id, i, 10); if (dmgNums.size() < 256) { DmgNum d{id, ce.pos[0] + (frand() * 10.0 - 5.0), ce.pos[1] + (frand() * 10.0 - 5.0), 0, 0.0, true}; d.x0 = d.x; d.y0 = d.y; dmgNums.push_back(d); } } - // the liquid of a hit on a prop (TakeDamage_Impl 00ab9a90, as hit_blood in entities.comp does for the creatures): N = trunc((u x 20 + 20) x blood_multiplier + 0.5) - // real particles of blood_material (default blood_fading, "" none), whatever the damage; each droplet turned by -0.6..0.6 rad from the one before, 0.5..1.25 x the - // base speed (1.2 cells per tick, the placeholder of the creatures), from the hit point +-1.5 (here the body). The lantern's fatal landing throws its oil this way - static std::map propBledAt; - // TODO(to-do #39): the engine sprays a prop's blood like a creature's (00ab9a90: from the hit point along the packet impulse x 20 x (0.5..1.25); an Energy Sphere's impulse is 0) - auto propBleed = [&](int id, double x, double y) { - if (!rt || propBledAt[id] == frame) return; - propBledAt[id] = frame; - const auto dcs = rt->components(id, "DamageModelComponent", "", true); - if (dcs.empty()) return; - auto& da = rt->comp(dcs.front())->attrs; - const auto bmIt = da.find("blood_material"); - const std::string bm = bmIt == da.end() ? std::string("blood_fading") : bmIt->second; - const ng::Material* om = bm.empty() ? nullptr : db.find(bm); - if (!om) return; - const auto mIt = da.find("blood_multiplier"); - const double mult = mIt == da.end() || mIt->second.empty() ? 1.0 : std::atof(mIt->second.c_str()); - const int n = (int)((frand() * 20.0 + 20.0) * mult + 0.5); - const uint32_t orgb = (om->graphics_color ? om->graphics_color : om->wang_color) & 0xFFFFFFu; - double vx = 0.0, vy = -1.0; - for (int k = 0; k < std::min(n, 64); ++k) { - const double sp = 0.5 + 0.75 * frand(), a = -0.6 + 1.2 * frand(), c = std::cos(a), s = std::sin(a); - const double nx = vx * c - vy * s, ny = vx * s + vy * c; vx = nx; vy = ny; - emitPart(x - 1.5 + 3.0 * frand(), y - 1.5 + 3.0 * frand(), vx * 1.2 * sp, vy * 1.2 * sp, 70.0 + 40.0 * frand(), 100.0 / 3600.0, orgb, 1, om->index); - } - if (std::getenv("NG_SCRIPTLOG")) std::printf("frame %d: prop %d bleeds %d x %s%c", frame, id, std::min(n, 64), bm.c_str(), 10); - }; for (int i = 0; i < entN; ++i) { // the damage numbers: the hp of an entity that reports damage went down since the last tick const ng::EntityState& ce = entReadPtr[i]; const int id = i == 0 ? rt->player() : slotToId[i]; @@ -10946,7 +11219,6 @@ int main(int argc, char** argv) { const float before = hpRep[i], now = ce.hp[0]; hpRep[i] = now; if (!(now < before - 1e-6f) || before > 1.0e8f) continue; - if (ce.st[0] == 4) propBleed(id, ce.pos[0], ce.pos[1]); auto rd = reportsDamage.find(id); if (rd == reportsDamage.end()) { bool on = false; @@ -11075,7 +11347,6 @@ int main(int argc, char** argv) { if (!pe || pe->tags.count("item")) continue; const double bx = entReadPtr[i].pos[0], by = entReadPtr[i].pos[1]; // (the entity's position: its explosion, spill and scripts) pe->x = bx; pe->y = by; - if (entReadPtr[i].hp[0] <= 0.0f) propBleed(pid, bx, by); // (the fatal hit bleeds too) const int propKiller = slotAttacker(entReadPtr[i], 30, frame); curAttacker = 0; for (int sk = 0; sk < entN && propKiller; ++sk) if (slotToId[sk] == propKiller) { curAttacker = sk + 1; break; } @@ -11272,7 +11543,7 @@ int main(int argc, char** argv) { if (!ee || !ee->alive || ee->parent) continue; // (a wand in a hand or an inventory has a parent: its rays are off) if (ee->x < viewX - 64 || ee->x > viewX + kGridW + 64 || ee->y < viewY - 64 || ee->y > viewY + kGridH + 64) continue; for (int sc : rt->components(eid, "SpriteParticleEmitterComponent", "", false)) { - if (!rt->component_enabled(sc) || sprParts.size() > 2000) continue; + if (!rt->item_emitter_enabled(sc, 0) || sprParts.size() > 2000) continue; auto& a = rt->comp(sc)->attrs; if (a["is_emitting"] == "0" || a["is_emitting"] == "false") continue; auto n = [&](const char* key, double d) { const auto f = a.find(key); return f == a.end() || f->second.empty() ? d : std::atof(f->second.c_str()); }; @@ -12226,6 +12497,26 @@ int main(int argc, char** argv) { PERFMARK("HUD drawing"); if (playerPoly.active) { hs.wands.clear(); hs.items.clear(); hs.spell_icons.clear(); hs.spell_uses.clear(); hs.active_wand = -1; hs.active_slot = -1; } // the polymorphed body has no inventory hud::draw(canvas, hudAssets, hs); + if (showDebug && showColliders) { + PERFMARK("collider overlay drawing"); + const std::array debugViewport{viewX + (offX - 4) / scalePx, viewY + (offY - 4) / scalePx, + viewX + (offX + canvas.w + 4) / scalePx, viewY + (offY + canvas.h + 4) / scalePx}; + std::unordered_map colors; + for (const auto& b : physSys.debug_bodies()) { + colors[b.index] = b.frozen || !b.active ? 0xFFFF60FFu : b.type == 0 ? 0xFF60A0FFu : b.awake ? 0xFF60FF60u : 0xFFFFFF60u; + const int x = (int)screenOfX(b.x - viewX), y = (int)screenOfY(b.y - viewY); + if (x < -3 || y < -3 || x > canvas.w + 3 || y > canvas.h + 3) continue; + canvas.line(x - 3, y, x + 3, y, colors[b.index]); canvas.line(x, y - 3, x, y + 3, colors[b.index]); + if (x >= 0 && y >= 0 && x < canvas.w && y < canvas.h) canvas.text(x + 4, y, "RB" + std::to_string(b.index), colors[b.index], 1); + } + for (const auto& p : physSys.debug_polygons(&debugViewport)) for (size_t k = 0; k < p.points.size(); ++k) { + const auto& a = p.points[k]; const auto& b = p.points[(k + 1) % p.points.size()]; + canvas.line((int)screenOfX(a[0] - viewX), (int)screenOfY(a[1] - viewY), (int)screenOfX(b[0] - viewX), (int)screenOfY(b[1] - viewY), p.body < 0 ? 0x8860C0FFu : colors[p.body]); + } + for (const auto& c : physSys.debug_contacts()) if (c.separation < -0.1) canvas.rect((int)screenOfX(c.x - viewX) - 2, (int)screenOfY(c.y - viewY) - 2, 4, 4, 0xFFFF3030u); + canvas.text(14, canvas.h - 44, "RB: green awake, yellow asleep, blue static/terrain, magenta inactive; red overlap", 0xFFFFFFFFu, 1); + } + PERFMARK("HUD overlay upload"); if (g_gifOpen) gifEd.draw(canvas); // (over the HUD and the dimmed game) { // the overlay upload: the tile mask, and the drawn tiles whose pixels differ from what the GPU buffer holds (render.comp skips undrawn tiles) if (canvas.tw != ovlTw) { ovlTw = canvas.tw; std::fill(ovlValid.begin(), ovlValid.end(), (uint8_t)0); } // (a new window size: new tiles) @@ -12535,9 +12826,72 @@ int main(int argc, char** argv) { } } } - physSys.step(cellsReadPtr, cellsViewX, cellsViewY, kGridW, kGridH); + physSys.step(cellsReadPtr, cellsViewX, cellsViewY, kGridW, kGridH, cellsReadPtr + (size_t)kGridW * kGridH); + if (showDebug || showColliders || std::getenv("NG_RIGID_DUMP_AT")) { + captureRigidDebug(frame); + } if (std::getenv("NG_PHYSPROF")) std::printf("physics step %d: %.2f ms, %d fixtures%c", frame, std::chrono::duration(std::chrono::steady_clock::now() - physT0).count(), physSys.terrain_fixtures(), 10); rigid.sync_from_physics(); + // 007212b0/00721050: scan only after the grid center moved >512px. + // Persist pixels before releasing a body; sleeping in view never bakes it. + for (int kb : physSys.terrain_conversion_candidates(cellsViewX, cellsViewY, kGridW, kGridH)) { + const auto life = physSys.lifecycle(kb); + const auto& body = rigid.bodies()[kb]; + const int slot = body.i[1], ii = entBuilder.first_instance(slot); + if ((body.i[2] & 2) || ii < 0 || life.target < 1 || life.target >= (int)db.materials().size()) continue; + const auto si = entBuilder.layer().inst[ii]; + std::vector pixels(entBuilder.layer().texels.begin() + si.idle.tex, + entBuilder.layer().texels.begin() + si.idle.tex + (size_t)si.idle.w * si.idle.h); + if (auto lost = burntPending.find(kb); lost != burntPending.end()) + for (int pixel : lost->second) if (pixel >= 0 && pixel < (int)pixels.size()) pixels[pixel] = 0; + const auto pose = physSys.pose(kb); + float comx, comy; + const auto* header = rigid.shape_words() + (size_t)body.i[0] * ng::GpuBodies::kShapeHeaderWords; + std::memcpy(&comx, header + 5, 4); std::memcpy(&comy, header + 6, 4); + const double cs = std::cos(pose.angle), sn = std::sin(pose.angle); + const double ox = pose.x + cs * (body.q[0] - comx) - sn * (body.q[1] - comy); + const double oy = pose.y + sn * (body.q[0] - comx) + cs * (body.q[1] - comy); + auto readCell = [&](int x, int y) -> int { + const int gx = x - cellsViewX, gy = y - cellsViewY; + const uint32_t word = gx >= 0 && gy >= 0 && gx < kGridW && gy < kGridH ? cellsReadPtr[(size_t)gy * kGridW + gx] : store.get(x, y); + return word == kNoCell ? -1 : (int)(word & 0xFFFFu); + }; + bool unloaded = false; + ng::phys::convert_body_cells(pixels.data(), si.idle.w, si.idle.h, body.q[0], body.q[1], ox, oy, pose.angle, + life.material, life.target, [&](int x, int y) { const int m = readCell(x, y); unloaded |= m < 0; return m; }, + [](int, int, int, uint32_t) {}); + if (unloaded) continue; // retain the body when its destination is not resident + int made = 0; + ng::phys::convert_body_cells(pixels.data(), si.idle.w, si.idle.h, body.q[0], body.q[1], ox, oy, pose.angle, + life.material, life.target, readCell, [&](int x, int y, int material, uint32_t color) { + // The cell representation has an 8-bit shade, rather than per-cell RGB. + const uint32_t base = matTable[(size_t)material * 12]; + double dot = 0, norm = 0; + for (int shift : {0, 8, 16}) { const double a = (base >> shift) & 255u, b = (color >> shift) & 255u; dot += a * b; norm += a * a; } + const int shade = (int)std::lround(std::clamp(((norm > 0 ? dot / norm : 1.0) - 1.0) / .18 + .5, 0.0, 1.0) * 255.0); + const uint32_t word = (uint32_t)material | ((uint32_t)shade << 16); + const int gx = x - cellsViewX, gy = y - cellsViewY; + if (gx >= 0 && gy >= 0 && gx < kGridW && gy < kGridH) const_cast(cellsReadPtr)[(size_t)gy * kGridW + gx] = word; + storePut(x, y, word); + const int qx = x - viewX, qy = y - viewY; + if (qx >= 0 && qy >= 0 && qx < kGridW && qy < kGridH) { + const size_t cell = (size_t)qy * kGridW + qx; + queueUp(g.cells, cell * 4, &word, 4); + const float zero[2] = {}; queueUp(g.ext, cell * 8, zero, 8); + } + const int bx = floorDiv64(x), by = floorDiv64(y); + if (auto resident = farOf.find(farKey(bx, by)); resident != farOf.end()) { + const size_t offset = (size_t)resident->second * 4096 + (y & 63) * 64 + (x & 63); + queueUp(g.farCells, offset * 4, &word, 4); + } + ++made; + }); + if (std::getenv("NG_COLLDBG")) std::printf("frame %d: body %d converts %d cells to %s at (%.0f, %.0f)\n", frame, kb, made, db.materials()[life.target].name.c_str(), ox, oy); + rigid.remove_slot(slot); hideSlot(slot); + if (slotToId[slot] && rt) if (auto* entity = rt->ent(slotToId[slot])) entity->gpu = -1; + slotToId[slot] = 0; freeSlots.push_back(slot); + collExplode.erase(kb); collFx.erase(kb); collAudio.erase(kb); bodyMaterial.erase(kb); burntPending.erase(kb); burntSince.erase(kb); + } if (const char* hc = std::getenv("NG_HURTCREATURES")) if (frame == std::atoi(hc)) for (int i = 1; i < entN; ++i) if ((entReadPtr[i].st[0] == 1 || entReadPtr[i].st[0] == 2) && !(entReadPtr[i].st[3] & ng::kEntDead)) { const float hv = entReadPtr[i].hp[0] * 0.5f; entReadPtr[i].hp[0] = hv; patchEntity(i, offsetof(ng::EntityState, hp), &hv, 4); } // test: every creature loses half its hp if (const char* kc = std::getenv("NG_KILLCREATURES")) if (frame == std::atoi(kc)) for (int i = 1; i < entN; ++i) if ((entReadPtr[i].st[0] == 1 || entReadPtr[i].st[0] == 2) && !(entReadPtr[i].st[3] & ng::kEntDead)) { const int32_t lethal = entReadPtr[i].x[0] + 100000; entReadPtr[i].x[0] = lethal; patchEntity(i, offsetof(ng::EntityState, x), &lethal, 4); } // test: every creature takes a lethal hit if (const char* hp = std::getenv("NG_HITPROPS")) if (frame == std::atoi(hp)) for (int k = 0; k < rigid.count(); ++k) if (rigid.bodies()[k].n[0] > 0.0f && !(rigid.bodies()[k].i[2] & 2)) physSys.impacts().push_back({k, rigid.bodies()[k].n[0] * 1.2}); // test: every fragile body takes an impact just over its threshold @@ -12624,6 +12978,7 @@ int main(int argc, char** argv) { if (take > 0.0f && (frame - ph.last > 20 || (double)take >= ce.hp[0])) { ph.last = frame; const float mult = damageTypeMult(ce, "DAMAGE_PHYSICS_HIT"); + hitLiquidHost(s, take * mult, ce.pos[0], ce.pos[1], ph.vx * 2.0f, ph.vy * 2.0f, 1); const int32_t milli = entReadPtr[s].x[0] + (int32_t)std::lround(take * mult * 1000.0); entReadPtr[s].x[0] = milli; patchEntity(s, offsetof(ng::EntityState, x), &milli, 4); const int32_t imp[2] = {(int32_t)std::lround(ph.vx * 2.0f * 1000.0f), (int32_t)std::lround(ph.vy * 2.0f * 1000.0f)}; // the packet's impulse: the velocity x 2 (00ab6d00) @@ -12644,8 +12999,9 @@ int main(int argc, char** argv) { if (fb.n[0] > 0.0f && fb.i[1] >= 0 && !(fb.i[2] & 2) && ce.point_speed > fb.n[0] * 60.0) { const auto mi = collDamageMult.find(ownerBody); const double dmg = (mi == collDamageMult.end() ? 1.0 / 60.0 : mi->second) * ce.point_speed; + hitLiquidHost(fb.i[1], float(dmg), entReadPtr[fb.i[1]].pos[0], entReadPtr[fb.i[1]].pos[1], 0, 0, 1); const int32_t milli = (int32_t)std::lround(dmg * 1000.0); - if (milli >= 90 && entReadPtr[fb.i[1]].ex7[1] > 0.0f) { const int32_t ev[3] = {0, 0, milli}; patchEntity(fb.i[1], offsetof(ng::EntityState, ex8), ev, 12); } // (the leak of a body with liquid, min_damage_to_leak 0.09) + const int32_t total = entReadPtr[fb.i[1]].x[0] + milli; entReadPtr[fb.i[1]].x[0] = total; patchEntity(fb.i[1], offsetof(ng::EntityState, x), &total, 4); @@ -12653,13 +13009,13 @@ int main(int argc, char** argv) { } } const auto ex = collExplode.find(ce.body); - if (ex != collExplode.end() && frame - collExplodeAt[ce.body] > 30) { // 006f7f00: r = min(radius_max, sqrt(0.5 speed mass power)) with the BODY speed in physics units per second; r >= radius_min (a tiny one may be promoted by a random draw) and speed >= velocity_min / 10 + if (ex != collExplode.end()) { // 006f7f00: r = min(radius_max, sqrt(0.5 speed mass power)) with the BODY speed in physics units per second; r >= radius_min (a tiny one may be promoted by a random draw) and speed >= velocity_min / 10 const double sp = std::hypot(ce.vx, ce.vy) * 6.0; double r = std::sqrt(std::max(0.0, 0.5 * sp * ce.mass * ex->second.power)); r = std::min(r, ex->second.rmax); if (std::getenv("NG_COLLDBG")) std::printf("frame %d: collision of body %d: speed %.1f units/s, mass %.2f, r %.1f (min %.1f, power %.2f)%c", frame, ce.body, sp, ce.mass, r, ex->second.rmin, ex->second.power, 10); if (r < ex->second.rmin && frand() < ex->second.prob) r = ex->second.rmin; - if (r >= ex->second.rmin && r > 0.5 && sp >= ex->second.vmin / 10.0) { collExplodeAt[ce.body] = frame; pendingExplosions.push_back({ce.x, ce.y, r, ex->second.cfg}); } + if (r >= ex->second.rmin && sp >= ex->second.vmin / 10.0) { pendingExplosions.push_back({ce.x, ce.y, r, ex->second.cfg}); } } // TODO(to-do #11): crackable terrain cells, FUN_004b0e90 per cracked cell, the splitting draws { // 006f7c80: a body of a material with crackability > 0 (CellData +0x108) cracks where it hits: the body speed (physics units per second, as 006f7f00) @@ -13045,6 +13401,14 @@ int main(int argc, char** argv) { queueUp(g.luaTasks,(VkDeviceSize)ng::gpu_lua::neighbor_header*16,header,sizeof header); if(!neighborFields.empty())queueUp(g.luaTasks,(VkDeviceSize)(ng::gpu_lua::neighbor_header+1)*16,neighborFields.data(),neighborFields.size()*sizeof(neighborFields[0])); } + PERFMARK("hit-liquid policies"); + updateHitLiquidWorld(); + for (int i = 0; i < entN; ++i) hitLiquidPtr->entities[i] = hitLiquidPolicy(i == 0 && rt ? rt->player() : slotToId[i]); + for (int i = 0; i < gfx.type_count(); ++i) if (const ng::ProjDef* d = gfx.type_def(i)) { + hitLiquidPtr->projectile[i][0] = d->blood_count_multiplier; + hitLiquidPtr->projectile[i][1] = ng::projectile_hit_liquid_speed(d->ragdoll_force_multiplier); + } + PERFMARK("queued uploads + simulation recording"); { // the queued uploads go into the frame's command buffer (below), unless they do not fit its staging buffer: then the blocking path, which waited for the // whole queue to go idle - the last frame's render and present included (11 ms a frame at 60 Hz in the spell room, no CPU / GPU overlap anywhere) size_t qb = 0; @@ -13117,6 +13481,7 @@ int main(int argc, char** argv) { const VkBufferCopy c1{0, 0, (VkDeviceSize)kGridW * kGridH * 4}, c2{0, 0, (VkDeviceSize)kGridW * kGridH * 8}; vkCmdCopyBuffer(g.cmd, g.cells.buf, g.cellsOld.buf, 1, &c1); vkCmdCopyBuffer(g.cmd, g.ext.buf, g.extOld.buf, 1, &c2); + vkCmdCopyBuffer(g.cmd, g.terrainAge.buf, g.terrainAgeOld.buf, 1, &c1); vkCmdCopyBuffer(g.cmd, g.birth.buf, g.birthOld.buf, 1, &c1); // the ages of the cells that stay travel with them { // export the cells that leave the window (from the copy) for the persistent store const VkMemoryBarrier copied{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT}; @@ -13295,17 +13660,17 @@ int main(int argc, char** argv) { } PROF(4); if (!simOff && !checkMode) { // reactions between neighbouring cells, then burning - for (int ph = 0; ph < 4 * simSpeed; ++ph) { - pc.pass = ph & 3; - pc.frame = frame * simSpeed + ph / 4; - vkCmdBindPipeline(g.cmd, VK_PIPELINE_BIND_POINT_COMPUTE, g.pipeReact); - push(); - vkCmdDispatch(g.cmd, gx, gy, 1); - compute_barrier(g.cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, rw); - } for (int rep = 0; rep < simSpeed; ++rep) { - pc.pass = 0; pc.frame = frame * simSpeed + rep; + vkCmdBindPipeline(g.cmd, VK_PIPELINE_BIND_POINT_COMPUTE, g.pipeReact); + // Plan once, commit thermal changes, then visit disjoint adjacency pairs. + for (int phase : {4, 5, 0, 1, 2, 3}) { + pc.pass = phase; + push(); + vkCmdDispatch(g.cmd, gx, gy, 1); + compute_barrier(g.cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, rw); + } + pc.pass = 0; vkCmdBindPipeline(g.cmd, VK_PIPELINE_BIND_POINT_COMPUTE, g.pipeBurn); push(); vkCmdDispatch(g.cmd, gx, gy, 1); @@ -13360,6 +13725,14 @@ int main(int argc, char** argv) { } PROF(10); vkCmdBindPipeline(g.cmd, VK_PIPELINE_BIND_POINT_COMPUTE, g.pipeProjStep); + if (!simOff) { + pc.pass = 2; + push(); + vkCmdDispatch(g.cmd, 1, 1, 1); + compute_barrier(g.cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, rwBits); + pc.pass = 0; + push(); + } vkCmdDispatch(g.cmd, ng::GpuFx::kMaxProj / 64, 1, 1); compute_barrier(g.cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, rwBits); PROF(11); @@ -13386,6 +13759,8 @@ int main(int argc, char** argv) { } { const VkBufferCopy pcpy{16, 0, (VkDeviceSize)ng::GpuFx::kMaxProj * 96 + (std::getenv("NG_PARTDUMP") ? (VkDeviceSize)16384 * 48 : 0)}; vkCmdCopyBuffer(g.cmd, g.fxState.buf, g.fprojRead.buf, 1, &pcpy); } // the projectiles for the twins const VkBufferCopy cc{0, 0, (VkDeviceSize)kGridW * kGridH * 4}; + const VkBufferCopy ac{0, cc.size, cc.size}; + vkCmdCopyBuffer(g.cmd, g.terrainAge.buf, g.cellsRead.buf, 1, &ac); vkCmdCopyBuffer(g.cmd, g.cells.buf, g.cellsRead.buf, 1, &cc); windowLive = true; cellsViewX = viewX; cellsViewY = viewY; if (worldMode && frame % 5 == 0) { vkCmdCopyBuffer(g.cmd, g.bodyGrid.buf, g.bodyRead.buf, 1, &cc); bodyViewX = viewX; bodyViewY = viewY; } if (!farReadSlots.empty()) { // the dirty far slots back to the host (written into the store next frame) diff --git a/src/app/rigid_debug.h b/src/app/rigid_debug.h new file mode 100644 index 0000000..28a9c9e --- /dev/null +++ b/src/app/rigid_debug.h @@ -0,0 +1,43 @@ +#pragma once +#include "phys/phys_system.h" +#include +#include +#include +#include +#include + +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 rows; }; + std::deque history; + void append(int frame, std::vector 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); + } +}; +} diff --git a/src/core/materials.cpp b/src/core/materials.cpp index a561121..467613d 100644 --- a/src/core/materials.cpp +++ b/src/core/materials.cpp @@ -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 "); @@ -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); diff --git a/src/core/materials.h b/src/core/materials.h index 5bce7e4..c78f1ab 100644 --- a/src/core/materials.h +++ b/src/core/materials.h @@ -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 canonical_; std::vector mats_; std::vector reactions_; std::vector loose_twin_; diff --git a/src/entity/entity.cpp b/src/entity/entity.cpp index 3749a89..4a3746b 100644 --- a/src/entity/entity.cpp +++ b/src/entity/entity.cpp @@ -400,10 +400,8 @@ EntityState make_state(EntityDb& db, const Wak& wak, EntityLayer& L, std::mapget("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; diff --git a/src/entity/hit_liquid.h b/src/entity/hit_liquid.h new file mode 100644 index 0000000..1102de0 --- /dev/null +++ b/src/entity/hit_liquid.h @@ -0,0 +1,54 @@ +#pragma once +#include +#include +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); +} diff --git a/src/entity/runtime.cpp b/src/entity/runtime.cpp index 8960ce2..384846e 100644 --- a/src/entity/runtime.cpp +++ b/src/entity/runtime.cpp @@ -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& query) { +void EntityRuntime::material_area_step(const std::function& query, + const std::function& 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::functionattrs[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::functionalive) continue; std::vector> 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, diff --git a/src/entity/runtime.h b/src/entity/runtime.h index c810d70..228ce9f 100644 --- a/src/entity/runtime.h +++ b/src/entity/runtime.h @@ -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& query); + void material_area_step(const std::function& query, + const std::function& 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; diff --git a/src/gun/gpu_fx.h b/src/gun/gpu_fx.h index be5a093..65d224a 100644 --- a/src/gun/gpu_fx.h +++ b/src/gun/gpu_fx.h @@ -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; diff --git a/src/gun/projectiles.cpp b/src/gun/projectiles.cpp index bf29951..6f91ab9 100644 --- a/src/gun/projectiles.cpp +++ b/src/gun/projectiles.cpp @@ -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"}; diff --git a/src/gun/projectiles.h b/src/gun/projectiles.h index bdd0ca9..74da8d4 100644 --- a/src/gun/projectiles.h +++ b/src/gun/projectiles.h @@ -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) diff --git a/src/lua/lua_host.cpp b/src/lua/lua_host.cpp index 7052de5..fd55ea9 100644 --- a/src/lua/lua_host.cpp +++ b/src/lua/lua_host.cpp @@ -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 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 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; } diff --git a/src/phys/body_cells.h b/src/phys/body_cells.h new file mode 100644 index 0000000..f2fa1b6 --- /dev/null +++ b/src/phys/body_cells.h @@ -0,0 +1,59 @@ +#pragma once +#include +#include + +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 +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 +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 diff --git a/src/phys/phys_system.cpp b/src/phys/phys_system.cpp index b19ea94..4bfc608 100644 --- a/src/phys/phys_system.cpp +++ b/src/phys/phys_system.cpp @@ -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(world_); - terrain_static_ = std::make_unique(world_, kStaticOnlyCategory); + terrain_ = std::make_unique(world_, 4); + terrain_static_ = std::make_unique(world_, 1); + terrain_sticky_ = std::make_unique(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 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 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 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::minsecond)>>(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> 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> 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 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(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 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 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 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::debug_bodies() const { + std::vector 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::debug_polygons(const std::array* viewport) const { + std::vector 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::debug_contacts() const { + using namespace noita::cpu; + std::vector 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; diff --git a/src/phys/phys_system.h b/src/phys/phys_system.h index 7e3d06c..45df92f 100644 --- a/src/phys/phys_system.h +++ b/src/phys/phys_system.h @@ -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 +#include #include #include #include @@ -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 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 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 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& contacts() { return contact_events_; } // new contacts since the last clear std::vector& 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> points; }; + struct DebugContact { int a, b; double x, y, separation; }; + std::vector debug_bodies() const; + // Optional pixel-space viewport for drawing; null retains the complete dump. + std::vector debug_polygons(const std::array* viewport = nullptr) const; + std::vector debug_contacts() const; private: struct ShapeData { std::vector polys; // image pixels double com_x = 0, com_y = 0, mass = 1, radius = 1; + std::array bounds{}; // min x/y, max x/y in image pixels std::vector> 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 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> terrain_coverage_; void apply_liquids(const uint32_t* cells, int vx, int vy, int gw, int gh); std::vector materials_; std::vector shapes_; std::vector joint_breaks_; std::vector specs_; noita::cpu::WorldToi world_; - std::unique_ptr terrain_; // everything solid for the bodies (static cells and powders) - std::unique_ptr terrain_static_; // the static cells alone: the terrain of the bodies that go through sand - std::vector solid_lut_; // material id -> 1 when the cell is terrain for the bodies + std::unique_ptr terrain_; // native class 2: settled loose powder (category 4) + std::unique_ptr terrain_static_; // fixed class-1 terrain (category 1) + std::unique_ptr terrain_sticky_; // mobile/attached class-1 powder, separate from fixed terrain + std::vector solid_lut_; // material id -> 0 excluded, 1 static, 2 live powder classification required std::vector 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 built_; // chunks that have terrain fixtures - std::unordered_map stale_; // built chunks whose cells changed, not rebuilt yet: the frame of the first powder-only change (-1: a solid one) + std::unordered_set stale_; // changed chunks, rebuilt before a nearby body steps + std::unordered_set stale_sticky_; // changed attached/settled class-1 powder std::unordered_set stale_static_; // (and their static fixtures too) struct ExplosionRequest { double x, y, radius, fmin, fmax; }; std::vector pending_explosions_; @@ -157,6 +194,7 @@ private: std::unordered_map 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 diff --git a/src/phys/terrain_outline.cpp b/src/phys/terrain_outline.cpp index 00e2ab5..c9f8396 100644 --- a/src/phys/terrain_outline.cpp +++ b/src/phys/terrain_outline.cpp @@ -1,4 +1,5 @@ #include "phys/terrain_outline.h" +#include "clipper.hpp" #include #include @@ -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 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 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> trace_mask(const uint8_t* mask, int w, int h, float tolerance, std::vector* 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; }; diff --git a/src/phys/terrain_outline.h b/src/phys/terrain_outline.h index 9014a60..436f6f5 100644 --- a/src/phys/terrain_outline.h +++ b/src/phys/terrain_outline.h @@ -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> lines; + std::vector> 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> trace_mask(const uint8_t* mask, int w, int h, float tolerance, std::vector* is_hole = nullptr); diff --git a/src/phys/terrain_world.cpp b/src/phys/terrain_world.cpp index dda22b0..566e7dc 100644 --- a/src/phys/terrain_world.cpp +++ b/src/phys/terrain_world.cpp @@ -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& out) { +bool segment_quad(Vec2d p, Vec2d q, double chunk_bottom_local_y, bool powder, bool connector, std::vector& 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,26 +65,28 @@ 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 (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 quad; - if (!segment_quad(p, q, bottom, 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.friction = c.friction; - f.restitution = c.restitution; - f.density = 0; - ToiShape shape; - shape.vertices = std::move(quad); - shape.radius = 0.009999999776482582; // 01053788 - f.children.push_back(std::move(shape)); - if (create_world_fixture(w_, body, std::move(f)) >= 0) ++made; + 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 quad; + 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_, (std::uint16_t)(category_ == 1 ? 0xffff : 0xffdf), 0}; // 008102c0: category 4, mask 0xffdf + f.friction = c.friction; + f.restitution = c.restitution; + f.density = 0; + ToiShape shape; + shape.vertices = std::move(quad); + shape.radius = 0.009999999776482582; // 01053788 + f.children.push_back(std::move(shape)); + if (create_world_fixture(w_, body, std::move(f)) >= 0) ++made; + } } } return made; diff --git a/src/phys/terrain_world.h b/src/phys/terrain_world.h index 8310318..6a0329b 100644 --- a/src/phys/terrain_world.h +++ b/src/phys/terrain_world.h @@ -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; 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 bodies_; }; diff --git a/src/physics/gpu_bodies.cpp b/src/physics/gpu_bodies.cpp index 8794aef..225cc1f 100644 --- a/src/physics/gpu_bodies.cpp +++ b/src/physics/gpu_bodies.cpp @@ -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 used(shapes_); + for (int k = 0; k < *count_; ++k) if (!(bodies_[k].i[2] & 2)) used[bodies_[k].i[0]] = 1; + std::vector 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 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) { diff --git a/src/physics/gpu_bodies.h b/src/physics/gpu_bodies.h index 85e7d8c..7d3a022 100644 --- a/src/physics/gpu_bodies.h +++ b/src/physics/gpu_bodies.h @@ -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 shape_live_; struct Info { float radius, mass, com_x, com_y; }; std::vector info_; }; diff --git a/src/world/reactions.cpp b/src/world/reactions.cpp index 7f0b554..701454b 100644 --- a/src/world/reactions.cpp +++ b/src/world/reactions.cpp @@ -18,6 +18,7 @@ std::vector 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 build_thermal_conversions(const MaterialDb& db) { + std::vector 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> 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 A = matching(db, r.in[0]), B = matching(db, r.in[1]); std::vector C = r.in[2].empty() ? std::vector{-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(); } diff --git a/src/world/reactions.h b/src/world/reactions.h index 5185415..37971d2 100644 --- a/src/world/reactions.h +++ b/src/world/reactions.h @@ -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 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 rec; std::vector 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 build_thermal_conversions(const MaterialDb& db); } // namespace ng diff --git a/src/world/temple_state.h b/src/world/temple_state.h new file mode 100644 index 0000000..31a6505 --- /dev/null +++ b/src/world/temple_state.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include +#include + +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& 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 diff --git a/third_party/clipper/LICENSE b/third_party/clipper/LICENSE new file mode 100644 index 0000000..36b7cd9 --- /dev/null +++ b/third_party/clipper/LICENSE @@ -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. diff --git a/third_party/clipper/README.md b/third_party/clipper/README.md new file mode 100644 index 0000000..fb68049 --- /dev/null +++ b/third_party/clipper/README.md @@ -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. diff --git a/third_party/clipper/clipper.cpp b/third_party/clipper/clipper.cpp new file mode 100644 index 0000000..a2cfe2a --- /dev/null +++ b/third_party/clipper/clipper.cpp @@ -0,0 +1,3680 @@ +/******************************************************************************* +* * +* 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 * +* * +*******************************************************************************/ + +/******************************************************************************* +* * +* This is a translation of the Delphi Clipper library and the naming style * +* used has retained a Delphi flavour. * +* * +*******************************************************************************/ + +#include "clipper.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace ClipperLib { + +static long64 const loRange = 0x3FFFFFFF; +static long64 const hiRange = 0x3FFFFFFFFFFFFFFFLL; +static double const pi = 3.141592653589793238; +enum Direction { dRightToLeft, dLeftToRight }; + +#define HORIZONTAL (-1.0E+40) +#define TOLERANCE (1.0e-20) +#define NEAR_ZERO(val) (((val) > -TOLERANCE) && ((val) < TOLERANCE)) +#define NEAR_EQUAL(a, b) NEAR_ZERO((a) - (b)) + +const char coords_range_error[] = "Coordinate exceeds range bounds."; + +inline long64 Abs(long64 val) +{ + return val < 0 ? -val : val; +} + +//------------------------------------------------------------------------------ +// PolyTree methods ... +//------------------------------------------------------------------------------ + +void PolyTree::Clear() +{ + for (PolyNodes::size_type i = 0; i < AllNodes.size(); ++i) + delete AllNodes[i]; + AllNodes.resize(0); + Childs.resize(0); +} +//------------------------------------------------------------------------------ + +PolyNode* PolyTree::GetFirst() const +{ + if (!Childs.empty()) + return Childs[0]; + else + return 0; +} +//------------------------------------------------------------------------------ + +int PolyTree::Total() const +{ + return AllNodes.size(); +} + +//------------------------------------------------------------------------------ +// PolyNode methods ... +//------------------------------------------------------------------------------ + +PolyNode::PolyNode(): Childs(), Parent(0), Index(0) +{ +} +//------------------------------------------------------------------------------ + +int PolyNode::ChildCount() const +{ + return Childs.size(); +} +//------------------------------------------------------------------------------ + +void PolyNode::AddChild(PolyNode& child) +{ + unsigned cnt = Childs.size(); + Childs.push_back(&child); + child.Parent = this; + child.Index = cnt; +} +//------------------------------------------------------------------------------ + +PolyNode* PolyNode::GetNext() const +{ + if (!Childs.empty()) + return Childs[0]; + else + return GetNextSiblingUp(); +} +//------------------------------------------------------------------------------ + +PolyNode* PolyNode::GetNextSiblingUp() const +{ + if (!Parent) //protects against PolyTree.GetNextSiblingUp() + return 0; + else if (Index == Parent->Childs.size() - 1) + return Parent->GetNextSiblingUp(); + else + return Parent->Childs[Index + 1]; +} +//------------------------------------------------------------------------------ + +bool PolyNode::IsHole() const +{ + bool result = true; + PolyNode* node = Parent; + while (node) + { + result = !result; + node = node->Parent; + } + return result; +} + +//------------------------------------------------------------------------------ +// Int128 class (enables safe math on signed 64bit integers) +// eg Int128 val1((long64)9223372036854775807); //ie 2^63 -1 +// Int128 val2((long64)9223372036854775807); +// Int128 val3 = val1 * val2; +// val3.AsString => "85070591730234615847396907784232501249" (8.5e+37) +//------------------------------------------------------------------------------ + +class Int128 +{ + public: + + ulong64 lo; + long64 hi; + + Int128(long64 _lo = 0) + { + lo = (ulong64)_lo; + if (_lo < 0) hi = -1; else hi = 0; + } + + + Int128(const Int128 &val): lo(val.lo), hi(val.hi){} + + Int128(const long64& _hi, const ulong64& _lo): lo(_lo), hi(_hi){} + + long64 operator = (const long64 &val) + { + lo = (ulong64)val; + if (val < 0) hi = -1; else hi = 0; + return val; + } + + bool operator == (const Int128 &val) const + {return (hi == val.hi && lo == val.lo);} + + bool operator != (const Int128 &val) const + { return !(*this == val);} + + bool operator > (const Int128 &val) const + { + if (hi != val.hi) + return hi > val.hi; + else + return lo > val.lo; + } + + bool operator < (const Int128 &val) const + { + if (hi != val.hi) + return hi < val.hi; + else + return lo < val.lo; + } + + bool operator >= (const Int128 &val) const + { return !(*this < val);} + + bool operator <= (const Int128 &val) const + { return !(*this > val);} + + Int128& operator += (const Int128 &rhs) + { + hi += rhs.hi; + lo += rhs.lo; + if (lo < rhs.lo) hi++; + return *this; + } + + Int128 operator + (const Int128 &rhs) const + { + Int128 result(*this); + result+= rhs; + return result; + } + + Int128& operator -= (const Int128 &rhs) + { + *this += -rhs; + return *this; + } + + Int128 operator - (const Int128 &rhs) const + { + Int128 result(*this); + result -= rhs; + return result; + } + + Int128 operator-() const //unary negation + { + if (lo == 0) + return Int128(-hi,0); + else + return Int128(~hi,~lo +1); + } + + Int128 operator/ (const Int128 &rhs) const + { + if (rhs.lo == 0 && rhs.hi == 0) + throw "Int128 operator/: divide by zero"; + + bool negate = (rhs.hi < 0) != (hi < 0); + Int128 dividend = *this; + Int128 divisor = rhs; + if (dividend.hi < 0) dividend = -dividend; + if (divisor.hi < 0) divisor = -divisor; + + if (divisor < dividend) + { + Int128 result = Int128(0); + Int128 cntr = Int128(1); + while (divisor.hi >= 0 && !(divisor > dividend)) + { + divisor.hi <<= 1; + if ((long64)divisor.lo < 0) divisor.hi++; + divisor.lo <<= 1; + + cntr.hi <<= 1; + if ((long64)cntr.lo < 0) cntr.hi++; + cntr.lo <<= 1; + } + divisor.lo >>= 1; + if ((divisor.hi & 1) == 1) + divisor.lo |= 0x8000000000000000LL; + divisor.hi = (ulong64)divisor.hi >> 1; + + cntr.lo >>= 1; + if ((cntr.hi & 1) == 1) + cntr.lo |= 0x8000000000000000LL; + cntr.hi >>= 1; + + while (cntr.hi != 0 || cntr.lo != 0) + { + if (!(dividend < divisor)) + { + dividend -= divisor; + result.hi |= cntr.hi; + result.lo |= cntr.lo; + } + divisor.lo >>= 1; + if ((divisor.hi & 1) == 1) + divisor.lo |= 0x8000000000000000LL; + divisor.hi >>= 1; + + cntr.lo >>= 1; + if ((cntr.hi & 1) == 1) + cntr.lo |= 0x8000000000000000LL; + cntr.hi >>= 1; + } + if (negate) result = -result; + return result; + } + else if (rhs.hi == this->hi && rhs.lo == this->lo) + return Int128(1); + else + return Int128(0); + } + + double AsDouble() const + { + const double shift64 = 18446744073709551616.0; //2^64 + if (hi < 0) + { + if (lo == 0) return (double)hi * shift64; + else return -(double)(~lo + ~hi * shift64); + } + else + return (double)(lo + hi * shift64); + } +}; + +Int128 Int128Mul (long64 lhs, long64 rhs) +{ + bool negate = (lhs < 0) != (rhs < 0); + + if (lhs < 0) lhs = -lhs; + ulong64 int1Hi = ulong64(lhs) >> 32; + ulong64 int1Lo = ulong64(lhs & 0xFFFFFFFF); + + if (rhs < 0) rhs = -rhs; + ulong64 int2Hi = ulong64(rhs) >> 32; + ulong64 int2Lo = ulong64(rhs & 0xFFFFFFFF); + + //nb: see comments in clipper.pas + ulong64 a = int1Hi * int2Hi; + ulong64 b = int1Lo * int2Lo; + ulong64 c = int1Hi * int2Lo + int1Lo * int2Hi; + + Int128 tmp; + tmp.hi = long64(a + (c >> 32)); + tmp.lo = long64(c << 32); + tmp.lo += long64(b); + if (tmp.lo < b) tmp.hi++; + if (negate) tmp = -tmp; + return tmp; +} + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ + +bool FullRangeNeeded(const Polygon &pts) +{ + bool result = false; + for (Polygon::size_type i = 0; i < pts.size(); ++i) + { + if (Abs(pts[i].X) > hiRange || Abs(pts[i].Y) > hiRange) + throw coords_range_error; + else if (Abs(pts[i].X) > loRange || Abs(pts[i].Y) > loRange) + result = true; + } + return result; +} +//------------------------------------------------------------------------------ + +bool Orientation(const Polygon &poly) +{ + return Area(poly) >= 0; +} +//------------------------------------------------------------------------------ + +inline bool PointsEqual( const IntPoint &pt1, const IntPoint &pt2) +{ + return ( pt1.X == pt2.X && pt1.Y == pt2.Y ); +} +//------------------------------------------------------------------------------ + +double Area(const Polygon &poly) +{ + int highI = (int)poly.size() -1; + if (highI < 2) return 0; + + if (FullRangeNeeded(poly)) { + Int128 a; + a = Int128Mul(poly[highI].X + poly[0].X, poly[0].Y - poly[highI].Y); + for (int i = 1; i <= highI; ++i) + a += Int128Mul(poly[i - 1].X + poly[i].X, poly[i].Y - poly[i -1].Y); + return a.AsDouble() / 2; + } + else + { + double a; + a = ((double)poly[highI].X + poly[0].X) * ((double)poly[0].Y - poly[highI].Y); + for (int i = 1; i <= highI; ++i) + a += ((double)poly[i - 1].X + poly[i].X) * ((double)poly[i].Y - poly[i - 1].Y); + return a / 2; + } +} +//------------------------------------------------------------------------------ + +double Area(const OutRec &outRec, bool UseFullInt64Range) +{ + OutPt *op = outRec.pts; + if (!op) return 0; + if (UseFullInt64Range) { + Int128 a(0); + do { + a += Int128Mul(op->pt.X + op->prev->pt.X, op->prev->pt.Y - op->pt.Y); + op = op->next; + } while (op != outRec.pts); + return a.AsDouble() / 2; + } + else + { + double a = 0; + do { + a = a + (op->pt.X + op->prev->pt.X) * (op->prev->pt.Y - op->pt.Y); + op = op->next; + } while (op != outRec.pts); + return a / 2; + } +} +//------------------------------------------------------------------------------ + +bool PointIsVertex(const IntPoint &pt, OutPt *pp) +{ + OutPt *pp2 = pp; + do + { + if (PointsEqual(pp2->pt, pt)) return true; + pp2 = pp2->next; + } + while (pp2 != pp); + return false; +} +//------------------------------------------------------------------------------ + +bool PointOnLineSegment(const IntPoint pt, + const IntPoint linePt1, const IntPoint linePt2, bool UseFullInt64Range) +{ + if (UseFullInt64Range) + return ((pt.X == linePt1.X) && (pt.Y == linePt1.Y)) || + ((pt.X == linePt2.X) && (pt.Y == linePt2.Y)) || + (((pt.X > linePt1.X) == (pt.X < linePt2.X)) && + ((pt.Y > linePt1.Y) == (pt.Y < linePt2.Y)) && + ((Int128Mul((pt.X - linePt1.X), (linePt2.Y - linePt1.Y)) == + Int128Mul((linePt2.X - linePt1.X), (pt.Y - linePt1.Y))))); + else + return ((pt.X == linePt1.X) && (pt.Y == linePt1.Y)) || + ((pt.X == linePt2.X) && (pt.Y == linePt2.Y)) || + (((pt.X > linePt1.X) == (pt.X < linePt2.X)) && + ((pt.Y > linePt1.Y) == (pt.Y < linePt2.Y)) && + ((pt.X - linePt1.X) * (linePt2.Y - linePt1.Y) == + (linePt2.X - linePt1.X) * (pt.Y - linePt1.Y))); +} +//------------------------------------------------------------------------------ + +bool PointOnPolygon(const IntPoint pt, OutPt *pp, bool UseFullInt64Range) +{ + OutPt *pp2 = pp; + for (;;) + { + if (PointOnLineSegment(pt, pp2->pt, pp2->next->pt, UseFullInt64Range)) + return true; + pp2 = pp2->next; + if (pp2 == pp) return false; + } +} +//------------------------------------------------------------------------------ + +bool PointInPolygon(const IntPoint &pt, OutPt *pp, bool UseFullInt64Range) +{ + OutPt *pp2 = pp; + bool result = false; + if (UseFullInt64Range) { + do + { + if ((((pp2->pt.Y <= pt.Y) && (pt.Y < pp2->prev->pt.Y)) || + ((pp2->prev->pt.Y <= pt.Y) && (pt.Y < pp2->pt.Y))) && + Int128(pt.X - pp2->pt.X) < + Int128Mul(pp2->prev->pt.X - pp2->pt.X, pt.Y - pp2->pt.Y) / + Int128(pp2->prev->pt.Y - pp2->pt.Y)) + result = !result; + pp2 = pp2->next; + } + while (pp2 != pp); + } + else + { + do + { + if ((((pp2->pt.Y <= pt.Y) && (pt.Y < pp2->prev->pt.Y)) || + ((pp2->prev->pt.Y <= pt.Y) && (pt.Y < pp2->pt.Y))) && + (pt.X < (pp2->prev->pt.X - pp2->pt.X) * (pt.Y - pp2->pt.Y) / + (pp2->prev->pt.Y - pp2->pt.Y) + pp2->pt.X )) result = !result; + pp2 = pp2->next; + } + while (pp2 != pp); + } + return result; +} +//------------------------------------------------------------------------------ + +bool SlopesEqual(const TEdge &e1, const TEdge &e2, bool UseFullInt64Range) +{ + if (UseFullInt64Range) + return Int128Mul(e1.deltaY, e2.deltaX) == Int128Mul(e1.deltaX, e2.deltaY); + else return e1.deltaY * e2.deltaX == e1.deltaX * e2.deltaY; +} +//------------------------------------------------------------------------------ + +bool SlopesEqual(const IntPoint pt1, const IntPoint pt2, + const IntPoint pt3, bool UseFullInt64Range) +{ + if (UseFullInt64Range) + return Int128Mul(pt1.Y-pt2.Y, pt2.X-pt3.X) == Int128Mul(pt1.X-pt2.X, pt2.Y-pt3.Y); + else return (pt1.Y-pt2.Y)*(pt2.X-pt3.X) == (pt1.X-pt2.X)*(pt2.Y-pt3.Y); +} +//------------------------------------------------------------------------------ + +bool SlopesEqual(const IntPoint pt1, const IntPoint pt2, + const IntPoint pt3, const IntPoint pt4, bool UseFullInt64Range) +{ + if (UseFullInt64Range) + return Int128Mul(pt1.Y-pt2.Y, pt3.X-pt4.X) == Int128Mul(pt1.X-pt2.X, pt3.Y-pt4.Y); + else return (pt1.Y-pt2.Y)*(pt3.X-pt4.X) == (pt1.X-pt2.X)*(pt3.Y-pt4.Y); +} +//------------------------------------------------------------------------------ + +double GetDx(const IntPoint pt1, const IntPoint pt2) +{ + return (pt1.Y == pt2.Y) ? + HORIZONTAL : (double)(pt2.X - pt1.X) / (pt2.Y - pt1.Y); +} +//--------------------------------------------------------------------------- + +void SetDx(TEdge &e) +{ + e.deltaX = (e.xtop - e.xbot); + e.deltaY = (e.ytop - e.ybot); + + if (e.deltaY == 0) e.dx = HORIZONTAL; + else e.dx = (double)(e.deltaX) / e.deltaY; +} +//--------------------------------------------------------------------------- + +void SwapSides(TEdge &edge1, TEdge &edge2) +{ + EdgeSide side = edge1.side; + edge1.side = edge2.side; + edge2.side = side; +} +//------------------------------------------------------------------------------ + +void SwapPolyIndexes(TEdge &edge1, TEdge &edge2) +{ + int outIdx = edge1.outIdx; + edge1.outIdx = edge2.outIdx; + edge2.outIdx = outIdx; +} +//------------------------------------------------------------------------------ + +inline long64 Round(double val) +{ + return (val < 0) ? static_cast(val - 0.5) : static_cast(val + 0.5); +} +//------------------------------------------------------------------------------ + +long64 TopX(TEdge &edge, const long64 currentY) +{ + return ( currentY == edge.ytop ) ? + edge.xtop : edge.xbot + Round(edge.dx *(currentY - edge.ybot)); +} +//------------------------------------------------------------------------------ + +bool IntersectPoint(TEdge &edge1, TEdge &edge2, + IntPoint &ip, bool UseFullInt64Range) +{ + double b1, b2; + if (SlopesEqual(edge1, edge2, UseFullInt64Range)) + { + if (edge2.ybot > edge1.ybot) ip.Y = edge2.ybot; + else ip.Y = edge1.ybot; + return false; + } + else if (NEAR_ZERO(edge1.dx)) + { + ip.X = edge1.xbot; + if (NEAR_EQUAL(edge2.dx, HORIZONTAL)) + ip.Y = edge2.ybot; + else + { + b2 = edge2.ybot - (edge2.xbot / edge2.dx); + ip.Y = Round(ip.X / edge2.dx + b2); + } + } + else if (NEAR_ZERO(edge2.dx)) + { + ip.X = edge2.xbot; + if (NEAR_EQUAL(edge1.dx, HORIZONTAL)) + ip.Y = edge1.ybot; + else + { + b1 = edge1.ybot - (edge1.xbot / edge1.dx); + ip.Y = Round(ip.X / edge1.dx + b1); + } + } + else + { + b1 = edge1.xbot - edge1.ybot * edge1.dx; + b2 = edge2.xbot - edge2.ybot * edge2.dx; + double q = (b2-b1) / (edge1.dx - edge2.dx); + ip.Y = Round(q); + if (std::fabs(edge1.dx) < std::fabs(edge2.dx)) + ip.X = Round(edge1.dx * q + b1); + else + ip.X = Round(edge2.dx * q + b2); + } + + if (ip.Y < edge1.ytop || ip.Y < edge2.ytop) + { + if (edge1.ytop > edge2.ytop) + { + ip.X = edge1.xtop; + ip.Y = edge1.ytop; + return TopX(edge2, edge1.ytop) < edge1.xtop; + } + else + { + ip.X = edge2.xtop; + ip.Y = edge2.ytop; + return TopX(edge1, edge2.ytop) > edge2.xtop; + } + } + else + return true; +} +//------------------------------------------------------------------------------ + +void ReversePolyPtLinks(OutPt *pp) +{ + if (!pp) return; + OutPt *pp1, *pp2; + pp1 = pp; + do { + pp2 = pp1->next; + pp1->next = pp1->prev; + pp1->prev = pp2; + pp1 = pp2; + } while( pp1 != pp ); +} +//------------------------------------------------------------------------------ + +void DisposeOutPts(OutPt*& pp) +{ + if (pp == 0) return; + pp->prev->next = 0; + while( pp ) + { + OutPt *tmpPp = pp; + pp = pp->next; + delete tmpPp; + } +} +//------------------------------------------------------------------------------ + +void InitEdge(TEdge *e, TEdge *eNext, + TEdge *ePrev, const IntPoint &pt, PolyType polyType) +{ + std::memset(e, 0, sizeof(TEdge)); + e->next = eNext; + e->prev = ePrev; + e->xcurr = pt.X; + e->ycurr = pt.Y; + if (e->ycurr >= e->next->ycurr) + { + e->xbot = e->xcurr; + e->ybot = e->ycurr; + e->xtop = e->next->xcurr; + e->ytop = e->next->ycurr; + e->windDelta = 1; + } else + { + e->xtop = e->xcurr; + e->ytop = e->ycurr; + e->xbot = e->next->xcurr; + e->ybot = e->next->ycurr; + e->windDelta = -1; + } + SetDx(*e); + e->polyType = polyType; + e->outIdx = -1; +} +//------------------------------------------------------------------------------ + +inline void SwapX(TEdge &e) +{ + //swap horizontal edges' top and bottom x's so they follow the natural + //progression of the bounds - ie so their xbots will align with the + //adjoining lower edge. [Helpful in the ProcessHorizontal() method.] + e.xcurr = e.xtop; + e.xtop = e.xbot; + e.xbot = e.xcurr; +} +//------------------------------------------------------------------------------ + +void SwapPoints(IntPoint &pt1, IntPoint &pt2) +{ + IntPoint tmp = pt1; + pt1 = pt2; + pt2 = tmp; +} +//------------------------------------------------------------------------------ + +bool GetOverlapSegment(IntPoint pt1a, IntPoint pt1b, IntPoint pt2a, + IntPoint pt2b, IntPoint &pt1, IntPoint &pt2) +{ + //precondition: segments are colinear. + if (Abs(pt1a.X - pt1b.X) > Abs(pt1a.Y - pt1b.Y)) + { + if (pt1a.X > pt1b.X) SwapPoints(pt1a, pt1b); + if (pt2a.X > pt2b.X) SwapPoints(pt2a, pt2b); + if (pt1a.X > pt2a.X) pt1 = pt1a; else pt1 = pt2a; + if (pt1b.X < pt2b.X) pt2 = pt1b; else pt2 = pt2b; + return pt1.X < pt2.X; + } else + { + if (pt1a.Y < pt1b.Y) SwapPoints(pt1a, pt1b); + if (pt2a.Y < pt2b.Y) SwapPoints(pt2a, pt2b); + if (pt1a.Y < pt2a.Y) pt1 = pt1a; else pt1 = pt2a; + if (pt1b.Y > pt2b.Y) pt2 = pt1b; else pt2 = pt2b; + return pt1.Y > pt2.Y; + } +} +//------------------------------------------------------------------------------ + +bool FirstIsBottomPt(const OutPt* btmPt1, const OutPt* btmPt2) +{ + OutPt *p = btmPt1->prev; + while (PointsEqual(p->pt, btmPt1->pt) && (p != btmPt1)) p = p->prev; + double dx1p = std::fabs(GetDx(btmPt1->pt, p->pt)); + p = btmPt1->next; + while (PointsEqual(p->pt, btmPt1->pt) && (p != btmPt1)) p = p->next; + double dx1n = std::fabs(GetDx(btmPt1->pt, p->pt)); + + p = btmPt2->prev; + while (PointsEqual(p->pt, btmPt2->pt) && (p != btmPt2)) p = p->prev; + double dx2p = std::fabs(GetDx(btmPt2->pt, p->pt)); + p = btmPt2->next; + while (PointsEqual(p->pt, btmPt2->pt) && (p != btmPt2)) p = p->next; + double dx2n = std::fabs(GetDx(btmPt2->pt, p->pt)); + return (dx1p >= dx2p && dx1p >= dx2n) || (dx1n >= dx2p && dx1n >= dx2n); +} +//------------------------------------------------------------------------------ + +OutPt* GetBottomPt(OutPt *pp) +{ + OutPt* dups = 0; + OutPt* p = pp->next; + while (p != pp) + { + if (p->pt.Y > pp->pt.Y) + { + pp = p; + dups = 0; + } + else if (p->pt.Y == pp->pt.Y && p->pt.X <= pp->pt.X) + { + if (p->pt.X < pp->pt.X) + { + dups = 0; + pp = p; + } else + { + if (p->next != pp && p->prev != pp) dups = p; + } + } + p = p->next; + } + if (dups) + { + //there appears to be at least 2 vertices at bottomPt so ... + while (dups != p) + { + if (!FirstIsBottomPt(p, dups)) pp = dups; + dups = dups->next; + while (!PointsEqual(dups->pt, pp->pt)) dups = dups->next; + } + } + return pp; +} +//------------------------------------------------------------------------------ + +bool FindSegment(OutPt* &pp, bool UseFullInt64Range, + IntPoint &pt1, IntPoint &pt2) +{ + //outPt1 & outPt2 => the overlap segment (if the function returns true) + if (!pp) return false; + OutPt* pp2 = pp; + IntPoint pt1a = pt1, pt2a = pt2; + do + { + if (SlopesEqual(pt1a, pt2a, pp->pt, pp->prev->pt, UseFullInt64Range) && + SlopesEqual(pt1a, pt2a, pp->pt, UseFullInt64Range) && + GetOverlapSegment(pt1a, pt2a, pp->pt, pp->prev->pt, pt1, pt2)) + return true; + pp = pp->next; + } + while (pp != pp2); + return false; +} +//------------------------------------------------------------------------------ + +bool Pt3IsBetweenPt1AndPt2(const IntPoint pt1, + const IntPoint pt2, const IntPoint pt3) +{ + if (PointsEqual(pt1, pt3) || PointsEqual(pt2, pt3)) return true; + else if (pt1.X != pt2.X) return (pt1.X < pt3.X) == (pt3.X < pt2.X); + else return (pt1.Y < pt3.Y) == (pt3.Y < pt2.Y); +} +//------------------------------------------------------------------------------ + +OutPt* InsertPolyPtBetween(OutPt* p1, OutPt* p2, const IntPoint pt) +{ + if (p1 == p2) throw "JoinError"; + OutPt* result = new OutPt; + result->pt = pt; + if (p2 == p1->next) + { + p1->next = result; + p2->prev = result; + result->next = p2; + result->prev = p1; + } else + { + p2->next = result; + p1->prev = result; + result->next = p1; + result->prev = p2; + } + return result; +} + +//------------------------------------------------------------------------------ +// ClipperBase class methods ... +//------------------------------------------------------------------------------ + +ClipperBase::ClipperBase() //constructor +{ + m_MinimaList = 0; + m_CurrentLM = 0; + m_UseFullRange = true; +} +//------------------------------------------------------------------------------ + +ClipperBase::~ClipperBase() //destructor +{ + Clear(); +} +//------------------------------------------------------------------------------ + +void RangeTest(const IntPoint& pt, long64& maxrange) +{ + if (Abs(pt.X) > maxrange) + { + if (Abs(pt.X) > hiRange) + throw coords_range_error; + else maxrange = hiRange; + } + if (Abs(pt.Y) > maxrange) + { + if (Abs(pt.Y) > hiRange) + throw coords_range_error; + else maxrange = hiRange; + } +} +//------------------------------------------------------------------------------ + +bool ClipperBase::AddPolygon(const Polygon &pg, PolyType polyType) +{ + int len = (int)pg.size(); + if (len < 3) return false; + + long64 maxVal; + if (m_UseFullRange) maxVal = hiRange; else maxVal = loRange; + RangeTest(pg[0], maxVal); + + Polygon p(len); + p[0] = pg[0]; + int j = 0; + + for (int i = 0; i < len; ++i) + { + RangeTest(pg[i], maxVal); + + if (i == 0 || PointsEqual(p[j], pg[i])) continue; + else if (j > 0 && SlopesEqual(p[j-1], p[j], pg[i], m_UseFullRange)) + { + if (PointsEqual(p[j-1], pg[i])) j--; + } else j++; + p[j] = pg[i]; + } + if (j < 2) return false; + + len = j+1; + while (len > 2) + { + //nb: test for point equality before testing slopes ... + if (PointsEqual(p[j], p[0])) j--; + else if (PointsEqual(p[0], p[1]) || + SlopesEqual(p[j], p[0], p[1], m_UseFullRange)) + p[0] = p[j--]; + else if (SlopesEqual(p[j-1], p[j], p[0], m_UseFullRange)) j--; + else if (SlopesEqual(p[0], p[1], p[2], m_UseFullRange)) + { + for (int i = 2; i <= j; ++i) p[i-1] = p[i]; + j--; + } + else break; + len--; + } + if (len < 3) return false; + + //create a new edge array ... + TEdge *edges = new TEdge [len]; + m_edges.push_back(edges); + + //convert vertices to a double-linked-list of edges and initialize ... + edges[0].xcurr = p[0].X; + edges[0].ycurr = p[0].Y; + InitEdge(&edges[len-1], &edges[0], &edges[len-2], p[len-1], polyType); + for (int i = len-2; i > 0; --i) + InitEdge(&edges[i], &edges[i+1], &edges[i-1], p[i], polyType); + InitEdge(&edges[0], &edges[1], &edges[len-1], p[0], polyType); + + //reset xcurr & ycurr and find 'eHighest' (given the Y axis coordinates + //increase downward so the 'highest' edge will have the smallest ytop) ... + TEdge *e = &edges[0]; + TEdge *eHighest = e; + do + { + e->xcurr = e->xbot; + e->ycurr = e->ybot; + if (e->ytop < eHighest->ytop) eHighest = e; + e = e->next; + } + while ( e != &edges[0]); + + //make sure eHighest is positioned so the following loop works safely ... + if (eHighest->windDelta > 0) eHighest = eHighest->next; + if (NEAR_EQUAL(eHighest->dx, HORIZONTAL)) eHighest = eHighest->next; + + //finally insert each local minima ... + e = eHighest; + do { + e = AddBoundsToLML(e); + } + while( e != eHighest ); + return true; +} +//------------------------------------------------------------------------------ + +void ClipperBase::InsertLocalMinima(LocalMinima *newLm) +{ + if( ! m_MinimaList ) + { + m_MinimaList = newLm; + } + else if( newLm->Y >= m_MinimaList->Y ) + { + newLm->next = m_MinimaList; + m_MinimaList = newLm; + } else + { + LocalMinima* tmpLm = m_MinimaList; + while( tmpLm->next && ( newLm->Y < tmpLm->next->Y ) ) + tmpLm = tmpLm->next; + newLm->next = tmpLm->next; + tmpLm->next = newLm; + } +} +//------------------------------------------------------------------------------ + +TEdge* ClipperBase::AddBoundsToLML(TEdge *e) +{ + //Starting at the top of one bound we progress to the bottom where there's + //a local minima. We then go to the top of the next bound. These two bounds + //form the left and right (or right and left) bounds of the local minima. + e->nextInLML = 0; + e = e->next; + for (;;) + { + if (NEAR_EQUAL(e->dx, HORIZONTAL)) + { + //nb: proceed through horizontals when approaching from their right, + // but break on horizontal minima if approaching from their left. + // This ensures 'local minima' are always on the left of horizontals. + if (e->next->ytop < e->ytop && e->next->xbot > e->prev->xbot) break; + if (e->xtop != e->prev->xbot) SwapX(*e); + e->nextInLML = e->prev; + } + else if (e->ycurr == e->prev->ycurr) break; + else e->nextInLML = e->prev; + e = e->next; + } + + //e and e.prev are now at a local minima ... + LocalMinima* newLm = new LocalMinima; + newLm->next = 0; + newLm->Y = e->prev->ybot; + + if ( NEAR_EQUAL(e->dx, HORIZONTAL) ) //horizontal edges never start a left bound + { + if (e->xbot != e->prev->xbot) SwapX(*e); + newLm->leftBound = e->prev; + newLm->rightBound = e; + } else if (e->dx < e->prev->dx) + { + newLm->leftBound = e->prev; + newLm->rightBound = e; + } else + { + newLm->leftBound = e; + newLm->rightBound = e->prev; + } + newLm->leftBound->side = esLeft; + newLm->rightBound->side = esRight; + InsertLocalMinima( newLm ); + + for (;;) + { + if ( e->next->ytop == e->ytop && !NEAR_EQUAL(e->next->dx, HORIZONTAL) ) break; + e->nextInLML = e->next; + e = e->next; + if ( NEAR_EQUAL(e->dx, HORIZONTAL) && e->xbot != e->prev->xtop) SwapX(*e); + } + return e->next; +} +//------------------------------------------------------------------------------ + +bool ClipperBase::AddPolygons(const Polygons &ppg, PolyType polyType) +{ + bool result = false; + for (Polygons::size_type i = 0; i < ppg.size(); ++i) + if (AddPolygon(ppg[i], polyType)) result = true; + return result; +} +//------------------------------------------------------------------------------ + +void ClipperBase::Clear() +{ + DisposeLocalMinimaList(); + for (EdgeList::size_type i = 0; i < m_edges.size(); ++i) delete [] m_edges[i]; + m_edges.clear(); + m_UseFullRange = false; +} +//------------------------------------------------------------------------------ + +void ClipperBase::Reset() +{ + m_CurrentLM = m_MinimaList; + if( !m_CurrentLM ) return; //ie nothing to process + + //reset all edges ... + LocalMinima* lm = m_MinimaList; + while( lm ) + { + TEdge* e = lm->leftBound; + while( e ) + { + e->xcurr = e->xbot; + e->ycurr = e->ybot; + e->side = esLeft; + e->outIdx = -1; + e = e->nextInLML; + } + e = lm->rightBound; + while( e ) + { + e->xcurr = e->xbot; + e->ycurr = e->ybot; + e->side = esRight; + e->outIdx = -1; + e = e->nextInLML; + } + lm = lm->next; + } +} +//------------------------------------------------------------------------------ + +void ClipperBase::DisposeLocalMinimaList() +{ + while( m_MinimaList ) + { + LocalMinima* tmpLm = m_MinimaList->next; + delete m_MinimaList; + m_MinimaList = tmpLm; + } + m_CurrentLM = 0; +} +//------------------------------------------------------------------------------ + +void ClipperBase::PopLocalMinima() +{ + if( ! m_CurrentLM ) return; + m_CurrentLM = m_CurrentLM->next; +} +//------------------------------------------------------------------------------ + +IntRect ClipperBase::GetBounds() +{ + IntRect result; + LocalMinima* lm = m_MinimaList; + if (!lm) + { + result.left = result.top = result.right = result.bottom = 0; + return result; + } + result.left = lm->leftBound->xbot; + result.top = lm->leftBound->ybot; + result.right = lm->leftBound->xbot; + result.bottom = lm->leftBound->ybot; + while (lm) + { + if (lm->leftBound->ybot > result.bottom) + result.bottom = lm->leftBound->ybot; + TEdge* e = lm->leftBound; + for (;;) { + TEdge* bottomE = e; + while (e->nextInLML) + { + if (e->xbot < result.left) result.left = e->xbot; + if (e->xbot > result.right) result.right = e->xbot; + e = e->nextInLML; + } + if (e->xbot < result.left) result.left = e->xbot; + if (e->xbot > result.right) result.right = e->xbot; + if (e->xtop < result.left) result.left = e->xtop; + if (e->xtop > result.right) result.right = e->xtop; + if (e->ytop < result.top) result.top = e->ytop; + + if (bottomE == lm->leftBound) e = lm->rightBound; + else break; + } + lm = lm->next; + } + return result; +} + + +//------------------------------------------------------------------------------ +// TClipper methods ... +//------------------------------------------------------------------------------ + +Clipper::Clipper() : ClipperBase() //constructor +{ + m_Scanbeam = 0; + m_ActiveEdges = 0; + m_SortedEdges = 0; + m_IntersectNodes = 0; + m_ExecuteLocked = false; + m_UseFullRange = false; + m_ReverseOutput = false; + m_ForceSimple = false; +} +//------------------------------------------------------------------------------ + +Clipper::~Clipper() //destructor +{ + Clear(); + DisposeScanbeamList(); +} +//------------------------------------------------------------------------------ + +void Clipper::Clear() +{ + if (m_edges.empty()) return; //avoids problems with ClipperBase destructor + DisposeAllPolyPts(); + ClipperBase::Clear(); +} +//------------------------------------------------------------------------------ + +void Clipper::DisposeScanbeamList() +{ + while ( m_Scanbeam ) { + Scanbeam* sb2 = m_Scanbeam->next; + delete m_Scanbeam; + m_Scanbeam = sb2; + } +} +//------------------------------------------------------------------------------ + +void Clipper::Reset() +{ + ClipperBase::Reset(); + m_Scanbeam = 0; + m_ActiveEdges = 0; + m_SortedEdges = 0; + DisposeAllPolyPts(); + LocalMinima* lm = m_MinimaList; + while (lm) + { + InsertScanbeam(lm->Y); + lm = lm->next; + } +} +//------------------------------------------------------------------------------ + +bool Clipper::Execute(ClipType clipType, Polygons &solution, + PolyFillType subjFillType, PolyFillType clipFillType) +{ + if( m_ExecuteLocked ) return false; + m_ExecuteLocked = true; + solution.resize(0); + m_SubjFillType = subjFillType; + m_ClipFillType = clipFillType; + m_ClipType = clipType; + m_UsingPolyTree = false; + bool succeeded = ExecuteInternal(); + if (succeeded) BuildResult(solution); + m_ExecuteLocked = false; + return succeeded; +} +//------------------------------------------------------------------------------ + +bool Clipper::Execute(ClipType clipType, PolyTree& polytree, + PolyFillType subjFillType, PolyFillType clipFillType) +{ + if( m_ExecuteLocked ) return false; + m_ExecuteLocked = true; + m_SubjFillType = subjFillType; + m_ClipFillType = clipFillType; + m_ClipType = clipType; + m_UsingPolyTree = true; + bool succeeded = ExecuteInternal(); + if (succeeded) BuildResult2(polytree); + m_ExecuteLocked = false; + return succeeded; +} +//------------------------------------------------------------------------------ + +void Clipper::FixHoleLinkage(OutRec &outrec) +{ + //skip OutRecs that (a) contain outermost polygons or + //(b) already have the correct owner/child linkage ... + if (!outrec.FirstLeft || + (outrec.isHole != outrec.FirstLeft->isHole && + outrec.FirstLeft->pts)) return; + + OutRec* orfl = outrec.FirstLeft; + while (orfl && ((orfl->isHole == outrec.isHole) || !orfl->pts)) + orfl = orfl->FirstLeft; + outrec.FirstLeft = orfl; +} +//------------------------------------------------------------------------------ + +bool Clipper::ExecuteInternal() +{ + bool succeeded; + try { + Reset(); + if (!m_CurrentLM ) return true; + long64 botY = PopScanbeam(); + do { + InsertLocalMinimaIntoAEL(botY); + ClearHorzJoins(); + ProcessHorizontals(); + long64 topY = PopScanbeam(); + succeeded = ProcessIntersections(botY, topY); + if (!succeeded) break; + ProcessEdgesAtTopOfScanbeam(topY); + botY = topY; + } while( m_Scanbeam ); + } + catch(...) { + succeeded = false; + } + + if (succeeded) + { + //tidy up output polygons and fix orientations where necessary ... + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + { + OutRec *outRec = m_PolyOuts[i]; + if (!outRec->pts) continue; + FixupOutPolygon(*outRec); + if (!outRec->pts) continue; + + if ((outRec->isHole ^ m_ReverseOutput) == (Area(*outRec, m_UseFullRange) > 0)) + ReversePolyPtLinks(outRec->pts); + } + + if (!m_Joins.empty()) JoinCommonEdges(); + if (m_ForceSimple) DoSimplePolygons(); + } + + ClearJoins(); + ClearHorzJoins(); + return succeeded; +} +//------------------------------------------------------------------------------ + +void Clipper::InsertScanbeam(const long64 Y) +{ + if( !m_Scanbeam ) + { + m_Scanbeam = new Scanbeam; + m_Scanbeam->next = 0; + m_Scanbeam->Y = Y; + } + else if( Y > m_Scanbeam->Y ) + { + Scanbeam* newSb = new Scanbeam; + newSb->Y = Y; + newSb->next = m_Scanbeam; + m_Scanbeam = newSb; + } else + { + Scanbeam* sb2 = m_Scanbeam; + while( sb2->next && ( Y <= sb2->next->Y ) ) sb2 = sb2->next; + if( Y == sb2->Y ) return; //ie ignores duplicates + Scanbeam* newSb = new Scanbeam; + newSb->Y = Y; + newSb->next = sb2->next; + sb2->next = newSb; + } +} +//------------------------------------------------------------------------------ + +long64 Clipper::PopScanbeam() +{ + long64 Y = m_Scanbeam->Y; + Scanbeam* sb2 = m_Scanbeam; + m_Scanbeam = m_Scanbeam->next; + delete sb2; + return Y; +} +//------------------------------------------------------------------------------ + +void Clipper::DisposeAllPolyPts(){ + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + DisposeOutRec(i); + m_PolyOuts.clear(); +} +//------------------------------------------------------------------------------ + +void Clipper::DisposeOutRec(PolyOutList::size_type index) +{ + OutRec *outRec = m_PolyOuts[index]; + if (outRec->pts) DisposeOutPts(outRec->pts); + delete outRec; + m_PolyOuts[index] = 0; +} +//------------------------------------------------------------------------------ + +void Clipper::SetWindingCount(TEdge &edge) +{ + TEdge *e = edge.prevInAEL; + //find the edge of the same polytype that immediately preceeds 'edge' in AEL + while ( e && e->polyType != edge.polyType ) e = e->prevInAEL; + if ( !e ) + { + edge.windCnt = edge.windDelta; + edge.windCnt2 = 0; + e = m_ActiveEdges; //ie get ready to calc windCnt2 + } else if ( IsEvenOddFillType(edge) ) + { + //EvenOdd filling ... + edge.windCnt = 1; + edge.windCnt2 = e->windCnt2; + e = e->nextInAEL; //ie get ready to calc windCnt2 + } else + { + //nonZero, Positive or Negative filling ... + if ( e->windCnt * e->windDelta < 0 ) + { + if (Abs(e->windCnt) > 1) + { + if (e->windDelta * edge.windDelta < 0) edge.windCnt = e->windCnt; + else edge.windCnt = e->windCnt + edge.windDelta; + } else + edge.windCnt = e->windCnt + e->windDelta + edge.windDelta; + } else + { + if ( Abs(e->windCnt) > 1 && e->windDelta * edge.windDelta < 0) + edge.windCnt = e->windCnt; + else if ( e->windCnt + edge.windDelta == 0 ) + edge.windCnt = e->windCnt; + else edge.windCnt = e->windCnt + edge.windDelta; + } + edge.windCnt2 = e->windCnt2; + e = e->nextInAEL; //ie get ready to calc windCnt2 + } + + //update windCnt2 ... + if ( IsEvenOddAltFillType(edge) ) + { + //EvenOdd filling ... + while ( e != &edge ) + { + edge.windCnt2 = (edge.windCnt2 == 0) ? 1 : 0; + e = e->nextInAEL; + } + } else + { + //nonZero, Positive or Negative filling ... + while ( e != &edge ) + { + edge.windCnt2 += e->windDelta; + e = e->nextInAEL; + } + } +} +//------------------------------------------------------------------------------ + +bool Clipper::IsEvenOddFillType(const TEdge& edge) const +{ + if (edge.polyType == ptSubject) + return m_SubjFillType == pftEvenOdd; else + return m_ClipFillType == pftEvenOdd; +} +//------------------------------------------------------------------------------ + +bool Clipper::IsEvenOddAltFillType(const TEdge& edge) const +{ + if (edge.polyType == ptSubject) + return m_ClipFillType == pftEvenOdd; else + return m_SubjFillType == pftEvenOdd; +} +//------------------------------------------------------------------------------ + +bool Clipper::IsContributing(const TEdge& edge) const +{ + PolyFillType pft, pft2; + if (edge.polyType == ptSubject) + { + pft = m_SubjFillType; + pft2 = m_ClipFillType; + } else + { + pft = m_ClipFillType; + pft2 = m_SubjFillType; + } + + switch(pft) + { + case pftEvenOdd: + case pftNonZero: + if (Abs(edge.windCnt) != 1) return false; + break; + case pftPositive: + if (edge.windCnt != 1) return false; + break; + default: //pftNegative + if (edge.windCnt != -1) return false; + } + + switch(m_ClipType) + { + case ctIntersection: + switch(pft2) + { + case pftEvenOdd: + case pftNonZero: + return (edge.windCnt2 != 0); + case pftPositive: + return (edge.windCnt2 > 0); + default: + return (edge.windCnt2 < 0); + } + case ctUnion: + switch(pft2) + { + case pftEvenOdd: + case pftNonZero: + return (edge.windCnt2 == 0); + case pftPositive: + return (edge.windCnt2 <= 0); + default: + return (edge.windCnt2 >= 0); + } + case ctDifference: + if (edge.polyType == ptSubject) + switch(pft2) + { + case pftEvenOdd: + case pftNonZero: + return (edge.windCnt2 == 0); + case pftPositive: + return (edge.windCnt2 <= 0); + default: + return (edge.windCnt2 >= 0); + } + else + switch(pft2) + { + case pftEvenOdd: + case pftNonZero: + return (edge.windCnt2 != 0); + case pftPositive: + return (edge.windCnt2 > 0); + default: + return (edge.windCnt2 < 0); + } + default: + return true; + } +} +//------------------------------------------------------------------------------ + +void Clipper::AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &pt) +{ + TEdge *e, *prevE; + if( NEAR_EQUAL(e2->dx, HORIZONTAL) || ( e1->dx > e2->dx ) ) + { + AddOutPt( e1, pt ); + e2->outIdx = e1->outIdx; + e1->side = esLeft; + e2->side = esRight; + e = e1; + if (e->prevInAEL == e2) + prevE = e2->prevInAEL; + else + prevE = e->prevInAEL; + } else + { + AddOutPt( e2, pt ); + e1->outIdx = e2->outIdx; + e1->side = esRight; + e2->side = esLeft; + e = e2; + if (e->prevInAEL == e1) + prevE = e1->prevInAEL; + else + prevE = e->prevInAEL; + } + if (prevE && prevE->outIdx >= 0 && + (TopX(*prevE, pt.Y) == TopX(*e, pt.Y)) && + SlopesEqual(*e, *prevE, m_UseFullRange)) + AddJoin(e, prevE, -1, -1); +} +//------------------------------------------------------------------------------ + +void Clipper::AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &pt) +{ + AddOutPt( e1, pt ); + if( e1->outIdx == e2->outIdx ) + { + e1->outIdx = -1; + e2->outIdx = -1; + } + else if (e1->outIdx < e2->outIdx) + AppendPolygon(e1, e2); + else + AppendPolygon(e2, e1); +} +//------------------------------------------------------------------------------ + +void Clipper::AddEdgeToSEL(TEdge *edge) +{ + //SEL pointers in PEdge are reused to build a list of horizontal edges. + //However, we don't need to worry about order with horizontal edge processing. + if( !m_SortedEdges ) + { + m_SortedEdges = edge; + edge->prevInSEL = 0; + edge->nextInSEL = 0; + } + else + { + edge->nextInSEL = m_SortedEdges; + edge->prevInSEL = 0; + m_SortedEdges->prevInSEL = edge; + m_SortedEdges = edge; + } +} +//------------------------------------------------------------------------------ + +void Clipper::CopyAELToSEL() +{ + TEdge* e = m_ActiveEdges; + m_SortedEdges = e; + while ( e ) + { + e->prevInSEL = e->prevInAEL; + e->nextInSEL = e->nextInAEL; + e = e->nextInAEL; + } +} +//------------------------------------------------------------------------------ + +void Clipper::AddJoin(TEdge *e1, TEdge *e2, int e1OutIdx, int e2OutIdx) +{ + JoinRec* jr = new JoinRec; + if (e1OutIdx >= 0) + jr->poly1Idx = e1OutIdx; else + jr->poly1Idx = e1->outIdx; + jr->pt1a = IntPoint(e1->xcurr, e1->ycurr); + jr->pt1b = IntPoint(e1->xtop, e1->ytop); + if (e2OutIdx >= 0) + jr->poly2Idx = e2OutIdx; else + jr->poly2Idx = e2->outIdx; + jr->pt2a = IntPoint(e2->xcurr, e2->ycurr); + jr->pt2b = IntPoint(e2->xtop, e2->ytop); + m_Joins.push_back(jr); +} +//------------------------------------------------------------------------------ + +void Clipper::ClearJoins() +{ + for (JoinList::size_type i = 0; i < m_Joins.size(); i++) + delete m_Joins[i]; + m_Joins.resize(0); +} +//------------------------------------------------------------------------------ + +void Clipper::AddHorzJoin(TEdge *e, int idx) +{ + HorzJoinRec* hj = new HorzJoinRec; + hj->edge = e; + hj->savedIdx = idx; + m_HorizJoins.push_back(hj); +} +//------------------------------------------------------------------------------ + +void Clipper::ClearHorzJoins() +{ + for (HorzJoinList::size_type i = 0; i < m_HorizJoins.size(); i++) + delete m_HorizJoins[i]; + m_HorizJoins.resize(0); +} +//------------------------------------------------------------------------------ + +void Clipper::InsertLocalMinimaIntoAEL(const long64 botY) +{ + while( m_CurrentLM && ( m_CurrentLM->Y == botY ) ) + { + TEdge* lb = m_CurrentLM->leftBound; + TEdge* rb = m_CurrentLM->rightBound; + + InsertEdgeIntoAEL( lb ); + InsertScanbeam( lb->ytop ); + InsertEdgeIntoAEL( rb ); + + if (IsEvenOddFillType(*lb)) + { + lb->windDelta = 1; + rb->windDelta = 1; + } + else + { + rb->windDelta = -lb->windDelta; + } + SetWindingCount( *lb ); + rb->windCnt = lb->windCnt; + rb->windCnt2 = lb->windCnt2; + + if( NEAR_EQUAL(rb->dx, HORIZONTAL) ) + { + //nb: only rightbounds can have a horizontal bottom edge + AddEdgeToSEL( rb ); + InsertScanbeam( rb->nextInLML->ytop ); + } + else + InsertScanbeam( rb->ytop ); + + if( IsContributing(*lb) ) + AddLocalMinPoly( lb, rb, IntPoint(lb->xcurr, m_CurrentLM->Y) ); + + //if any output polygons share an edge, they'll need joining later ... + if (rb->outIdx >= 0 && NEAR_EQUAL(rb->dx, HORIZONTAL)) + { + for (HorzJoinList::size_type i = 0; i < m_HorizJoins.size(); ++i) + { + IntPoint pt, pt2; //returned by GetOverlapSegment() but unused here. + HorzJoinRec* hj = m_HorizJoins[i]; + //if horizontals rb and hj.edge overlap, flag for joining later ... + if (GetOverlapSegment(IntPoint(hj->edge->xbot, hj->edge->ybot), + IntPoint(hj->edge->xtop, hj->edge->ytop), + IntPoint(rb->xbot, rb->ybot), + IntPoint(rb->xtop, rb->ytop), pt, pt2)) + AddJoin(hj->edge, rb, hj->savedIdx); + } + } + + if( lb->nextInAEL != rb ) + { + if (rb->outIdx >= 0 && rb->prevInAEL->outIdx >= 0 && + SlopesEqual(*rb->prevInAEL, *rb, m_UseFullRange)) + AddJoin(rb, rb->prevInAEL); + + TEdge* e = lb->nextInAEL; + IntPoint pt = IntPoint(lb->xcurr, lb->ycurr); + while( e != rb ) + { + if(!e) throw clipperException("InsertLocalMinimaIntoAEL: missing rightbound!"); + //nb: For calculating winding counts etc, IntersectEdges() assumes + //that param1 will be to the right of param2 ABOVE the intersection ... + IntersectEdges( rb , e , pt , ipNone); //order important here + e = e->nextInAEL; + } + } + PopLocalMinima(); + } +} +//------------------------------------------------------------------------------ + +void Clipper::DeleteFromAEL(TEdge *e) +{ + TEdge* AelPrev = e->prevInAEL; + TEdge* AelNext = e->nextInAEL; + if( !AelPrev && !AelNext && (e != m_ActiveEdges) ) return; //already deleted + if( AelPrev ) AelPrev->nextInAEL = AelNext; + else m_ActiveEdges = AelNext; + if( AelNext ) AelNext->prevInAEL = AelPrev; + e->nextInAEL = 0; + e->prevInAEL = 0; +} +//------------------------------------------------------------------------------ + +void Clipper::DeleteFromSEL(TEdge *e) +{ + TEdge* SelPrev = e->prevInSEL; + TEdge* SelNext = e->nextInSEL; + if( !SelPrev && !SelNext && (e != m_SortedEdges) ) return; //already deleted + if( SelPrev ) SelPrev->nextInSEL = SelNext; + else m_SortedEdges = SelNext; + if( SelNext ) SelNext->prevInSEL = SelPrev; + e->nextInSEL = 0; + e->prevInSEL = 0; +} +//------------------------------------------------------------------------------ + +void Clipper::IntersectEdges(TEdge *e1, TEdge *e2, + const IntPoint &pt, const IntersectProtects protects) +{ + //e1 will be to the left of e2 BELOW the intersection. Therefore e1 is before + //e2 in AEL except when e1 is being inserted at the intersection point ... + bool e1stops = !(ipLeft & protects) && !e1->nextInLML && + e1->xtop == pt.X && e1->ytop == pt.Y; + bool e2stops = !(ipRight & protects) && !e2->nextInLML && + e2->xtop == pt.X && e2->ytop == pt.Y; + bool e1Contributing = ( e1->outIdx >= 0 ); + bool e2contributing = ( e2->outIdx >= 0 ); + + //update winding counts... + //assumes that e1 will be to the right of e2 ABOVE the intersection + if ( e1->polyType == e2->polyType ) + { + if ( IsEvenOddFillType( *e1) ) + { + int oldE1WindCnt = e1->windCnt; + e1->windCnt = e2->windCnt; + e2->windCnt = oldE1WindCnt; + } else + { + if (e1->windCnt + e2->windDelta == 0 ) e1->windCnt = -e1->windCnt; + else e1->windCnt += e2->windDelta; + if ( e2->windCnt - e1->windDelta == 0 ) e2->windCnt = -e2->windCnt; + else e2->windCnt -= e1->windDelta; + } + } else + { + if (!IsEvenOddFillType(*e2)) e1->windCnt2 += e2->windDelta; + else e1->windCnt2 = ( e1->windCnt2 == 0 ) ? 1 : 0; + if (!IsEvenOddFillType(*e1)) e2->windCnt2 -= e1->windDelta; + else e2->windCnt2 = ( e2->windCnt2 == 0 ) ? 1 : 0; + } + + PolyFillType e1FillType, e2FillType, e1FillType2, e2FillType2; + if (e1->polyType == ptSubject) + { + e1FillType = m_SubjFillType; + e1FillType2 = m_ClipFillType; + } else + { + e1FillType = m_ClipFillType; + e1FillType2 = m_SubjFillType; + } + if (e2->polyType == ptSubject) + { + e2FillType = m_SubjFillType; + e2FillType2 = m_ClipFillType; + } else + { + e2FillType = m_ClipFillType; + e2FillType2 = m_SubjFillType; + } + + long64 e1Wc, e2Wc; + switch (e1FillType) + { + case pftPositive: e1Wc = e1->windCnt; break; + case pftNegative: e1Wc = -e1->windCnt; break; + default: e1Wc = Abs(e1->windCnt); + } + switch(e2FillType) + { + case pftPositive: e2Wc = e2->windCnt; break; + case pftNegative: e2Wc = -e2->windCnt; break; + default: e2Wc = Abs(e2->windCnt); + } + + if ( e1Contributing && e2contributing ) + { + if ( e1stops || e2stops || + (e1Wc != 0 && e1Wc != 1) || (e2Wc != 0 && e2Wc != 1) || + (e1->polyType != e2->polyType && m_ClipType != ctXor) ) + AddLocalMaxPoly(e1, e2, pt); + else + { + AddOutPt(e1, pt); + AddOutPt(e2, pt); + SwapSides( *e1 , *e2 ); + SwapPolyIndexes( *e1 , *e2 ); + } + } + else if ( e1Contributing ) + { + if (e2Wc == 0 || e2Wc == 1) + { + AddOutPt(e1, pt); + SwapSides(*e1, *e2); + SwapPolyIndexes(*e1, *e2); + } + } + else if ( e2contributing ) + { + if (e1Wc == 0 || e1Wc == 1) + { + AddOutPt(e2, pt); + SwapSides(*e1, *e2); + SwapPolyIndexes(*e1, *e2); + } + } + else if ( (e1Wc == 0 || e1Wc == 1) && + (e2Wc == 0 || e2Wc == 1) && !e1stops && !e2stops ) + { + //neither edge is currently contributing ... + + long64 e1Wc2, e2Wc2; + switch (e1FillType2) + { + case pftPositive: e1Wc2 = e1->windCnt2; break; + case pftNegative : e1Wc2 = -e1->windCnt2; break; + default: e1Wc2 = Abs(e1->windCnt2); + } + switch (e2FillType2) + { + case pftPositive: e2Wc2 = e2->windCnt2; break; + case pftNegative: e2Wc2 = -e2->windCnt2; break; + default: e2Wc2 = Abs(e2->windCnt2); + } + + if (e1->polyType != e2->polyType) + AddLocalMinPoly(e1, e2, pt); + else if (e1Wc == 1 && e2Wc == 1) + switch( m_ClipType ) { + case ctIntersection: + if (e1Wc2 > 0 && e2Wc2 > 0) + AddLocalMinPoly(e1, e2, pt); + break; + case ctUnion: + if ( e1Wc2 <= 0 && e2Wc2 <= 0 ) + AddLocalMinPoly(e1, e2, pt); + break; + case ctDifference: + if (((e1->polyType == ptClip) && (e1Wc2 > 0) && (e2Wc2 > 0)) || + ((e1->polyType == ptSubject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) + AddLocalMinPoly(e1, e2, pt); + break; + case ctXor: + AddLocalMinPoly(e1, e2, pt); + } + else + SwapSides( *e1, *e2 ); + } + + if( (e1stops != e2stops) && + ( (e1stops && (e1->outIdx >= 0)) || (e2stops && (e2->outIdx >= 0)) ) ) + { + SwapSides( *e1, *e2 ); + SwapPolyIndexes( *e1, *e2 ); + } + + //finally, delete any non-contributing maxima edges ... + if( e1stops ) DeleteFromAEL( e1 ); + if( e2stops ) DeleteFromAEL( e2 ); +} +//------------------------------------------------------------------------------ + +void Clipper::SetHoleState(TEdge *e, OutRec *outrec) +{ + bool isHole = false; + TEdge *e2 = e->prevInAEL; + while (e2) + { + if (e2->outIdx >= 0) + { + isHole = !isHole; + if (! outrec->FirstLeft) + outrec->FirstLeft = m_PolyOuts[e2->outIdx]; + } + e2 = e2->prevInAEL; + } + if (isHole) outrec->isHole = true; +} +//------------------------------------------------------------------------------ + +OutRec* GetLowermostRec(OutRec *outRec1, OutRec *outRec2) +{ + //work out which polygon fragment has the correct hole state ... + if (!outRec1->bottomPt) + outRec1->bottomPt = GetBottomPt(outRec1->pts); + if (!outRec2->bottomPt) + outRec2->bottomPt = GetBottomPt(outRec2->pts); + OutPt *outPt1 = outRec1->bottomPt; + OutPt *outPt2 = outRec2->bottomPt; + if (outPt1->pt.Y > outPt2->pt.Y) return outRec1; + else if (outPt1->pt.Y < outPt2->pt.Y) return outRec2; + else if (outPt1->pt.X < outPt2->pt.X) return outRec1; + else if (outPt1->pt.X > outPt2->pt.X) return outRec2; + else if (outPt1->next == outPt1) return outRec2; + else if (outPt2->next == outPt2) return outRec1; + else if (FirstIsBottomPt(outPt1, outPt2)) return outRec1; + else return outRec2; +} +//------------------------------------------------------------------------------ + +bool Param1RightOfParam2(OutRec* outRec1, OutRec* outRec2) +{ + do + { + outRec1 = outRec1->FirstLeft; + if (outRec1 == outRec2) return true; + } while (outRec1); + return false; +} +//------------------------------------------------------------------------------ + +OutRec* Clipper::GetOutRec(int idx) +{ + OutRec* outrec = m_PolyOuts[idx]; + while (outrec != m_PolyOuts[outrec->idx]) + outrec = m_PolyOuts[outrec->idx]; + return outrec; +} +//------------------------------------------------------------------------------ + +void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) +{ + //get the start and ends of both output polygons ... + OutRec *outRec1 = m_PolyOuts[e1->outIdx]; + OutRec *outRec2 = m_PolyOuts[e2->outIdx]; + + OutRec *holeStateRec; + if (Param1RightOfParam2(outRec1, outRec2)) + holeStateRec = outRec2; + else if (Param1RightOfParam2(outRec2, outRec1)) + holeStateRec = outRec1; + else + holeStateRec = GetLowermostRec(outRec1, outRec2); + + OutPt* p1_lft = outRec1->pts; + OutPt* p1_rt = p1_lft->prev; + OutPt* p2_lft = outRec2->pts; + OutPt* p2_rt = p2_lft->prev; + + EdgeSide side; + //join e2 poly onto e1 poly and delete pointers to e2 ... + if( e1->side == esLeft ) + { + if( e2->side == esLeft ) + { + //z y x a b c + ReversePolyPtLinks(p2_lft); + p2_lft->next = p1_lft; + p1_lft->prev = p2_lft; + p1_rt->next = p2_rt; + p2_rt->prev = p1_rt; + outRec1->pts = p2_rt; + } else + { + //x y z a b c + p2_rt->next = p1_lft; + p1_lft->prev = p2_rt; + p2_lft->prev = p1_rt; + p1_rt->next = p2_lft; + outRec1->pts = p2_lft; + } + side = esLeft; + } else + { + if( e2->side == esRight ) + { + //a b c z y x + ReversePolyPtLinks(p2_lft); + p1_rt->next = p2_rt; + p2_rt->prev = p1_rt; + p2_lft->next = p1_lft; + p1_lft->prev = p2_lft; + } else + { + //a b c x y z + p1_rt->next = p2_lft; + p2_lft->prev = p1_rt; + p1_lft->prev = p2_rt; + p2_rt->next = p1_lft; + } + side = esRight; + } + + outRec1->bottomPt = 0; + if (holeStateRec == outRec2) + { + if (outRec2->FirstLeft != outRec1) + outRec1->FirstLeft = outRec2->FirstLeft; + outRec1->isHole = outRec2->isHole; + } + outRec2->pts = 0; + outRec2->bottomPt = 0; + + outRec2->FirstLeft = outRec1; + + int OKIdx = e1->outIdx; + int ObsoleteIdx = e2->outIdx; + + e1->outIdx = -1; //nb: safe because we only get here via AddLocalMaxPoly + e2->outIdx = -1; + + TEdge* e = m_ActiveEdges; + while( e ) + { + if( e->outIdx == ObsoleteIdx ) + { + e->outIdx = OKIdx; + e->side = side; + break; + } + e = e->nextInAEL; + } + + outRec2->idx = outRec1->idx; +} +//------------------------------------------------------------------------------ + +OutRec* Clipper::CreateOutRec() +{ + OutRec* result = new OutRec; + result->isHole = false; + result->FirstLeft = 0; + result->pts = 0; + result->bottomPt = 0; + result->polyNode = 0; + m_PolyOuts.push_back(result); + result->idx = (int)m_PolyOuts.size()-1; + return result; +} +//------------------------------------------------------------------------------ + +void Clipper::AddOutPt(TEdge *e, const IntPoint &pt) +{ + bool ToFront = (e->side == esLeft); + if( e->outIdx < 0 ) + { + OutRec *outRec = CreateOutRec(); + e->outIdx = outRec->idx; + OutPt* newOp = new OutPt; + outRec->pts = newOp; + newOp->pt = pt; + newOp->idx = outRec->idx; + newOp->next = newOp; + newOp->prev = newOp; + SetHoleState(e, outRec); + } else + { + OutRec *outRec = m_PolyOuts[e->outIdx]; + OutPt* op = outRec->pts; + if ((ToFront && PointsEqual(pt, op->pt)) || + (!ToFront && PointsEqual(pt, op->prev->pt))) return; + + OutPt* newOp = new OutPt; + newOp->pt = pt; + newOp->idx = outRec->idx; + newOp->next = op; + newOp->prev = op->prev; + newOp->prev->next = newOp; + op->prev = newOp; + if (ToFront) outRec->pts = newOp; + } +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessHorizontals() +{ + TEdge* horzEdge = m_SortedEdges; + while( horzEdge ) + { + DeleteFromSEL( horzEdge ); + ProcessHorizontal( horzEdge ); + horzEdge = m_SortedEdges; + } +} +//------------------------------------------------------------------------------ + +bool Clipper::IsTopHorz(const long64 XPos) +{ + TEdge* e = m_SortedEdges; + while( e ) + { + if( ( XPos >= std::min(e->xcurr, e->xtop) ) && + ( XPos <= std::max(e->xcurr, e->xtop) ) ) return false; + e = e->nextInSEL; + } + return true; +} +//------------------------------------------------------------------------------ + +inline bool IsMinima(TEdge *e) +{ + return e && (e->prev->nextInLML != e) && (e->next->nextInLML != e); +} +//------------------------------------------------------------------------------ + +inline bool IsMaxima(TEdge *e, const long64 Y) +{ + return e && e->ytop == Y && !e->nextInLML; +} +//------------------------------------------------------------------------------ + +inline bool IsIntermediate(TEdge *e, const long64 Y) +{ + return e->ytop == Y && e->nextInLML; +} +//------------------------------------------------------------------------------ + +TEdge *GetMaximaPair(TEdge *e) +{ + if( !IsMaxima(e->next, e->ytop) || e->next->xtop != e->xtop ) + return e->prev; else + return e->next; +} +//------------------------------------------------------------------------------ + +void Clipper::SwapPositionsInAEL(TEdge *edge1, TEdge *edge2) +{ + if( edge1->nextInAEL == edge2 ) + { + TEdge* next = edge2->nextInAEL; + if( next ) next->prevInAEL = edge1; + TEdge* prev = edge1->prevInAEL; + if( prev ) prev->nextInAEL = edge2; + edge2->prevInAEL = prev; + edge2->nextInAEL = edge1; + edge1->prevInAEL = edge2; + edge1->nextInAEL = next; + } + else if( edge2->nextInAEL == edge1 ) + { + TEdge* next = edge1->nextInAEL; + if( next ) next->prevInAEL = edge2; + TEdge* prev = edge2->prevInAEL; + if( prev ) prev->nextInAEL = edge1; + edge1->prevInAEL = prev; + edge1->nextInAEL = edge2; + edge2->prevInAEL = edge1; + edge2->nextInAEL = next; + } + else + { + TEdge* next = edge1->nextInAEL; + TEdge* prev = edge1->prevInAEL; + edge1->nextInAEL = edge2->nextInAEL; + if( edge1->nextInAEL ) edge1->nextInAEL->prevInAEL = edge1; + edge1->prevInAEL = edge2->prevInAEL; + if( edge1->prevInAEL ) edge1->prevInAEL->nextInAEL = edge1; + edge2->nextInAEL = next; + if( edge2->nextInAEL ) edge2->nextInAEL->prevInAEL = edge2; + edge2->prevInAEL = prev; + if( edge2->prevInAEL ) edge2->prevInAEL->nextInAEL = edge2; + } + + if( !edge1->prevInAEL ) m_ActiveEdges = edge1; + else if( !edge2->prevInAEL ) m_ActiveEdges = edge2; +} +//------------------------------------------------------------------------------ + +void Clipper::SwapPositionsInSEL(TEdge *edge1, TEdge *edge2) +{ + if( !( edge1->nextInSEL ) && !( edge1->prevInSEL ) ) return; + if( !( edge2->nextInSEL ) && !( edge2->prevInSEL ) ) return; + + if( edge1->nextInSEL == edge2 ) + { + TEdge* next = edge2->nextInSEL; + if( next ) next->prevInSEL = edge1; + TEdge* prev = edge1->prevInSEL; + if( prev ) prev->nextInSEL = edge2; + edge2->prevInSEL = prev; + edge2->nextInSEL = edge1; + edge1->prevInSEL = edge2; + edge1->nextInSEL = next; + } + else if( edge2->nextInSEL == edge1 ) + { + TEdge* next = edge1->nextInSEL; + if( next ) next->prevInSEL = edge2; + TEdge* prev = edge2->prevInSEL; + if( prev ) prev->nextInSEL = edge1; + edge1->prevInSEL = prev; + edge1->nextInSEL = edge2; + edge2->prevInSEL = edge1; + edge2->nextInSEL = next; + } + else + { + TEdge* next = edge1->nextInSEL; + TEdge* prev = edge1->prevInSEL; + edge1->nextInSEL = edge2->nextInSEL; + if( edge1->nextInSEL ) edge1->nextInSEL->prevInSEL = edge1; + edge1->prevInSEL = edge2->prevInSEL; + if( edge1->prevInSEL ) edge1->prevInSEL->nextInSEL = edge1; + edge2->nextInSEL = next; + if( edge2->nextInSEL ) edge2->nextInSEL->prevInSEL = edge2; + edge2->prevInSEL = prev; + if( edge2->prevInSEL ) edge2->prevInSEL->nextInSEL = edge2; + } + + if( !edge1->prevInSEL ) m_SortedEdges = edge1; + else if( !edge2->prevInSEL ) m_SortedEdges = edge2; +} +//------------------------------------------------------------------------------ + +TEdge* GetNextInAEL(TEdge *e, Direction dir) +{ + return dir == dLeftToRight ? e->nextInAEL : e->prevInAEL; +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessHorizontal(TEdge *horzEdge) +{ + Direction dir; + long64 horzLeft, horzRight; + + if( horzEdge->xcurr < horzEdge->xtop ) + { + horzLeft = horzEdge->xcurr; + horzRight = horzEdge->xtop; + dir = dLeftToRight; + } else + { + horzLeft = horzEdge->xtop; + horzRight = horzEdge->xcurr; + dir = dRightToLeft; + } + + TEdge* eMaxPair; + if( horzEdge->nextInLML ) eMaxPair = 0; + else eMaxPair = GetMaximaPair(horzEdge); + + TEdge* e = GetNextInAEL( horzEdge , dir ); + while( e ) + { + if ( e->xcurr == horzEdge->xtop && !eMaxPair ) + { + if (SlopesEqual(*e, *horzEdge->nextInLML, m_UseFullRange)) + { + //if output polygons share an edge, they'll need joining later ... + if (horzEdge->outIdx >= 0 && e->outIdx >= 0) + AddJoin(horzEdge->nextInLML, e, horzEdge->outIdx); + break; //we've reached the end of the horizontal line + } + else if (e->dx < horzEdge->nextInLML->dx) + //we really have got to the end of the intermediate horz edge so quit. + //nb: More -ve slopes follow more +ve slopes ABOVE the horizontal. + break; + } + + TEdge* eNext = GetNextInAEL( e, dir ); + + if (eMaxPair || + ((dir == dLeftToRight) && (e->xcurr < horzRight)) || + ((dir == dRightToLeft) && (e->xcurr > horzLeft))) + { + //so far we're still in range of the horizontal edge + if( e == eMaxPair ) + { + //horzEdge is evidently a maxima horizontal and we've arrived at its end. + if (dir == dLeftToRight) + IntersectEdges(horzEdge, e, IntPoint(e->xcurr, horzEdge->ycurr), ipNone); + else + IntersectEdges(e, horzEdge, IntPoint(e->xcurr, horzEdge->ycurr), ipNone); + if (eMaxPair->outIdx >= 0) throw clipperException("ProcessHorizontal error"); + return; + } + else if( NEAR_EQUAL(e->dx, HORIZONTAL) && !IsMinima(e) && !(e->xcurr > e->xtop) ) + { + //An overlapping horizontal edge. Overlapping horizontal edges are + //processed as if layered with the current horizontal edge (horizEdge) + //being infinitesimally lower that the next (e). Therfore, we + //intersect with e only if e.xcurr is within the bounds of horzEdge ... + if( dir == dLeftToRight ) + IntersectEdges( horzEdge , e, IntPoint(e->xcurr, horzEdge->ycurr), + (IsTopHorz( e->xcurr ))? ipLeft : ipBoth ); + else + IntersectEdges( e, horzEdge, IntPoint(e->xcurr, horzEdge->ycurr), + (IsTopHorz( e->xcurr ))? ipRight : ipBoth ); + } + else if( dir == dLeftToRight ) + { + IntersectEdges( horzEdge, e, IntPoint(e->xcurr, horzEdge->ycurr), + (IsTopHorz( e->xcurr ))? ipLeft : ipBoth ); + } + else + { + IntersectEdges( e, horzEdge, IntPoint(e->xcurr, horzEdge->ycurr), + (IsTopHorz( e->xcurr ))? ipRight : ipBoth ); + } + SwapPositionsInAEL( horzEdge, e ); + } + else if( (dir == dLeftToRight && e->xcurr >= horzRight) || + (dir == dRightToLeft && e->xcurr <= horzLeft) ) break; + e = eNext; + } //end while + + if( horzEdge->nextInLML ) + { + if( horzEdge->outIdx >= 0 ) + AddOutPt( horzEdge, IntPoint(horzEdge->xtop, horzEdge->ytop)); + UpdateEdgeIntoAEL( horzEdge ); + } + else + { + if ( horzEdge->outIdx >= 0 ) + IntersectEdges( horzEdge, eMaxPair, + IntPoint(horzEdge->xtop, horzEdge->ycurr), ipBoth); + if (eMaxPair->outIdx >= 0) throw clipperException("ProcessHorizontal error"); + DeleteFromAEL(eMaxPair); + DeleteFromAEL(horzEdge); + } +} +//------------------------------------------------------------------------------ + +void Clipper::UpdateEdgeIntoAEL(TEdge *&e) +{ + if( !e->nextInLML ) throw + clipperException("UpdateEdgeIntoAEL: invalid call"); + TEdge* AelPrev = e->prevInAEL; + TEdge* AelNext = e->nextInAEL; + e->nextInLML->outIdx = e->outIdx; + if( AelPrev ) AelPrev->nextInAEL = e->nextInLML; + else m_ActiveEdges = e->nextInLML; + if( AelNext ) AelNext->prevInAEL = e->nextInLML; + e->nextInLML->side = e->side; + e->nextInLML->windDelta = e->windDelta; + e->nextInLML->windCnt = e->windCnt; + e->nextInLML->windCnt2 = e->windCnt2; + e = e->nextInLML; + e->prevInAEL = AelPrev; + e->nextInAEL = AelNext; + if( !NEAR_EQUAL(e->dx, HORIZONTAL) ) InsertScanbeam( e->ytop ); +} +//------------------------------------------------------------------------------ + +bool Clipper::ProcessIntersections(const long64 botY, const long64 topY) +{ + if( !m_ActiveEdges ) return true; + try { + BuildIntersectList(botY, topY); + if (!m_IntersectNodes) return true; + if (!m_IntersectNodes->next || FixupIntersectionOrder()) ProcessIntersectList(); + else return false; + } + catch(...) { + m_SortedEdges = 0; + DisposeIntersectNodes(); + throw clipperException("ProcessIntersections error"); + } + m_SortedEdges = 0; + return true; +} +//------------------------------------------------------------------------------ + +void Clipper::DisposeIntersectNodes() +{ + while ( m_IntersectNodes ) + { + IntersectNode* iNode = m_IntersectNodes->next; + delete m_IntersectNodes; + m_IntersectNodes = iNode; + } +} +//------------------------------------------------------------------------------ + +void Clipper::BuildIntersectList(const long64 botY, const long64 topY) +{ + if ( !m_ActiveEdges ) return; + + //prepare for sorting ... + TEdge* e = m_ActiveEdges; + m_SortedEdges = e; + while( e ) + { + e->prevInSEL = e->prevInAEL; + e->nextInSEL = e->nextInAEL; + e->xcurr = TopX( *e, topY ); + e = e->nextInAEL; + } + + //bubblesort ... + bool isModified; + do + { + isModified = false; + e = m_SortedEdges; + while( e->nextInSEL ) + { + TEdge *eNext = e->nextInSEL; + IntPoint pt; + if(e->xcurr > eNext->xcurr) + { + if (!IntersectPoint(*e, *eNext, pt, m_UseFullRange) && e->xcurr > eNext->xcurr +1) + throw clipperException("Intersection error"); + if (pt.Y > botY) + { + pt.Y = botY; + pt.X = TopX(*e, pt.Y); + } + InsertIntersectNode( e, eNext, pt ); + SwapPositionsInSEL(e, eNext); + isModified = true; + } + else + e = eNext; + } + if( e->prevInSEL ) e->prevInSEL->nextInSEL = 0; + else break; + } + while ( isModified ); + m_SortedEdges = 0; //important +} +//------------------------------------------------------------------------------ + +void Clipper::InsertIntersectNode(TEdge *e1, TEdge *e2, const IntPoint &pt) +{ + IntersectNode* newNode = new IntersectNode; + newNode->edge1 = e1; + newNode->edge2 = e2; + newNode->pt = pt; + newNode->next = 0; + if( !m_IntersectNodes ) m_IntersectNodes = newNode; + else if(newNode->pt.Y > m_IntersectNodes->pt.Y ) + { + newNode->next = m_IntersectNodes; + m_IntersectNodes = newNode; + } + else + { + IntersectNode* iNode = m_IntersectNodes; + while(iNode->next && newNode->pt.Y <= iNode->next->pt.Y) + iNode = iNode->next; + newNode->next = iNode->next; + iNode->next = newNode; + } +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessIntersectList() +{ + while( m_IntersectNodes ) + { + IntersectNode* iNode = m_IntersectNodes->next; + { + IntersectEdges( m_IntersectNodes->edge1 , + m_IntersectNodes->edge2 , m_IntersectNodes->pt, ipBoth ); + SwapPositionsInAEL( m_IntersectNodes->edge1 , m_IntersectNodes->edge2 ); + } + delete m_IntersectNodes; + m_IntersectNodes = iNode; + } +} +//------------------------------------------------------------------------------ + +void Clipper::DoMaxima(TEdge *e, long64 topY) +{ + TEdge* eMaxPair = GetMaximaPair(e); + long64 X = e->xtop; + TEdge* eNext = e->nextInAEL; + while( eNext != eMaxPair ) + { + if (!eNext) throw clipperException("DoMaxima error"); + IntersectEdges( e, eNext, IntPoint(X, topY), ipBoth ); + SwapPositionsInAEL(e, eNext); + eNext = e->nextInAEL; + } + if( e->outIdx < 0 && eMaxPair->outIdx < 0 ) + { + DeleteFromAEL( e ); + DeleteFromAEL( eMaxPair ); + } + else if( e->outIdx >= 0 && eMaxPair->outIdx >= 0 ) + { + IntersectEdges( e, eMaxPair, IntPoint(X, topY), ipNone ); + } + else throw clipperException("DoMaxima error"); +} +//------------------------------------------------------------------------------ + +void Clipper::ProcessEdgesAtTopOfScanbeam(const long64 topY) +{ + TEdge* e = m_ActiveEdges; + while( e ) + { + //1. process maxima, treating them as if they're 'bent' horizontal edges, + // but exclude maxima with horizontal edges. nb: e can't be a horizontal. + if( IsMaxima(e, topY) && !NEAR_EQUAL(GetMaximaPair(e)->dx, HORIZONTAL) ) + { + //'e' might be removed from AEL, as may any following edges so ... + TEdge* ePrev = e->prevInAEL; + DoMaxima(e, topY); + if( !ePrev ) e = m_ActiveEdges; + else e = ePrev->nextInAEL; + } + else + { + bool intermediateVert = IsIntermediate(e, topY); + //2. promote horizontal edges, otherwise update xcurr and ycurr ... + if (intermediateVert && NEAR_EQUAL(e->nextInLML->dx, HORIZONTAL) ) + { + if (e->outIdx >= 0) + { + AddOutPt(e, IntPoint(e->xtop, e->ytop)); + + for (HorzJoinList::size_type i = 0; i < m_HorizJoins.size(); ++i) + { + IntPoint pt, pt2; + HorzJoinRec* hj = m_HorizJoins[i]; + if (GetOverlapSegment(IntPoint(hj->edge->xbot, hj->edge->ybot), + IntPoint(hj->edge->xtop, hj->edge->ytop), + IntPoint(e->nextInLML->xbot, e->nextInLML->ybot), + IntPoint(e->nextInLML->xtop, e->nextInLML->ytop), pt, pt2)) + AddJoin(hj->edge, e->nextInLML, hj->savedIdx, e->outIdx); + } + + AddHorzJoin(e->nextInLML, e->outIdx); + } + UpdateEdgeIntoAEL(e); + AddEdgeToSEL(e); + } else + { + e->xcurr = TopX( *e, topY ); + e->ycurr = topY; + + if (m_ForceSimple && e->prevInAEL && + e->prevInAEL->xcurr == e->xcurr && + e->outIdx >= 0 && e->prevInAEL->outIdx >= 0) + { + if (intermediateVert) + AddOutPt(e->prevInAEL, IntPoint(e->xcurr, topY)); + else + AddOutPt(e, IntPoint(e->xcurr, topY)); + } + } + e = e->nextInAEL; + } + } + + //3. Process horizontals at the top of the scanbeam ... + ProcessHorizontals(); + + //4. Promote intermediate vertices ... + e = m_ActiveEdges; + while( e ) + { + if( IsIntermediate( e, topY ) ) + { + if( e->outIdx >= 0 ) AddOutPt(e, IntPoint(e->xtop,e->ytop)); + UpdateEdgeIntoAEL(e); + + //if output polygons share an edge, they'll need joining later ... + TEdge* ePrev = e->prevInAEL; + TEdge* eNext = e->nextInAEL; + if (ePrev && ePrev->xcurr == e->xbot && + ePrev->ycurr == e->ybot && e->outIdx >= 0 && + ePrev->outIdx >= 0 && ePrev->ycurr > ePrev->ytop && + SlopesEqual(*e, *ePrev, m_UseFullRange)) + { + AddOutPt(ePrev, IntPoint(e->xbot, e->ybot)); + AddJoin(e, ePrev); + } + else if (eNext && eNext->xcurr == e->xbot && + eNext->ycurr == e->ybot && e->outIdx >= 0 && + eNext->outIdx >= 0 && eNext->ycurr > eNext->ytop && + SlopesEqual(*e, *eNext, m_UseFullRange)) + { + AddOutPt(eNext, IntPoint(e->xbot, e->ybot)); + AddJoin(e, eNext); + } + } + e = e->nextInAEL; + } +} +//------------------------------------------------------------------------------ + +void Clipper::FixupOutPolygon(OutRec &outrec) +{ + //FixupOutPolygon() - removes duplicate points and simplifies consecutive + //parallel edges by removing the middle vertex. + OutPt *lastOK = 0; + outrec.bottomPt = 0; + OutPt *pp = outrec.pts; + + for (;;) + { + if (pp->prev == pp || pp->prev == pp->next ) + { + DisposeOutPts(pp); + outrec.pts = 0; + return; + } + //test for duplicate points and for same slope (cross-product) ... + if ( PointsEqual(pp->pt, pp->next->pt) || + SlopesEqual(pp->prev->pt, pp->pt, pp->next->pt, m_UseFullRange) ) + { + lastOK = 0; + OutPt *tmp = pp; + pp->prev->next = pp->next; + pp->next->prev = pp->prev; + pp = pp->prev; + delete tmp; + } + else if (pp == lastOK) break; + else + { + if (!lastOK) lastOK = pp; + pp = pp->next; + } + } + outrec.pts = pp; +} +//------------------------------------------------------------------------------ + +void Clipper::BuildResult(Polygons &polys) +{ + polys.reserve(m_PolyOuts.size()); + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + { + if (m_PolyOuts[i]->pts) + { + Polygon pg; + OutPt* p = m_PolyOuts[i]->pts; + do + { + pg.push_back(p->pt); + p = p->prev; + } while (p != m_PolyOuts[i]->pts); + if (pg.size() > 2) + polys.push_back(pg); + } + } +} +//------------------------------------------------------------------------------ + +int PointCount(OutPt *pts) +{ + if (!pts) return 0; + int result = 0; + OutPt* p = pts; + do + { + result++; + p = p->next; + } + while (p != pts); + return result; +} +//------------------------------------------------------------------------------ + +void Clipper::BuildResult2(PolyTree& polytree) +{ + polytree.Clear(); + polytree.AllNodes.reserve(m_PolyOuts.size()); + //add each output polygon/contour to polytree ... + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); i++) + { + OutRec* outRec = m_PolyOuts[i]; + int cnt = PointCount(outRec->pts); + if (cnt < 3) continue; + FixHoleLinkage(*outRec); + PolyNode* pn = new PolyNode(); + //nb: polytree takes ownership of all the PolyNodes + polytree.AllNodes.push_back(pn); + outRec->polyNode = pn; + pn->Parent = 0; + pn->Index = 0; + pn->Contour.reserve(cnt); + OutPt *op = outRec->pts; + for (int j = 0; j < cnt; j++) + { + pn->Contour.push_back(op->pt); + op = op->prev; + } + } + + //fixup PolyNode links etc ... + polytree.Childs.reserve(m_PolyOuts.size()); + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); i++) + { + OutRec* outRec = m_PolyOuts[i]; + if (!outRec->polyNode) continue; + if (outRec->FirstLeft) + outRec->FirstLeft->polyNode->AddChild(*outRec->polyNode); + else + polytree.AddChild(*outRec->polyNode); + } +} +//------------------------------------------------------------------------------ + +void SwapIntersectNodes(IntersectNode &int1, IntersectNode &int2) +{ + //just swap the contents (because fIntersectNodes is a single-linked-list) + IntersectNode inode = int1; //gets a copy of Int1 + int1.edge1 = int2.edge1; + int1.edge2 = int2.edge2; + int1.pt = int2.pt; + int2.edge1 = inode.edge1; + int2.edge2 = inode.edge2; + int2.pt = inode.pt; +} +//------------------------------------------------------------------------------ + +inline bool EdgesAdjacent(const IntersectNode &inode) +{ + return (inode.edge1->nextInSEL == inode.edge2) || + (inode.edge1->prevInSEL == inode.edge2); +} +//------------------------------------------------------------------------------ + +bool Clipper::FixupIntersectionOrder() +{ + //pre-condition: intersections are sorted bottom-most (then left-most) first. + //Now it's crucial that intersections are made only between adjacent edges, + //so to ensure this the order of intersections may need adjusting ... + IntersectNode *inode = m_IntersectNodes; + CopyAELToSEL(); + while (inode) + { + if (!EdgesAdjacent(*inode)) + { + IntersectNode *nextNode = inode->next; + while (nextNode && !EdgesAdjacent(*nextNode)) + nextNode = nextNode->next; + if (!nextNode) + return false; + SwapIntersectNodes(*inode, *nextNode); + } + SwapPositionsInSEL(inode->edge1, inode->edge2); + inode = inode->next; + } + return true; +} +//------------------------------------------------------------------------------ + +inline bool E2InsertsBeforeE1(TEdge &e1, TEdge &e2) +{ + if (e2.xcurr == e1.xcurr) + { + if (e2.ytop > e1.ytop) + return e2.xtop < TopX(e1, e2.ytop); + else return e1.xtop > TopX(e2, e1.ytop); + } + else return e2.xcurr < e1.xcurr; +} +//------------------------------------------------------------------------------ + +void Clipper::InsertEdgeIntoAEL(TEdge *edge) +{ + edge->prevInAEL = 0; + edge->nextInAEL = 0; + if( !m_ActiveEdges ) + { + m_ActiveEdges = edge; + } + else if( E2InsertsBeforeE1(*m_ActiveEdges, *edge) ) + { + edge->nextInAEL = m_ActiveEdges; + m_ActiveEdges->prevInAEL = edge; + m_ActiveEdges = edge; + } else + { + TEdge* e = m_ActiveEdges; + while( e->nextInAEL && !E2InsertsBeforeE1(*e->nextInAEL , *edge) ) + e = e->nextInAEL; + edge->nextInAEL = e->nextInAEL; + if( e->nextInAEL ) e->nextInAEL->prevInAEL = edge; + edge->prevInAEL = e; + e->nextInAEL = edge; + } +} +//---------------------------------------------------------------------- + +bool Clipper::JoinPoints(const JoinRec *j, OutPt *&p1, OutPt *&p2) +{ + OutRec *outRec1 = m_PolyOuts[j->poly1Idx]; + OutRec *outRec2 = m_PolyOuts[j->poly2Idx]; + if (!outRec1 || !outRec2) return false; + OutPt *pp1a = outRec1->pts; + OutPt *pp2a = outRec2->pts; + IntPoint pt1 = j->pt2a, pt2 = j->pt2b; + IntPoint pt3 = j->pt1a, pt4 = j->pt1b; + if (!FindSegment(pp1a, m_UseFullRange, pt1, pt2)) return false; + if (outRec1 == outRec2) + { + //we're searching the same polygon for overlapping segments so + //segment 2 mustn't be the same as segment 1 ... + pp2a = pp1a->next; + if (!FindSegment(pp2a, m_UseFullRange, pt3, pt4) || (pp2a == pp1a)) + return false; + } + else if (!FindSegment(pp2a, m_UseFullRange, pt3, pt4)) return false; + + if (!GetOverlapSegment(pt1, pt2, pt3, pt4, pt1, pt2)) return false; + + OutPt *p3, *p4, *prev = pp1a->prev; + //get p1 & p2 polypts - the overlap start & endpoints on poly1 + if (PointsEqual(pp1a->pt, pt1)) p1 = pp1a; + else if (PointsEqual(prev->pt, pt1)) p1 = prev; + else p1 = InsertPolyPtBetween(pp1a, prev, pt1); + + if (PointsEqual(pp1a->pt, pt2)) p2 = pp1a; + else if (PointsEqual(prev->pt, pt2)) p2 = prev; + else if ((p1 == pp1a) || (p1 == prev)) + p2 = InsertPolyPtBetween(pp1a, prev, pt2); + else if (Pt3IsBetweenPt1AndPt2(pp1a->pt, p1->pt, pt2)) + p2 = InsertPolyPtBetween(pp1a, p1, pt2); else + p2 = InsertPolyPtBetween(p1, prev, pt2); + + //get p3 & p4 polypts - the overlap start & endpoints on poly2 + prev = pp2a->prev; + if (PointsEqual(pp2a->pt, pt1)) p3 = pp2a; + else if (PointsEqual(prev->pt, pt1)) p3 = prev; + else p3 = InsertPolyPtBetween(pp2a, prev, pt1); + + if (PointsEqual(pp2a->pt, pt2)) p4 = pp2a; + else if (PointsEqual(prev->pt, pt2)) p4 = prev; + else if ((p3 == pp2a) || (p3 == prev)) + p4 = InsertPolyPtBetween(pp2a, prev, pt2); + else if (Pt3IsBetweenPt1AndPt2(pp2a->pt, p3->pt, pt2)) + p4 = InsertPolyPtBetween(pp2a, p3, pt2); else + p4 = InsertPolyPtBetween(p3, prev, pt2); + + //p1.pt == p3.pt and p2.pt == p4.pt so join p1 to p3 and p2 to p4 ... + if (p1->next == p2 && p3->prev == p4) + { + p1->next = p3; + p3->prev = p1; + p2->prev = p4; + p4->next = p2; + return true; + } + else if (p1->prev == p2 && p3->next == p4) + { + p1->prev = p3; + p3->next = p1; + p2->next = p4; + p4->prev = p2; + return true; + } + else + return false; //an orientation is probably wrong +} +//---------------------------------------------------------------------- + +void Clipper::FixupJoinRecs(JoinRec *j, OutPt *pt, unsigned startIdx) +{ + for (JoinList::size_type k = startIdx; k < m_Joins.size(); k++) + { + JoinRec* j2 = m_Joins[k]; + if (j2->poly1Idx == j->poly1Idx && PointIsVertex(j2->pt1a, pt)) + j2->poly1Idx = j->poly2Idx; + if (j2->poly2Idx == j->poly1Idx && PointIsVertex(j2->pt2a, pt)) + j2->poly2Idx = j->poly2Idx; + } +} +//---------------------------------------------------------------------- + +bool Poly2ContainsPoly1(OutPt* outPt1, OutPt* outPt2, bool UseFullInt64Range) +{ + OutPt* pt = outPt1; + //Because the polygons may be touching, we need to find a vertex that + //isn't touching the other polygon ... + if (PointOnPolygon(pt->pt, outPt2, UseFullInt64Range)) + { + pt = pt->next; + while (pt != outPt1 && PointOnPolygon(pt->pt, outPt2, UseFullInt64Range)) + pt = pt->next; + if (pt == outPt1) return true; + } + return PointInPolygon(pt->pt, outPt2, UseFullInt64Range); +} +//---------------------------------------------------------------------- + +void Clipper::FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec) +{ + + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + { + OutRec* outRec = m_PolyOuts[i]; + if (outRec->pts && outRec->FirstLeft == OldOutRec) + { + if (Poly2ContainsPoly1(outRec->pts, NewOutRec->pts, m_UseFullRange)) + outRec->FirstLeft = NewOutRec; + } + } +} +//---------------------------------------------------------------------- + +void Clipper::FixupFirstLefts2(OutRec* OldOutRec, OutRec* NewOutRec) +{ + for (PolyOutList::size_type i = 0; i < m_PolyOuts.size(); ++i) + { + OutRec* outRec = m_PolyOuts[i]; + if (outRec->FirstLeft == OldOutRec) outRec->FirstLeft = NewOutRec; + } +} +//---------------------------------------------------------------------- + +void Clipper::JoinCommonEdges() +{ + for (JoinList::size_type i = 0; i < m_Joins.size(); i++) + { + JoinRec* j = m_Joins[i]; + + OutRec *outRec1 = GetOutRec(j->poly1Idx); + OutRec *outRec2 = GetOutRec(j->poly2Idx); + + if (!outRec1->pts || !outRec2->pts) continue; + + //get the polygon fragment with the correct hole state (FirstLeft) + //before calling JoinPoints() ... + OutRec *holeStateRec; + if (outRec1 == outRec2) holeStateRec = outRec1; + else if (Param1RightOfParam2(outRec1, outRec2)) holeStateRec = outRec2; + else if (Param1RightOfParam2(outRec2, outRec1)) holeStateRec = outRec1; + else holeStateRec = GetLowermostRec(outRec1, outRec2); + + OutPt *p1, *p2; + if (!JoinPoints(j, p1, p2)) continue; + + if (outRec1 == outRec2) + { + //instead of joining two polygons, we've just created a new one by + //splitting one polygon into two. + outRec1->pts = p1; + outRec1->bottomPt = 0; + outRec2 = CreateOutRec(); + outRec2->pts = p2; + + if (Poly2ContainsPoly1(outRec2->pts, outRec1->pts, m_UseFullRange)) + { + //outRec2 is contained by outRec1 ... + outRec2->isHole = !outRec1->isHole; + outRec2->FirstLeft = outRec1; + + FixupJoinRecs(j, p2, i+1); + + //fixup FirstLeft pointers that may need reassigning to OutRec1 + if (m_UsingPolyTree) FixupFirstLefts2(outRec2, outRec1); + + FixupOutPolygon(*outRec1); //nb: do this BEFORE testing orientation + FixupOutPolygon(*outRec2); // but AFTER calling FixupJoinRecs() + + + if ((outRec2->isHole ^ m_ReverseOutput) == (Area(*outRec2, m_UseFullRange) > 0)) + ReversePolyPtLinks(outRec2->pts); + + } else if (Poly2ContainsPoly1(outRec1->pts, outRec2->pts, m_UseFullRange)) + { + //outRec1 is contained by outRec2 ... + outRec2->isHole = outRec1->isHole; + outRec1->isHole = !outRec2->isHole; + outRec2->FirstLeft = outRec1->FirstLeft; + outRec1->FirstLeft = outRec2; + + FixupJoinRecs(j, p2, i+1); + + //fixup FirstLeft pointers that may need reassigning to OutRec1 + if (m_UsingPolyTree) FixupFirstLefts2(outRec1, outRec2); + + FixupOutPolygon(*outRec1); //nb: do this BEFORE testing orientation + FixupOutPolygon(*outRec2); // but AFTER calling FixupJoinRecs() + + if ((outRec1->isHole ^ m_ReverseOutput) == (Area(*outRec1, m_UseFullRange) > 0)) + ReversePolyPtLinks(outRec1->pts); + } + else + { + //the 2 polygons are completely separate ... + outRec2->isHole = outRec1->isHole; + outRec2->FirstLeft = outRec1->FirstLeft; + + FixupJoinRecs(j, p2, i+1); + + //fixup FirstLeft pointers that may need reassigning to OutRec2 + if (m_UsingPolyTree) FixupFirstLefts1(outRec1, outRec2); + + FixupOutPolygon(*outRec1); //nb: do this BEFORE testing orientation + FixupOutPolygon(*outRec2); // but AFTER calling FixupJoinRecs() + } + + } else + { + //joined 2 polygons together ... + + //cleanup redundant edges ... + FixupOutPolygon(*outRec1); + + outRec2->pts = 0; + outRec2->bottomPt = 0; + outRec2->idx = outRec1->idx; + + outRec1->isHole = holeStateRec->isHole; + if (holeStateRec == outRec2) + outRec1->FirstLeft = outRec2->FirstLeft; + outRec2->FirstLeft = outRec1; + + //fixup FirstLeft pointers that may need reassigning to OutRec1 + if (m_UsingPolyTree) FixupFirstLefts2(outRec2, outRec1); + } + } +} +//------------------------------------------------------------------------------ + +inline void UpdateOutPtIdxs(OutRec& outrec) +{ + OutPt* op = outrec.pts; + do + { + op->idx = outrec.idx; + op = op->prev; + } + while(op != outrec.pts); +} +//------------------------------------------------------------------------------ + +void Clipper::DoSimplePolygons() +{ + PolyOutList::size_type i = 0; + while (i < m_PolyOuts.size()) + { + OutRec* outrec = m_PolyOuts[i++]; + OutPt* op = outrec->pts; + if (!op) continue; + do //for each Pt in Polygon until duplicate found do ... + { + OutPt* op2 = op->next; + while (op2 != outrec->pts) + { + if (PointsEqual(op->pt, op2->pt) && op2->next != op && op2->prev != op) + { + //split the polygon into two ... + OutPt* op3 = op->prev; + OutPt* op4 = op2->prev; + op->prev = op4; + op4->next = op; + op2->prev = op3; + op3->next = op2; + + outrec->pts = op; + OutRec* outrec2 = CreateOutRec(); + outrec2->pts = op2; + UpdateOutPtIdxs(*outrec2); + if (Poly2ContainsPoly1(outrec2->pts, outrec->pts, m_UseFullRange)) + { + //OutRec2 is contained by OutRec1 ... + outrec2->isHole = !outrec->isHole; + outrec2->FirstLeft = outrec; + } + else + if (Poly2ContainsPoly1(outrec->pts, outrec2->pts, m_UseFullRange)) + { + //OutRec1 is contained by OutRec2 ... + outrec2->isHole = outrec->isHole; + outrec->isHole = !outrec2->isHole; + outrec2->FirstLeft = outrec->FirstLeft; + outrec->FirstLeft = outrec2; + } else + { + //the 2 polygons are separate ... + outrec2->isHole = outrec->isHole; + outrec2->FirstLeft = outrec->FirstLeft; + } + op2 = op; //ie get ready for the next iteration + } + op2 = op2->next; + } + op = op->next; + } + while (op != outrec->pts); + } +} +//------------------------------------------------------------------------------ + +void ReversePolygon(Polygon& p) +{ + std::reverse(p.begin(), p.end()); +} +//------------------------------------------------------------------------------ + +void ReversePolygons(Polygons& p) +{ + for (Polygons::size_type i = 0; i < p.size(); ++i) + ReversePolygon(p[i]); +} + +//------------------------------------------------------------------------------ +// OffsetPolygon functions ... +//------------------------------------------------------------------------------ + +struct DoublePoint +{ + double X; + double Y; + DoublePoint(double x = 0, double y = 0) : X(x), Y(y) {} +}; +//------------------------------------------------------------------------------ + +Polygon BuildArc(const IntPoint &pt, + const double a1, const double a2, const double r, double limit) +{ + //see notes in clipper.pas regarding steps + double arcFrac = std::fabs(a2 - a1) / (2 * pi); + int steps = (int)(arcFrac * pi / std::acos(1 - limit / std::fabs(r))); + if (steps < 2) steps = 2; + else if (steps > (int)(222.0 * arcFrac)) steps = (int)(222.0 * arcFrac); + + double x = std::cos(a1); + double y = std::sin(a1); + double c = std::cos((a2 - a1) / steps); + double s = std::sin((a2 - a1) / steps); + Polygon result(steps +1); + for (int i = 0; i <= steps; ++i) + { + result[i].X = pt.X + Round(x * r); + result[i].Y = pt.Y + Round(y * r); + double x2 = x; + x = x * c - s * y; //cross product + y = x2 * s + y * c; //dot product + } + return result; +} +//------------------------------------------------------------------------------ + +DoublePoint GetUnitNormal(const IntPoint &pt1, const IntPoint &pt2) +{ + if(pt2.X == pt1.X && pt2.Y == pt1.Y) + return DoublePoint(0, 0); + + double dx = (double)(pt2.X - pt1.X); + double dy = (double)(pt2.Y - pt1.Y); + double f = 1 *1.0/ std::sqrt( dx*dx + dy*dy ); + dx *= f; + dy *= f; + return DoublePoint(dy, -dx); +} + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ + +class PolyOffsetBuilder +{ +private: + const Polygons& m_p; + Polygon* m_curr_poly; + std::vector normals; + double m_delta, m_rmin, m_r; + size_t m_i, m_j, m_k; + static const int buffLength = 128; + +public: + +PolyOffsetBuilder(const Polygons& in_polys, Polygons& out_polys, + bool isPolygon, double delta, JoinType jointype, EndType endtype, double limit): m_p(in_polys) +{ + //precondition: &out_polys != &in_polys + + if (NEAR_ZERO(delta)) {out_polys = in_polys; return;} + m_rmin = 0.5; + m_delta = delta; + if (jointype == jtMiter) + { + if (limit > 2) m_rmin = 2.0 / (limit * limit); + limit = 0.25; //just in case endtype == etRound + } + else + { + if (limit <= 0) limit = 0.25; + else if (limit > std::fabs(delta)) limit = std::fabs(delta); + } + + double deltaSq = delta*delta; + out_polys.clear(); + out_polys.resize(m_p.size()); + for (m_i = 0; m_i < m_p.size(); m_i++) + { + size_t len = m_p[m_i].size(); + if (len > 1 && m_p[m_i][0].X == m_p[m_i][len - 1].X && + m_p[m_i][0].Y == m_p[m_i][len-1].Y) len--; + + if (len == 0 || (len < 3 && delta <= 0)) + continue; + else if (len == 1) + { + out_polys[m_i] = BuildArc(m_p[m_i][0], 0, 2*pi, delta, limit); + continue; + } + + //build normals ... + normals.clear(); + normals.resize(len); + for (m_j = 0; m_j < len -1; ++m_j) + normals[m_j] = GetUnitNormal(m_p[m_i][m_j], m_p[m_i][m_j +1]); + if (isPolygon) + normals[len-1] = GetUnitNormal(m_p[m_i][len-1], m_p[m_i][0]); + else //is polyline + normals[len-1] = normals[len-2]; + + m_curr_poly = &out_polys[m_i]; + if (isPolygon) + { + m_k = len -1; + for (m_j = 0; m_j < len; ++m_j) + OffsetPoint(jointype, limit); + } + else //is polyline + { + //offset the polyline going forward ... + m_k = 0; + for (m_j = 1; m_j < len -1; ++m_j) + OffsetPoint(jointype, limit); + + //handle the end (butt, round or square) ... + IntPoint pt1; + if (endtype == etButt) + { + m_j = len - 1; + pt1 = IntPoint(Round(m_p[m_i][m_j].X + normals[m_j].X * m_delta), + Round(m_p[m_i][m_j].Y + normals[m_j].Y * m_delta)); + AddPoint(pt1); + pt1 = IntPoint(Round(m_p[m_i][m_j].X - normals[m_j].X * m_delta), + Round(m_p[m_i][m_j].Y - normals[m_j].Y * m_delta)); + AddPoint(pt1); + } + else + { + m_j = len - 1; + m_k = len - 2; + normals[m_j].X = -normals[m_j].X; + normals[m_j].Y = -normals[m_j].Y; + if (endtype == etSquare) DoSquare(); + else DoRound(limit); + } + + //re-build Normals ... + for (int j = len - 1; j > 0; --j) + { + normals[j].X = -normals[j - 1].X; + normals[j].Y = -normals[j - 1].Y; + } + normals[0].X = -normals[1].X; + normals[0].Y = -normals[1].Y; + + //offset the polyline going backward ... + m_k = len -1; + for (m_j = m_k - 1; m_j > 0; --m_j) + OffsetPoint(jointype, limit); + + //finally handle the start (butt, round or square) ... + if (endtype == etButt) + { + pt1 = IntPoint(Round(m_p[m_i][0].X - normals[0].X * m_delta), + Round(m_p[m_i][0].Y - normals[0].Y * m_delta)); + AddPoint(pt1); + pt1 = IntPoint(Round(m_p[m_i][0].X + normals[0].X * m_delta), + Round(m_p[m_i][0].Y + normals[0].Y * m_delta)); + AddPoint(pt1); + } else + { + m_k = 1; + if (endtype == etSquare) DoSquare(); + else DoRound(limit); + } + } + } + + //and clean up untidy corners using Clipper ... + Clipper clpr; + clpr.AddPolygons(out_polys, ptSubject); + if (delta > 0) + { + if (!clpr.Execute(ctUnion, out_polys, pftPositive, pftPositive)) + out_polys.clear(); + } + else + { + IntRect r = clpr.GetBounds(); + Polygon outer(4); + outer[0] = IntPoint(r.left - 10, r.bottom + 10); + outer[1] = IntPoint(r.right + 10, r.bottom + 10); + outer[2] = IntPoint(r.right + 10, r.top - 10); + outer[3] = IntPoint(r.left - 10, r.top - 10); + + clpr.AddPolygon(outer, ptSubject); + if (clpr.Execute(ctUnion, out_polys, pftNegative, pftNegative)) + { + out_polys.erase(out_polys.begin()); + ReversePolygons(out_polys); + + } else + out_polys.clear(); + } +} +//------------------------------------------------------------------------------ + +private: + +void OffsetPoint(JoinType jointype, double limit) +{ + switch (jointype) + { + case jtMiter: + { + m_r = 1 + (normals[m_j].X*normals[m_k].X + + normals[m_j].Y*normals[m_k].Y); + if (m_r >= m_rmin) DoMiter(); else DoSquare(); + break; + } + case jtSquare: DoSquare(); break; + case jtRound: DoRound(limit); break; + } + m_k = m_j; +} +//------------------------------------------------------------------------------ + +void AddPoint(const IntPoint& pt) +{ + if (m_curr_poly->size() == m_curr_poly->capacity()) + m_curr_poly->reserve(m_curr_poly->capacity() + buffLength); + m_curr_poly->push_back(pt); +} +//------------------------------------------------------------------------------ + +void DoSquare() +{ + IntPoint pt1 = IntPoint(Round(m_p[m_i][m_j].X + normals[m_k].X * m_delta), + Round(m_p[m_i][m_j].Y + normals[m_k].Y * m_delta)); + IntPoint pt2 = IntPoint(Round(m_p[m_i][m_j].X + normals[m_j].X * m_delta), + Round(m_p[m_i][m_j].Y + normals[m_j].Y * m_delta)); + if ((normals[m_k].X * normals[m_j].Y - normals[m_j].X * normals[m_k].Y) * m_delta >= 0) + { + double a1 = std::atan2(normals[m_k].Y, normals[m_k].X); + double a2 = std::atan2(-normals[m_j].Y, -normals[m_j].X); + a1 = std::fabs(a2 - a1); + if (a1 > pi) a1 = pi * 2 - a1; + double dx = std::tan((pi - a1) / 4) * std::fabs(m_delta); + pt1 = IntPoint((long64)(pt1.X -normals[m_k].Y * dx), (long64)(pt1.Y + normals[m_k].X * dx)); + AddPoint(pt1); + pt2 = IntPoint((long64)(pt2.X + normals[m_j].Y * dx), (long64)(pt2.Y -normals[m_j].X * dx)); + AddPoint(pt2); + } + else + { + AddPoint(pt1); + AddPoint(m_p[m_i][m_j]); + AddPoint(pt2); + } +} +//------------------------------------------------------------------------------ + +void DoMiter() +{ + if ((normals[m_k].X * normals[m_j].Y - normals[m_j].X * normals[m_k].Y) * m_delta >= 0) + { + double q = m_delta / m_r; + AddPoint(IntPoint(Round(m_p[m_i][m_j].X + (normals[m_k].X + normals[m_j].X) * q), + Round(m_p[m_i][m_j].Y + (normals[m_k].Y + normals[m_j].Y) * q))); + } + else + { + IntPoint pt1 = IntPoint(Round(m_p[m_i][m_j].X + normals[m_k].X * m_delta), + Round(m_p[m_i][m_j].Y + normals[m_k].Y * m_delta)); + IntPoint pt2 = IntPoint(Round(m_p[m_i][m_j].X + normals[m_j].X * m_delta), + Round(m_p[m_i][m_j].Y + normals[m_j].Y * m_delta)); + AddPoint(pt1); + AddPoint(m_p[m_i][m_j]); + AddPoint(pt2); + } +} +//------------------------------------------------------------------------------ + +void DoRound(double limit) +{ + IntPoint pt1 = IntPoint(Round(m_p[m_i][m_j].X + normals[m_k].X * m_delta), + Round(m_p[m_i][m_j].Y + normals[m_k].Y * m_delta)); + IntPoint pt2 = IntPoint(Round(m_p[m_i][m_j].X + normals[m_j].X * m_delta), + Round(m_p[m_i][m_j].Y + normals[m_j].Y * m_delta)); + AddPoint(pt1); + //round off reflex angles (ie > 180 deg) unless almost flat (ie < ~10deg). + if ((normals[m_k].X*normals[m_j].Y - normals[m_j].X*normals[m_k].Y) * m_delta >= 0) + { + if (normals[m_j].X * normals[m_k].X + normals[m_j].Y * normals[m_k].Y < 0.985) + { + double a1 = std::atan2(normals[m_k].Y, normals[m_k].X); + double a2 = std::atan2(normals[m_j].Y, normals[m_j].X); + if (m_delta > 0 && a2 < a1) a2 += pi *2; + else if (m_delta < 0 && a2 > a1) a2 -= pi *2; + Polygon arc = BuildArc(m_p[m_i][m_j], a1, a2, m_delta, limit); + for (Polygon::size_type m = 0; m < arc.size(); m++) + AddPoint(arc[m]); + } + } + else + AddPoint(m_p[m_i][m_j]); + AddPoint(pt2); +} +//-------------------------------------------------------------------------- + +}; //end PolyOffsetBuilder + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ + +bool UpdateBotPt(const IntPoint &pt, IntPoint &botPt) +{ + if (pt.Y > botPt.Y || (pt.Y == botPt.Y && pt.X < botPt.X)) + { + botPt = pt; + return true; + } + else return false; +} +//-------------------------------------------------------------------------- + +void OffsetPolygons(const Polygons &in_polys, Polygons &out_polys, + double delta, JoinType jointype, double limit, bool autoFix) +{ + if (!autoFix && &in_polys != &out_polys) + { + PolyOffsetBuilder(in_polys, out_polys, true, delta, jointype, etClosed, limit); + return; + } + + Polygons inPolys = Polygons(in_polys); + out_polys.clear(); + + //ChecksInput - fixes polygon orientation if necessary and removes + //duplicate vertices. Can be set false when you're sure that polygon + //orientation is correct and that there are no duplicate vertices. + if (autoFix) + { + size_t polyCount = inPolys.size(), botPoly = 0; + while (botPoly < polyCount && inPolys[botPoly].empty()) botPoly++; + if (botPoly == polyCount) return; + + //botPt: used to find the lowermost (in inverted Y-axis) & leftmost point + //This point (on m_p[botPoly]) must be on an outer polygon ring and if + //its orientation is false (counterclockwise) then assume all polygons + //need reversing ... + IntPoint botPt = inPolys[botPoly][0]; + for (size_t i = botPoly; i < polyCount; ++i) + { + if (inPolys[i].size() < 3) { inPolys[i].clear(); continue; } + if (UpdateBotPt(inPolys[i][0], botPt)) botPoly = i; + Polygon::iterator it = inPolys[i].begin() +1; + while (it != inPolys[i].end()) + { + if (PointsEqual(*it, *(it -1))) + it = inPolys[i].erase(it); + else + { + if (UpdateBotPt(*it, botPt)) botPoly = i; + ++it; + } + } + } + if (!Orientation(inPolys[botPoly])) + ReversePolygons(inPolys); + } + PolyOffsetBuilder(inPolys, out_polys, true, delta, jointype, etClosed, limit); +} +//------------------------------------------------------------------------------ + +void OffsetPolyLines(const Polygons &in_lines, Polygons &out_lines, + double delta, JoinType jointype, EndType endtype, + double limit, bool autoFix) +{ + if (!autoFix && endtype != etClosed && &in_lines != &out_lines) + { + PolyOffsetBuilder(in_lines, out_lines, false, delta, jointype, endtype, limit); + return; + } + + Polygons inLines = Polygons(in_lines); + if (autoFix) + for (size_t i = 0; i < inLines.size(); ++i) + { + if (inLines[i].size() < 2) { inLines[i].clear(); continue; } + Polygon::iterator it = inLines[i].begin() +1; + while (it != inLines[i].end()) + { + if (PointsEqual(*it, *(it -1))) + it = inLines[i].erase(it); + else + ++it; + } + } + + if (endtype == etClosed) + { + size_t sz = inLines.size(); + inLines.resize(sz * 2); + for (size_t i = 0; i < sz; ++i) + { + inLines[sz+i] = inLines[i]; + ReversePolygon(inLines[sz+i]); + } + PolyOffsetBuilder(inLines, out_lines, true, delta, jointype, endtype, limit); + } + else + PolyOffsetBuilder(inLines, out_lines, false, delta, jointype, endtype, limit); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygon(const Polygon &in_poly, Polygons &out_polys, PolyFillType fillType) +{ + Clipper c; + c.ForceSimple(true); + c.AddPolygon(in_poly, ptSubject); + c.Execute(ctUnion, out_polys, fillType, fillType); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygons(const Polygons &in_polys, Polygons &out_polys, PolyFillType fillType) +{ + Clipper c; + c.ForceSimple(true); + c.AddPolygons(in_polys, ptSubject); + c.Execute(ctUnion, out_polys, fillType, fillType); +} +//------------------------------------------------------------------------------ + +void SimplifyPolygons(Polygons &polys, PolyFillType fillType) +{ + SimplifyPolygons(polys, polys, fillType); +} +//------------------------------------------------------------------------------ + +inline double DistanceSqrd(const IntPoint& pt1, const IntPoint& pt2) +{ + double dx = ((double)pt1.X - pt2.X); + double dy = ((double)pt1.Y - pt2.Y); + return (dx*dx + dy*dy); +} +//------------------------------------------------------------------------------ + +DoublePoint ClosestPointOnLine(const IntPoint& pt, const IntPoint& linePt1, const IntPoint& linePt2) +{ + double dx = ((double)linePt2.X - linePt1.X); + double dy = ((double)linePt2.Y - linePt1.Y); + if (dx == 0 && dy == 0) + return DoublePoint((double)linePt1.X, (double)linePt1.Y); + double q = ((pt.X-linePt1.X)*dx + (pt.Y-linePt1.Y)*dy) / (dx*dx + dy*dy); + return DoublePoint( + (1-q)*linePt1.X + q*linePt2.X, + (1-q)*linePt1.Y + q*linePt2.Y); +} +//------------------------------------------------------------------------------ + +bool SlopesNearColinear(const IntPoint& pt1, + const IntPoint& pt2, const IntPoint& pt3, double distSqrd) +{ + if (DistanceSqrd(pt1, pt2) > DistanceSqrd(pt1, pt3)) return false; + DoublePoint cpol = ClosestPointOnLine(pt2, pt1, pt3); + double dx = pt2.X - cpol.X; + double dy = pt2.Y - cpol.Y; + return (dx*dx + dy*dy) < distSqrd; +} +//------------------------------------------------------------------------------ + +bool PointsAreClose(IntPoint pt1, IntPoint pt2, double distSqrd) +{ + double dx = (double)pt1.X - pt2.X; + double dy = (double)pt1.Y - pt2.Y; + return ((dx * dx) + (dy * dy) <= distSqrd); +} +//------------------------------------------------------------------------------ + +void CleanPolygon(const Polygon& in_poly, Polygon& out_poly, double distance) +{ + //distance = proximity in units/pixels below which vertices + //will be stripped. Default ~= sqrt(2). + int highI = in_poly.size() -1; + double distSqrd = distance * distance; + while (highI > 0 && PointsAreClose(in_poly[highI], in_poly[0], distSqrd)) highI--; + if (highI < 2) { out_poly.clear(); return; } + + if (&in_poly != &out_poly) + out_poly.resize(highI + 1); + + IntPoint pt = in_poly[highI]; + int i = 0, k = 0; + for (;;) + { + while (i < highI && PointsAreClose(pt, in_poly[i+1], distSqrd)) i+=2; + int i2 = i; + while (i < highI && (PointsAreClose(in_poly[i], in_poly[i+1], distSqrd) || + SlopesNearColinear(pt, in_poly[i], in_poly[i+1], distSqrd))) i++; + if (i >= highI) break; + else if (i != i2) continue; + pt = in_poly[i++]; + out_poly[k++] = pt; + } + if (i <= highI) out_poly[k++] = in_poly[i]; + if (k > 2 && SlopesNearColinear(out_poly[k -2], out_poly[k -1], out_poly[0], distSqrd)) k--; + if (k < 3) out_poly.clear(); + else if (k <= highI) out_poly.resize(k); +} +//------------------------------------------------------------------------------ + +void CleanPolygons(const Polygons& in_polys, Polygons& out_polys, double distance) +{ + for (Polygons::size_type i = 0; i < in_polys.size(); ++i) + CleanPolygon(in_polys[i], out_polys[i], distance); +} +//------------------------------------------------------------------------------ + +void AddPolyNodeToPolygons(const PolyNode& polynode, Polygons& polygons) +{ + if (!polynode.Contour.empty()) + polygons.push_back(polynode.Contour); + for (int i = 0; i < polynode.ChildCount(); ++i) + AddPolyNodeToPolygons(*polynode.Childs[i], polygons); +} +//------------------------------------------------------------------------------ + +void PolyTreeToPolygons(const PolyTree& polytree, Polygons& polygons) +{ + polygons.resize(0); + polygons.reserve(polytree.Total()); + AddPolyNodeToPolygons(polytree, polygons); +} +//------------------------------------------------------------------------------ + +std::ostream& operator <<(std::ostream &s, IntPoint& p) +{ + s << p.X << ' ' << p.Y << "\n"; + return s; +} +//------------------------------------------------------------------------------ + +std::ostream& operator <<(std::ostream &s, Polygon &p) +{ + for (Polygon::size_type i = 0; i < p.size(); i++) + s << p[i]; + s << "\n"; + return s; +} +//------------------------------------------------------------------------------ + +std::ostream& operator <<(std::ostream &s, Polygons &p) +{ + for (Polygons::size_type i = 0; i < p.size(); i++) + s << p[i]; + s << "\n"; + return s; +} +//------------------------------------------------------------------------------ + +} //ClipperLib namespace diff --git a/third_party/clipper/clipper.hpp b/third_party/clipper/clipper.hpp new file mode 100644 index 0000000..86e9c04 --- /dev/null +++ b/third_party/clipper/clipper.hpp @@ -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 +#include +#include +#include +#include + +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(static_cast(a) | static_cast(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 + +