Lot of bugfixes

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

No files matched your search

+21
View File
@@ -139,6 +139,26 @@ struct Canvas {
void frame(int x, int y, int rw, int rh, uint32_t c) {
rect(x, y, rw, 1, c); rect(x, y + rh - 1, rw, 1, c); rect(x, y, 1, rh, c); rect(x + rw - 1, y, 1, rh, c);
}
void line(int x0, int y0, int x1, int y1, uint32_t c) {
// Clip first: terrain fixtures may be far outside the camera.
double a = 0, b = 1;
const double dx = double(x1) - x0, dy = double(y1) - y0;
auto clip = [&](double p, double q) { if (p == 0) return q >= 0; const double t = q / p; if (p < 0) a = std::max(a, t); else b = std::min(b, t); return a <= b; };
if (!clip(-dx, x0) || !clip(dx, w - 1 - x0) || !clip(-dy, y0) || !clip(dy, h - 1 - y0)) return;
const int ax = (int)std::lround(x0 + a * dx), ay = (int)std::lround(y0 + a * dy);
const int bx = (int)std::lround(x0 + b * dx), by = (int)std::lround(y0 + b * dy);
// Integer stepping avoids two floating divisions/rounds per debug-line pixel.
int x = ax, y = ay;
const int ix = std::abs(bx - ax), iy = -std::abs(by - ay), sx = ax < bx ? 1 : -1, sy = ay < by ? 1 : -1;
int error = ix + iy;
for (;;) {
put(x, y, c);
if (x == bx && y == by) break;
const int twice = 2 * error;
if (twice >= iy) { error += iy; x += sx; }
if (twice <= ix) { error += ix; y += sy; }
}
}
int text_width(const std::string& s, int scale) const {
if (font && font->ok()) { int w = 0; for (unsigned char ch : s) { const GameFont::Glyph& gl = font->g[ch]; w += (gl.ok ? gl.width : font->word_space) + font->char_space; } return w * scale; }
return (int)s.size() * 6 * scale - scale;
@@ -601,6 +621,7 @@ inline void draw(Canvas& cv, const Assets& as, const State& s) {
cv.text(cv.w - 14 - cv.text_width(t, 2), s.debug ? 90 : 14, t, rgba(255, 220, 120), 2);
}
if (s.debug) {
cv.text(14, cv.h - 30, "F3+C colliders F3+L save physics dump (debug folder)", rgba(160, 255, 160), 1);
const std::string l1 = fmt("%.0f fps", s.fps) + " frame " + std::to_string(s.frame);
const std::string l2 = "player " + fmt("%.0f", s.px) + ", " + fmt("%.0f", s.py) + " view " + std::to_string(s.view_x) + ", " + std::to_string(s.view_y);
const std::string l3 = "entities " + std::to_string(s.entities) + " stored tiles " + std::to_string(s.stored_tiles) + " " + s.seed_text;
+525 -150
View File
File diff suppressed because it is too large. Load diff
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "phys/phys_system.h"
#include <deque>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <string>
namespace ng {
// Enabled by F3 only; retain three seconds of simulation poses, not render frames.
struct RigidDebug {
struct Row { phys::PhysSystem::DebugBody b; phys::PhysSystem::Lifecycle lifecycle; int slot = -1, entity = 0, flags = 0, material = -1; std::string file; };
struct Sample { int frame; std::vector<Row> rows; };
std::deque<Sample> history;
void append(int frame, std::vector<Row> rows) {
history.push_back({frame, std::move(rows)});
while (history.size() > 180) history.pop_front();
}
bool save(const std::filesystem::path& dir, const phys::PhysSystem& physics, const std::string& metadata) const {
std::error_code ec; std::filesystem::create_directories(dir, ec);
if (ec) return false;
std::ofstream meta(dir / "context.txt"), poses(dir / "poses.csv"), polygons(dir / "colliders.csv"), contacts(dir / "contacts.csv");
if (!meta || !poses || !polygons || !contacts) return false;
meta << metadata << "\nCoordinates: world pixels; velocities: pixels/frame; angles: radians.\n"
"Body -1 is cell terrain. Collider radius is the solver skin, in pixels.\nposes.csv retains up to 180 ticks while F3 is enabled; body slots can be reused.\n";
poses << std::setprecision(12) << "frame,body,cpu,shape,slot,entity,file,material,flags,type,group,x,y,angle,vx,vy,omega,mass,awake,active,frozen,bullet,nailed,born_tick,sleep_convert,conversion_target,settle_static\n";
for (const auto& sample : history) for (const auto& r : sample.rows) {
const auto& b = r.b;
poses << sample.frame << ',' << b.index << ',' << b.cpu << ',' << b.shape << ',' << r.slot << ',' << r.entity << ',' << std::quoted(r.file) << ',' << r.material << ',' << r.flags << ',' << b.type << ',' << b.group << ','
<< b.x << ',' << b.y << ',' << b.angle << ',' << b.vx << ',' << b.vy << ',' << b.omega << ',' << b.mass << ',' << b.awake << ',' << b.active << ',' << b.frozen << ',' << b.bullet << ',' << b.nailed << ',' << r.lifecycle.born << ',' << r.lifecycle.sleep_convert << ',' << r.lifecycle.target << ',' << r.lifecycle.settle_static << '\n';
}
polygons << std::setprecision(12) << "polygon,body,fixture,category,mask,group,sensor,radius,vertex,x,y\n";
int pi = 0;
for (const auto& p : physics.debug_polygons()) {
for (int k = 0; k < (int)p.points.size(); ++k) polygons << pi << ',' << p.body << ',' << p.fixture << ',' << p.category << ',' << p.mask << ',' << p.group << ',' << p.sensor << ',' << p.radius << ',' << k << ',' << p.points[k][0] << ',' << p.points[k][1] << '\n';
++pi;
}
contacts << std::setprecision(12) << "body_a,body_b,x,y,separation\n";
for (const auto& c : physics.debug_contacts()) contacts << c.a << ',' << c.b << ',' << c.x << ',' << c.y << ',' << c.separation << '\n';
return bool(meta && poses && polygons && contacts);
}
};
}
+4
View File
@@ -63,6 +63,7 @@ void apply_attrs(Material& m, const XmlNode& n) {
} // namespace
void MaterialDb::load(std::string_view xml_text) {
canonical_.clear();
auto root = parse_xml(xml_text);
if (root->name != "Materials") throw std::runtime_error("materials: root is not <Materials>");
@@ -133,6 +134,8 @@ void MaterialDb::load(std::string_view xml_text) {
void MaterialDb::add_loose_twins() {
const size_t n = mats_.size();
loose_twin_.assign(n, -1);
canonical_.resize(n);
for (size_t i = 0; i < n; ++i) canonical_[i] = (int)i;
for (size_t i = 0; i < n; ++i) {
if (mats_[i].type != CellType::Liquid || attr_int(mats_[i], "liquid_static", 0) != 1 || attr_int(mats_[i], "liquid_sand", 0) != 1) continue;
Material t = mats_[i];
@@ -141,6 +144,7 @@ void MaterialDb::add_loose_twins() {
t.index = (int)mats_.size();
by_name_[t.name] = t.index;
loose_twin_[i] = t.index;
canonical_.push_back((int)i);
mats_.push_back(std::move(t));
}
loose_twin_.resize(mats_.size(), -1);
+4
View File
@@ -43,7 +43,11 @@ public:
void add_loose_twins();
int loose_twin(int index) const { return index >= 0 && index < (int)loose_twin_.size() ? loose_twin_[index] : -1; }
// Synthetic looseness changes the static byte, not native CellData identity.
int canonical(int index) const { return index >= 0 && index < (int)canonical_.size() ? canonical_[index] : index; }
private:
std::vector<int> canonical_;
std::vector<Material> mats_;
std::vector<Reaction> reactions_;
std::vector<int> loose_twin_;
+9 -4
View File
@@ -400,10 +400,8 @@ EntityState make_state(EntityDb& db, const Wak& wak, EntityLayer& L, std::map<st
e.ex4[2] = comp_f(dm, "air_lack_of_damage", 0.4f) / 60.0f;
}
}
if (dm && (e.st[0] == 1 || e.st[0] == 2) && mat_lookup) { // the blood: blood_material (the liquid a hit makes), else blood_spray_material, else "blood" (00ab9a90 picks the spray only for a recent entity)
std::string bm = dm->get("blood_material");
if (bm.empty()) bm = dm->get("blood_spray_material");
if (bm.empty()) bm = "blood";
if (dm && mat_lookup) { // 00ab9a90: missing uses blood_fading; explicit empty means none.
const std::string bm = dm->get("blood_material", "blood_fading");
const int id = bm.empty() ? -1 : mat_lookup(bm);
e.ex7[0] = comp_f(dm, "blood_multiplier", 1.0f);
if (id >= 0) { e.ex3[0] = id + 1; e.ex3[1] = (int32_t)(mat_color ? mat_color(bm) : 0xFFA01010u); }
@@ -520,6 +518,13 @@ int EntityLayerBuilder::add_body_entity(int tex, int w, int h, int ox, int oy, d
if (reuse >= 0 && reuse < (int)L_.ents.size()) {
const int idx = first_instance(reuse);
if (idx >= 0) {
// The released slot may have been a lantern-chain segment or a
// secondary prop body. Its new chunk is independent: otherwise
// makeRigid treats it as an old part and never creates a collider.
L_.body_part_slots.erase(reuse);
L_.body_parts.erase(reuse);
for (auto& [owner, parts] : L_.body_parts)
parts.erase(std::remove_if(parts.begin(), parts.end(), [&](const BodyPart& p) { return p.slot == reuse; }), parts.end());
L_.ents[reuse] = st;
si.ent = reuse;
L_.inst[idx] = si;
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <cmath>
#include <cstdint>
namespace ng {
// 00ab9a90 and real-particle constructor 00716af0. RNG supplied by caller.
inline int hit_liquid_count(float u, float entity, float incoming, int world) {
return int(((u * 20.0f + 20.0f) * entity + 0.5f) * incoming) * world;
}
inline bool hit_liquid_enabled(float damage, float incoming, int fx, bool frozen) {
return damage > 0.0f && incoming > 0.0f && fx != 0 && !frozen;
}
inline float projectile_hit_liquid_speed(float ragdollForce) {
return ragdollForce != 0.0f ? std::sqrt(0.005f / ragdollForce) : 1.0f;
}
inline bool inventory_hit_liquid_enabled(float damage, float hp, float probability, float threshold, float u, bool emitter) {
return probability > 0 && damage < hp && damage > threshold && u < probability && emitter;
}
inline void hit_liquid_local_point(float& x, float& y, float sx, float sy, float angle, bool hitbox, const float* bounds) {
if (sx != 0) x /= sx;
if (sy != 0) y /= sy;
const float c = std::cos(angle), s = std::sin(angle), nx = c * x + s * y;
y = -s * x + c * y; x = nx;
if (hitbox) {
x = x < bounds[0] ? bounds[0] : (x > bounds[1] ? bounds[1] : x);
y = y < bounds[2] ? bounds[2] : (y > bounds[3] ? bounds[3] : y);
} else if (x * x + y * y > 65536.0f) x = y = 0;
}
struct HitLiquidDrop { float x, y, vx, vy; };
inline HitLiquidDrop hit_liquid_drop(float x, float y, float& vx, float& vy,
float speedScale, float speedU, float angleU, float yU, float xU) {
const float a = -0.6f + 1.2f * angleU, c = std::cos(a), s = std::sin(a);
const float nx = vx * c - vy * s;
vy = vx * s + vy * c; vx = nx;
const float speed = (20.0f / 60.0f) * (0.5f + 0.75f * speedU) * speedScale;
return {x - 1.0f + 3.0f * xU, y - 1.0f + 3.0f * yU, vx * speed, vy * speed};
}
inline int ragdoll_hit_liquid_reserve(float u, float world, float ragdoll, double partMass, double totalMass, int absolute) {
const double amount = absolute < 0 || totalMass <= 0 ? partMass * 1000.0 : partMass / totalMass * absolute;
return int((0.8f + 0.40000004f * u) * float(int(world * ragdoll) * int(amount)));
}
// std430 ABI: binding 67; one entry per entity, then one per projectile type.
struct HitLiquidPolicy { uint32_t materials = 0, multiplier = 0, baseColour = 0, sprayColour = 0;
uint32_t flags = 0, reserved[3] = {};
float bounds[4] = {}; // min x, max x, min y, max y
float inventory[4] = {0, 0.09f, 1, 1}; // probability, minimum damage, entity scale x/y
float pressure[4] = {0.7f, 1.1f, 0, 0}; // pressure range, trail gap, reserved
};
struct HitLiquidTable {
int32_t settings[4] = {1, 0, 0, 0};
HitLiquidPolicy entities[4096];
float projectile[512][4] = {};
};
static_assert(sizeof(HitLiquidPolicy) == 80);
}
+51 -3
View File
@@ -155,6 +155,7 @@ int EntityRuntime::add_comp_internal(int entity, const std::string& type, std::m
void EntityRuntime::erase_comp_internal(int cid) {
RtComponent* c = comp(cid);
if (!c) return;
if (c->type == "LifetimeComponent" && c->attrs.count("_ng_blink_hidden") && alive(c->entity) && on_sprite_fade) on_sprite_fade(c->entity, 0);
update_component_fields(*c, -1);
auto& owners = entities_by_component_[c->type];
auto owner = owners.find(c->entity);
@@ -877,6 +878,7 @@ void EntityRuntime::set_component_enabled(int cid, bool en) {
c->enabled = en;
if (changed) if (auto* e = ent(c->entity)) e->component_revision = ++component_revision_;
if (changed && en && c->type == "LifetimeComponent") c->attrs["_ng_born"] = std::to_string(frame_); // its count starts when it is switched on (the tutorial stones' emitters: disabled until the player reaches the trigger)
if (changed && !en && c->type == "LifetimeComponent" && c->attrs.erase("_ng_blink_hidden") && on_sprite_fade) on_sprite_fade(c->entity, 0);
if (changed && c->type == "LuaComponent" && !c->attrs["script_enabled_changed"].empty()) // the component's own switch: script_enabled_changed( entity_id, is_enabled )
run_script_callback(c->entity, cid, c->attrs["script_enabled_changed"], "enabled_changed", {LVal::number(c->entity), LVal::boolean(en)});
if (changed && on_components_enabled) on_components_enabled(c->entity);
@@ -894,6 +896,19 @@ void EntityRuntime::set_component_enabled(int cid, bool en) {
}
}
bool EntityRuntime::component_enabled(int cid) { RtComponent* c = comp(cid); return c && c->enabled; }
bool EntityRuntime::item_emitter_enabled(int cid, int held_item) {
const RtComponent* c = comp(cid);
if (!c || !c->enabled) return false;
const bool world = comp_has_tag(cid, "enabled_in_world"), hand = comp_has_tag(cid, "enabled_in_hand"),
stored = comp_has_tag(cid, "enabled_in_inventory");
if (!world && !hand && !stored) return true;
const RtEntity* item = ent(c->entity);
if (!item) return false;
if (components(item->id, "ItemComponent", "", true).empty() &&
components(item->id, "ItemActionComponent", "", true).empty()) return true;
if (!item->parent) return world;
return item->id == held_item ? hand : stored;
}
std::string EntityRuntime::component_type(int cid) { RtComponent* c = comp(cid); return c ? c->type : ""; }
int EntityRuntime::component_entity(int cid) { RtComponent* c = comp(cid); return c ? c->entity : 0; }
bool EntityRuntime::comp_has_tag(int cid, const std::string& t) { RtComponent* c = comp(cid); return c && c->tags.count(t); }
@@ -1077,7 +1092,8 @@ std::string EntityRuntime::interact_hint(double px, double py) {
return best;
}
void EntityRuntime::material_area_step(const std::function<int(double, double, double, double, const std::string&, const std::string&, int*)>& query) {
void EntityRuntime::material_area_step(const std::function<int(double, double, double, double, const std::string&, const std::string&, int*)>& query,
const std::function<bool(double, double, double, double)>& ready) {
// 00bb5320: look_for_failure: Message_MaterialAreaCheckerFailed when a cell of the box (air too) is neither material nor material2 (the engine scans one row / column per update);
// otherwise Message_MaterialAreaCheckerSuccess when every cell is one of them, or at least count_min cells when count_min > 0
struct Job { int entity, comp; bool failure; double x0, y0, x1, y1; std::string m1, m2; int count_min; };
@@ -1092,10 +1108,27 @@ void EntityRuntime::material_area_step(const std::function<int(double, double, d
if ((frame_ + cid) % every != 0) continue;
auto num = [&](const char* k) { return std::atof(c->attrs[k].c_str()); };
Job j{id, cid, (c->attrs["look_for_failure"].empty() || c->attrs["look_for_failure"] == "1"), e.x + num("area_aabb.min_x"), e.y + num("area_aabb.min_y"), e.x + num("area_aabb.max_x"), e.y + num("area_aabb.max_y"), c->attrs["material"], c->attrs["material2"], (int)num("count_min")};
// 00bb5320 calls 008833e0 with the complete AABB before moving
// the failure cursor. A partial pixel scene is not a wall breach.
if (ready && !ready(j.x0, j.y0, j.x1, j.y1)) continue;
if (j.failure) {
// 00bb5320 scans one short row/column per update, advancing
// along the long axis. Scanning the whole temple boundary every
// frame also reports transient terrain changes far too eagerly.
const bool vertical = num("area_aabb.max_x") - num("area_aabb.min_x") <= num("area_aabb.max_y") - num("area_aabb.min_y");
const int lo = (int)num(vertical ? "area_aabb.min_y" : "area_aabb.min_x");
const int hi = (int)num(vertical ? "area_aabb.max_y" : "area_aabb.max_x");
const auto scan = c->attrs.find("_ng_area_cursor");
const int cursor = scan == c->attrs.end() ? lo : std::clamp(std::atoi(scan->second.c_str()), lo, std::max(lo, hi));
c->attrs["_ng_area_cursor"] = std::to_string(cursor < hi ? cursor + 1 : lo);
if (vertical) j.y0 = j.y1 = (int)e.y + cursor;
else j.x0 = j.x1 = (int)e.x + cursor;
}
jobs.push_back(std::move(j));
}
}
for (const Job& j : jobs) {
if (!alive(j.entity) || !component_enabled(j.comp)) continue;
int total = j.failure ? 1 : 0; // (in: 1 = the part of the box in the loaded window will do - the failure checks scan what is loaded)
const int r = query(j.x0, j.y0, j.x1, j.y1, j.m1, j.m2, &total);
if (r < 0) continue;
@@ -1105,7 +1138,7 @@ void EntityRuntime::material_area_step(const std::function<int(double, double, d
const char* attr = j.failure ? "script_material_area_checker_failed" : "script_material_area_checker_success";
const char* fn = j.failure ? "material_area_checker_failed" : "material_area_checker_success";
RtEntity* e = ent(j.entity);
if (!e) continue;
if (!e || !e->alive) continue;
std::vector<std::pair<int, std::string>> scripts;
for (int cid : e->comps) {
RtComponent* c = comp(cid);
@@ -1224,7 +1257,22 @@ void EntityRuntime::tick() {
const auto nb = c->attrs.find("_ng_born");
const int born = nb != c->attrs.end() ? std::atoi(nb->second.c_str()) : e.born;
if (life > 0 && frame_ - born >= (int)life) expire.push_back(id);
else if (life > 0 && on_sprite_fade && c->attrs["fade_sprites"] == "1") on_sprite_fade(id, (frame_ - born) / life);
else if (on_sprite_fade) {
const int remaining = (int)life - (frame_ - born);
const bool wasHidden = c->attrs.count("_ng_blink_hidden") != 0;
bool hidden = false;
if (life > 0 && remaining < 180 && e.parent == 0 &&
(!components(id, "PhysicsBodyComponent", "", false).empty() || !components(id, "PhysicsBody2Component", "", false).empty())) {
// 00b9a010: physics items blink for one frame at each interval,
// accelerating over their final three seconds (gold included).
const int period = remaining < 15 ? 2 : remaining < 60 ? 5 : remaining < 120 ? 8 : 20;
hidden = remaining % period == 0;
}
if (hidden) c->attrs["_ng_blink_hidden"] = "1";
else c->attrs.erase("_ng_blink_hidden");
const bool fade = life > 0 && c->attrs["fade_sprites"] == "1";
if (fade || hidden != wasHidden) on_sprite_fade(id, hidden ? 1.0 : fade ? (frame_ - born) / life : 0.0);
}
}
}
{ // SpriteComponent kill_entity_after_finished: the entity goes when the sprite's animation (rect_animation, else the default one) has played to its end once,
+5 -1
View File
@@ -142,7 +142,8 @@ public:
// (x0, y0, x1, y1 in world cells): -1 = the area is not loaded (nothing happens), 0 = no, 1 = yes. A checker that looks for failure calls
// script_material_area_checker_failed of the entity's LuaComponent when the answer is 0, the other one the _success script when it is 1.
// query: the cells of the box that are m1 or m2 (*total: all its cells), -1 when the box is not loaded
void material_area_step(const std::function<int(double x0, double y0, double x1, double y1, const std::string& m1, const std::string& m2, int* total)>& query);
void material_area_step(const std::function<int(double x0, double y0, double x1, double y1, const std::string& m1, const std::string& m2, int* total)>& query,
const std::function<bool(double x0, double y0, double x1, double y1)>& ready = {});
// TeleportComponent (with an enabled HitboxComponent): true when the player point is inside the hitbox of an enabled teleporter; gives the destination
// (target.x / target.y, absolute or relative to the teleporter) and the entity.
bool teleport_step(double player_x, double player_y, double& target_x, double& target_y, int* teleporter = nullptr);
@@ -193,6 +194,9 @@ public:
void remove_component(int comp) override;
void set_component_enabled(int comp, bool enabled) override;
bool component_enabled(int comp) override;
// Item emitter context tags apply to the item itself: a card inside the
// held wand is stored, not held. Untagged/non-item effects keep their state.
bool item_emitter_enabled(int comp, int held_item);
std::string component_type(int comp) override;
int component_entity(int comp) override;
bool comp_has_tag(int comp, const std::string& tag) override;
+1 -1
View File
@@ -20,7 +20,7 @@ public:
static constexpr size_t kTableBytes = (size_t)(kVerletBase + kMaxVerlet * kVerletStride) * 16;
static constexpr size_t kTexWords = 1u << 20;
static constexpr int kMaxNodes = 2048, kMaxSpecs = 4096, kMaxRequests = 256, kPersistent = 128; // the last 128 nodes and specs are permanent (creature attacks), the rest is a ring
static constexpr size_t kHostBytes = 16 + 1024 * 16 + 8192 * 16 + 16384 * 16;
static constexpr size_t kHostBytes = 16 + 1024 * 16 + 8192 * 16 + 16384 * 16 + 16 + 4096 * 4;
static constexpr int kMaxProj = 32768; // the projectile pool (shaders/fx_state.glsl FP_MAX); a new projectile takes a free record, a live one is overwritten only when the pool is full
static constexpr size_t kStateBytes = 16 + (size_t)kMaxProj * 96 + 16384 * 48 + 512 * 48; // (animations: 0..255 the GPU's ring, 256..511 the host's)
static constexpr size_t kLightBytes = 16 + 512 * 32;
+1
View File
@@ -208,6 +208,7 @@ const ProjDef* ProjectileSim::def_from(const std::string& file, const EntityDef&
d.dont_damage_shooter = attr(pc, "explosion_dont_damage_shooter", 0) != 0;
d.not_scaled_by_gamefx = attr(pc, "config_explosion.not_scaled_by_gamefx", 0) != 0;
d.hit_knockback_force = attr(pc, "knockback_force", 0.0f);
d.blood_count_multiplier = attr(pc, "blood_count_multiplier", 1.0f);
d.ragdoll_force_multiplier = attr(pc, "ragdoll_force_multiplier", 0.025f);
if (pc) { // (the packet's RAGDOLL_FX: 00c091f0 copies ragdoll_fx_on_collision)
static const char* fxNames[10] = {"NONE", "NORMAL", "BLOOD_EXPLOSION", "BLOOD_SPRAY", "FROZEN", "CONVERT_TO_MATERIAL", "CUSTOM_RAGDOLL_ENTITY", "DISINTEGRATED", "NO_RAGDOLL_FILE", "PLAYER_RAGDOLL_CAMERA"};
+1
View File
@@ -67,6 +67,7 @@ struct ProjDef {
float hit_knockback_force = 0.0f; // ProjectileComponent knockback_force (+5d0, default 0): a hit knocks back by knockback_force x speed x the projectile's VelocityComponent mass (00c091f0)
float ragdoll_explosion_mult = 1.0f; // config_explosion physics_multiplier_ragdoll_force (ConfigExplosion +0x108, default 1): scales the impulse an explosion gives the entities
int ragdoll_fx = 1; // ProjectileComponent ragdoll_fx_on_collision (+53c): RAGDOLL_FX NONE 0, NORMAL 1, BLOOD_EXPLOSION 2, BLOOD_SPRAY 3, FROZEN 4, CONVERT_TO_MATERIAL 5, CUSTOM_RAGDOLL_ENTITY 6, DISINTEGRATED 7, NO_RAGDOLL_FILE 8, PLAYER_RAGDOLL_CAMERA 9
float blood_count_multiplier = 1.0f; // ProjectileComponent +0x5dc
float ragdoll_force_multiplier = 0.025f; // ProjectileComponent (+5d4, default 0.025): scales the force vector of the hit
float velocity_mass = 0.05f; // VelocityComponent mass (+50, default 0.05)
float physics_min = 0, physics_max = 0.2f; // config_explosion.physics_explosion_power.min / .max: the force on rigid bodies (00657960)
+7 -1
View File
@@ -385,7 +385,13 @@ int n_StatsBiomeGetValue(LuaHost& h, lua_State* L) {
return 1;
}
int n_StatsResetBiome(LuaHost& h, lua_State*) { h.biome_stats_base = h.stats; return 0; }
int n_GlobalsSetValue(LuaHost& h, lua_State* L) { if (h.buffer_world_writes) { h.globals[str(L, 1)] = str(L, 2); h.wrote_world = true; return 0; } std::lock_guard<std::mutex> lk(LuaHost::shared_mutex); h.world_globals()[str(L, 1)] = str(L, 2); return 0; }
int n_GlobalsSetValue(LuaHost& h, lua_State* L) {
const auto key = str(L, 1), value = str(L, 2);
if (std::getenv("NG_EVENTLOG") && key.rfind("TEMPLE_", 0) == 0)
std::printf("temple global %s = %s%s\n", key.c_str(), value.c_str(), h.buffer_world_writes ? " (generation trial)" : "");
if (h.buffer_world_writes) { h.globals[key] = value; h.wrote_world = true; return 0; }
std::lock_guard<std::mutex> lk(LuaHost::shared_mutex); h.world_globals()[key] = value; return 0;
}
// session numbers (new game plus count etc.): a fresh run has everything at 0
int n_SessionNumbersGetValue(LuaHost&, lua_State* L) { lua_pushstring(L, "0"); return 1; }
int n_GameGetFrameNum(LuaHost&, lua_State* L) { lua_pushnumber(L, 0); return 1; }
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include <cmath>
#include <cstdint>
namespace ng::phys {
// 00720960: visit surviving body pixels, preserving their source color, and
// fill only empty grid cells. Rotated masks bridge one/two-cell horizontal gaps.
// get_material returns -1 for unloaded space; put receives the source ARGB.
template<class Get, class Put>
void convert_body_cells(const uint32_t* pixels, int width, int height, double anchor_x, double anchor_y,
double origin_x, double origin_y, double angle, int old_material, int target, Get get_material, Put put) {
const double cs = std::cos(angle), sn = std::sin(angle);
for (int py = 0; py < height; ++py) for (int px = 0; px < width; ++px) {
const uint32_t color = pixels[py * width + px];
if ((color >> 24) < 128) continue;
const double lx = px + .5 - anchor_x, ly = py + .5 - anchor_y;
const int x = (int)std::floor(origin_x + cs * lx - sn * ly), y = (int)std::floor(origin_y + sn * lx + cs * ly);
if (get_material(x, y) == 0) put(x, y, target, color);
if (angle == 0) continue;
const auto matches = [&](int m) { return m > 0 && (m == target || m == old_material); };
if (matches(get_material(x - 2, y))) {
if (get_material(x - 1, y) == 0) put(x - 1, y, target, color);
} else if (matches(get_material(x - 3, y))) {
if (get_material(x - 1, y) == 0) put(x - 1, y, target, color);
if (get_material(x - 2, y) == 0) put(x - 2, y, target, color);
}
}
}
// 00ba5f20 / 00ba61ab: the native lookup deliberately saturates at sector 31
// after a quarter turn; it is not a uniform division of a full turn into 32.
inline int loose_ground_sector(int x, int y) {
constexpr float pi = 3.1415927f, tau = 6.2831855f;
float angle = 0;
const float length = std::sqrt(float(x * x + y * y));
if (length != 0) angle = float(std::atan2(double(float(y) / length), double(float(x) / length)));
while (angle < 0) angle += tau;
angle = float(std::fmod(double(angle), double(tau)));
const int sector = int(((angle / pi) * 2.0f) * 32.0f);
return sector < 0 ? 0 : sector > 31 ? 31 : sector;
}
// 00ba5940: single-precision unit steps and truncation towards zero, including
// the negative coordinates on the left side of a Holy Mountain.
template<class Hit>
bool loose_ground_ray(float sx, float sy, float ex, float ey, int& hx, int& hy, Hit hit) {
const float dx = ex - sx, dy = ey - sy, squared = dx * dx + dy * dy;
const int limit = (int)(squared + .5f);
hx = (int)ex; hy = (int)ey;
if (limit < 1) return hit(hx, hy);
const float length = std::sqrt(squared), ux = dx / length, uy = dy / length;
float x = sx, y = sy;
for (int step = 0; step < 1024 && (x - sx) * (x - sx) + (y - sy) * (y - sy) < (float)limit; ++step) {
x += ux; y += uy; hx = (int)x; hy = (int)y;
if (hit(hx, hy)) return true;
}
return false;
}
} // namespace ng::phys
+266 -53
View File
@@ -12,6 +12,7 @@
#include "physics/rigid.h"
#include "recovered_units.h"
#include "shape_queries.h"
#include "contact_solver.h"
namespace ng::phys {
namespace {
@@ -19,7 +20,6 @@ namespace {
using noita::cpu::Vec2d;
constexpr double kDt = noita::cpu::original_body_step; // 009a48d0: the body world steps by 1/60 s
constexpr double kUnitsPerFrameFromCells = 6.0; // 1 cell per frame = 60 cells/s = 6 physics units/s
constexpr int kFreezeMargin = 16; // cells inside the window edge beyond which a body is frozen (it has no terrain around it)
uint64_t fnv(uint64_t h, uint8_t b) { return (h ^ b) * 1099511628211ull; }
@@ -27,13 +27,19 @@ uint64_t fnv(uint64_t h, uint8_t b) { return (h ^ b) * 1099511628211ull; }
PhysSystem::PhysSystem() {
world_.gravity = {0, 12}; // 00718740: gravity (0, 12) physics units per second squared
terrain_ = std::make_unique<TerrainPhysics>(world_);
terrain_static_ = std::make_unique<TerrainPhysics>(world_, kStaticOnlyCategory);
terrain_ = std::make_unique<TerrainPhysics>(world_, 4);
terrain_static_ = std::make_unique<TerrainPhysics>(world_, 1);
terrain_sticky_ = std::make_unique<TerrainPhysics>(world_, 1);
}
int PhysSystem::add_shape(const RigidShape& s) {
int PhysSystem::add_shape(const RigidShape& s, int reuse) {
ShapeData d;
d.polys = mask_polygons(s.mask.data(), s.w, s.h, 1.0f, 6);
d.bounds = {(double)s.w, (double)s.h, 0, 0};
for (const auto& poly : d.polys) for (const auto& p : poly) {
d.bounds[0] = std::min(d.bounds[0], (double)p.x); d.bounds[1] = std::min(d.bounds[1], (double)p.y);
d.bounds[2] = std::max(d.bounds[2], (double)p.x); d.bounds[3] = std::max(d.bounds[3], (double)p.y);
}
if (std::getenv("NG_SHAPEAREA")) { // (debug: the polygon area against the pixel count)
double a = 0; int px = 0;
for (const ConvexPoly& cp : d.polys) for (size_t k = 0; k < cp.size(); ++k) a += 0.5 * (cp[k].x * cp[(k + 1) % cp.size()].y - cp[(k + 1) % cp.size()].x * cp[k].y);
@@ -46,6 +52,7 @@ int PhysSystem::add_shape(const RigidShape& s) {
d.mass = s.mass;
d.radius = s.radius;
d.samples = s.samples;
if (reuse >= 0 && reuse < (int)shapes_.size()) { shapes_[reuse] = std::move(d); return reuse; }
shapes_.push_back(std::move(d));
return (int)shapes_.size() - 1;
}
@@ -54,6 +61,7 @@ void PhysSystem::add_body(int index, int shape, double ax, double ay, double ox,
if (index >= (int)specs_.size()) specs_.resize(index + 1);
Spec s;
s.shape = shape;
s.lifecycle.born = frame_;
s.ax = ax; s.ay = ay; s.ox = ox; s.oy = oy; if (std::getenv("NG_ALLSINK")) s.go_through_sand = true; // (test: every body goes through sand)
if (specs_[index].cpu >= 0) remove_body(index);
specs_[index] = s;
@@ -92,6 +100,12 @@ void PhysSystem::set_surface(int index, double friction, double restitution) {
for (int fi : world_.bodies[specs_[index].cpu].fixtures) { world_.fixtures[fi].friction = friction; world_.fixtures[fi].restitution = restitution; }
}
void PhysSystem::set_bullet(int index, bool on) {
if (index < 0 || index >= (int)specs_.size() || !specs_[index].live) return;
specs_[index].bullet = on;
if (specs_[index].cpu >= 0) world_.bodies[specs_[index].cpu].flags.bullet = on;
}
void PhysSystem::set_density(int index, double density) {
if (index < 0 || index >= (int)specs_.size() || !specs_[index].live) return;
specs_[index].density = density;
@@ -124,6 +138,7 @@ void PhysSystem::create_pending() {
BodyDefinition def;
def.type = 2;
def.position = {s.ox / kPixelsPerUnit, s.oy / kPixelsPerUnit};
def.angle = s.angle;
def.velocity = {s.vx * kUnitsPerFrameFromCells, s.vy * kUnitsPerFrameFromCells};
def.angular_velocity = s.omega * 60.0;
def.allow_sleep = s.allow_sleep; def.fixed_rotation = s.fixed_rotation; def.bullet = s.bullet;
@@ -135,7 +150,7 @@ void PhysSystem::create_pending() {
f.density = s.density;
f.friction = s.friction;
f.restitution = s.restitution;
f.filter = {2, (std::uint16_t)(s.go_through_sand ? 0xffff & ~4 : 0xffff & ~kStaticOnlyCategory), (std::int16_t)s.group}; // 006f8860: the default fixture category 2 (a body that goes through sand meets only the static terrain)
f.filter = {2, (std::uint16_t)(s.go_through_sand ? 0xffff & ~4 : 0xffff), (std::int16_t)s.group}; // 006f8860: the default fixture category 2 (a body that goes through sand meets only the static terrain)
ToiShape shape;
for (const FPoint& p : poly) shape.vertices.push_back({(p.x - s.ax) / kPixelsPerUnit, (p.y - s.ay) / kPixelsPerUnit});
shape.radius = 0.009999999776482582;
@@ -188,23 +203,35 @@ void PhysSystem::set_materials(std::vector<MaterialClass> table) {
for (size_t m = 0; m < materials_.size() && m < 65536; ++m) solid_lut_[m] = !materials_[m].solid ? 0 : (materials_[m].powder ? 2 : 1); // 1 static, 2 powder
}
void PhysSystem::update_terrain(const uint32_t* cells, int vx, int vy, int gw, int gh) {
// The solid mask of the window. A chunk gets its fixtures once it lies completely inside the window (with the one-cell margin the contours need) and again
// whenever a cell in or next to it changes; chunks that scroll out keep theirs (the terrain there is not simulated and does not change).
void PhysSystem::update_terrain(const uint32_t* cells, int vx, int vy, int gw, int gh, const uint32_t* terrain_age) {
// Terrain fixtures cover the loaded window, including clipped edge chunks.
// Rebuild changed cells and changed clipping; chunks outside retain their fixtures.
cur_.resize((size_t)gw * gh);
for (size_t i = 0; i < cur_.size(); ++i) cur_[i] = solid_lut_[cells[i] & 0xFFFFu];
for (size_t i = 0; i < cur_.size(); ++i) {
const uint32_t cell = cells[i], m = cell & 0xffffu;
uint8_t cls = solid_lut_[m];
if (cls == 2) {
const auto& mat = materials_[m];
const bool settled = terrain_age && (terrain_age[i] >> 16) == m && (terrain_age[i] & 255u) >= 20;
if (cell & (1u << 26)) cls = 3; // native static byte is tested before never_box2d
else if (mat.never_box2d) cls = 0;
else if (mat.sticks_to_ceiling && settled) cls = 3;
else cls = settled ? 2 : 0;
}
cur_[i] = cls;
}
auto floor_div = [](int a, int b) { return a >= 0 ? a / b : -((-a + b - 1) / b); };
std::unordered_set<int64_t> dirty, dirtyStatic, dirtySolid;
auto mark = [&](int x, int y, bool staticToo, bool solidToo) {
for (int dy = -1; dy <= 1; dy += 2) for (int dx = -1; dx <= 1; dx += 2) {
std::unordered_set<int64_t> dirty, dirtyStatic, dirtySticky;
auto mark = [&](int x, int y, unsigned changed) {
for (int dy = -8; dy <= 8; dy += 16) for (int dx = -8; dx <= 8; dx += 16) {
const int cx = floor_div(x + dx, kChunk), cy = floor_div(y + dy, kChunk);
dirty.insert(((int64_t)cx << 32) ^ (uint32_t)cy);
if (staticToo) dirtyStatic.insert(((int64_t)cx << 32) ^ (uint32_t)cy);
if (solidToo) dirtySolid.insert(((int64_t)cx << 32) ^ (uint32_t)cy);
if (changed & 1u) dirtyStatic.insert(((int64_t)cx << 32) ^ (uint32_t)cy);
if (changed & 2u) dirtySticky.insert(((int64_t)cx << 32) ^ (uint32_t)cy);
}
dirty.insert(((int64_t)floor_div(x, kChunk) << 32) ^ (uint32_t)floor_div(y, kChunk));
if (staticToo) dirtyStatic.insert(((int64_t)floor_div(x, kChunk) << 32) ^ (uint32_t)floor_div(y, kChunk));
if (solidToo) dirtySolid.insert(((int64_t)floor_div(x, kChunk) << 32) ^ (uint32_t)floor_div(y, kChunk));
if (changed & 1u) dirtyStatic.insert(((int64_t)floor_div(x, kChunk) << 32) ^ (uint32_t)floor_div(y, kChunk));
if (changed & 2u) dirtySticky.insert(((int64_t)floor_div(x, kChunk) << 32) ^ (uint32_t)floor_div(y, kChunk));
};
if (prev_.size() == cur_.size() && prev_w_ == gw && prev_h_ == gh) {
const int ox0 = std::max(vx, prev_vx_), ox1 = std::min(vx, prev_vx_) + gw, oy0 = std::max(vy, prev_vy_), oy1 = std::min(vy, prev_vy_) + gh;
@@ -213,36 +240,43 @@ void PhysSystem::update_terrain(const uint32_t* cells, int vx, int vy, int gw, i
const uint8_t* b = &prev_[(size_t)(y - prev_vy_) * gw + (ox0 - prev_vx_)];
const int n = ox1 - ox0;
if (n <= 0 || std::memcmp(a, b, (size_t)n) == 0) continue;
for (int x = 0; x < n; ++x) if (a[x] != b[x]) mark(ox0 + x, y, (a[x] == 1) != (b[x] == 1), a[x] != 2 && b[x] != 2); // (2: powder)
for (int x = 0; x < n; ++x) if (a[x] != b[x]) mark(ox0 + x, y, unsigned((a[x] == 1) != (b[x] == 1)) | (unsigned((a[x] == 3) != (b[x] == 3)) << 1)); // (2: powder)
}
}
prev_ = cur_; prev_vx_ = vx; prev_vy_ = vy; prev_w_ = gw; prev_h_ = gh;
SolidFn solid = [&](int x, int y) {
const int gx = x - vx, gy = y - vy;
return gx >= 0 && gy >= 0 && gx < gw && gy < gh && cur_[(size_t)gy * gw + gx] != 0;
return gx >= 0 && gy >= 0 && gx < gw && gy < gh && cur_[(size_t)gy * gw + gx] == 2;
};
SolidFn solidStatic = [&](int x, int y) {
const int gx = x - vx, gy = y - vy;
return gx >= 0 && gy >= 0 && gx < gw && gy < gh && cur_[(size_t)gy * gw + gx] == 1;
};
// Keep fixed terrain boundaries when settling/attached powder grows around
// a body. Sharing one contour removes the masonry surface below a body
// already inside mud, leaving it inside a collision shell down to the wall's
// underside. Both independent surfaces retain native category-1 filtering.
SolidFn solidSticky = [&](int x, int y) {
const int gx = x - vx, gy = y - vy;
return gx >= 0 && gy >= 0 && gx < gw && gy < gh && cur_[(size_t)gy * gw + gx] == 3;
};
CoefficientFn coeff = [&](int x, int y) {
const int gx = x - vx, gy = y - vy;
if (gx < 0 || gy < 0 || gx >= gw || gy >= gh) return TerrainCoefficients{};
const uint32_t m = cells[(size_t)gy * gw + gx] & 0xFFFFu;
return m < materials_.size() ? TerrainCoefficients{materials_[m].friction, materials_[m].restitution} : TerrainCoefficients{};
};
const int cx0 = floor_div(vx + 1 + kChunk - 1, kChunk), cx1 = floor_div(vx + gw - 1 - kChunk, kChunk); // chunks with a cell of margin on every side inside the window
const int cy0 = floor_div(vy + 1 + kChunk - 1, kChunk), cy1 = floor_div(vy + gh - 1 - kChunk, kChunk);
// A built chunk whose cells changed is rebuilt only while a body is near it (its box, the radius + 16 cells + two frames of its motion, touches the chunk):
// the fixtures matter to the bodies only, and moving powder (the snow of the Hiisi base) dirtied dozens of chunks every frame (4 ms, and the rebuild woke
// every body on them). The others stay stale until a body comes. A chunk where only powder moved (falling snow) waits 10 frames even so.
// A solid change waits 4 frames (the first change counts; powder only: 10): the snow cave's ice melts and freezes again cell by cell all the time, and a rebuild every
// frame near the bodies floating in its pools (which it also woke) was 2-3 ms of each physics step
for (int64_t k : dirty) if (built_.count(k)) { auto [it, fresh] = stale_.emplace(k, frame_); if (dirtySolid.count(k) && (fresh || it->second > frame_ - 6) && !std::getenv("NG_NOSOLIDDELAY")) it->second = std::min<std::remove_reference_t<decltype(it->second)>>(it->second, frame_ - 6); }
const int cx0 = floor_div(vx, kChunk), cx1 = floor_div(vx + gw - 1, kChunk);
const int cy0 = floor_div(vy, kChunk), cy1 = floor_div(vy + gh - 1, kChunk);
// Dirty terrain is rebuilt before stepping nearby bodies. Settling classification
// keeps moving powder out of this queue; no extra debounce delays removed supports.
// The eight-cell dirty halo also covers powder contours inset across chunk seams.
for (int64_t k : dirty) if (built_.count(k)) stale_.insert(k);
for (int64_t k : dirtyStatic) if (built_.count(k)) stale_static_.insert(k);
for (int64_t k : dirtySticky) if (built_.count(k)) stale_sticky_.insert(k);
std::vector<std::array<double, 4>> near; // the boxes of the active bodies, in cells
for (const Spec& s : specs_) {
if (s.cpu < 0 || s.frozen) continue;
if (s.cpu < 0) continue;
const auto& b = world_.bodies[s.cpu];
const double px = b.sweep.center_end.x * kPixelsPerUnit, py = b.sweep.center_end.y * kPixelsPerUnit;
const double r = (s.shape >= 0 ? shapes_[s.shape].radius : 8.0) + 16.0 + 2.0 * std::hypot(b.velocity.x, b.velocity.y) * kPixelsPerUnit / 60.0;
@@ -256,9 +290,13 @@ void PhysSystem::update_terrain(const uint32_t* cells, int vx, int vy, int gw, i
std::vector<std::pair<int, int>> rebuilt;
for (int cy = cy0; cy <= cy1; ++cy)
for (int cx = cx0; cx <= cx1; ++cx) {
if (cx * kChunk - 1 < vx || cy * kChunk - 1 < vy || cx * kChunk + kChunk + 1 > vx + gw || cy * kChunk + kChunk + 1 > vy + gh) continue;
const int64_t key = ((int64_t)cx << 32) ^ (uint32_t)cy;
const bool known = built_.count(key) != 0;
// Edge chunks are clipped to the loaded cells. Rebuild when scrolling
// changes that coverage, including when a clipped chunk becomes full.
const std::array<int, 4> coverage = {std::max(vx, cx * kChunk - 1), std::max(vy, cy * kChunk - 1),
std::min(vx + gw, (cx + 1) * kChunk + 1), std::min(vy + gh, (cy + 1) * kChunk + 1)};
if (known && terrain_coverage_[key] != coverage) { stale_.insert(key); stale_static_.insert(key); stale_sticky_.insert(key); }
if (!known) { // a chunk new in the window is built once it has been there 15 frames: after a teleport the pixel scenes reach the cells a few frames after the
// terrain, and a chunk built at once had the bare rock's outline (the holy mountain's statue fell 6 px onto it, then sat inside the brick's thin shell, shaking);
// the bodies over it stay frozen until it is built
@@ -267,18 +305,20 @@ void PhysSystem::update_terrain(const uint32_t* cells, int vx, int vy, int gw, i
}
if (known) {
const auto st = stale_.find(key);
if (st == stale_.end() || (st->second >= 0 && frame_ - st->second < 10) || !body_near(cx, cy)) continue;
if (st == stale_.end() || !body_near(cx, cy)) continue;
}
terrain_->rebuild_chunk(solid, coeff, cx, cy);
if (!known || stale_static_.count(key)) terrain_static_->rebuild_chunk(solidStatic, coeff, cx, cy);
built_.insert(key); stale_.erase(key); stale_static_.erase(key); first_seen_.erase(key);
if (!known || stale_sticky_.count(key)) terrain_sticky_->rebuild_chunk(solidSticky, coeff, cx, cy);
built_.insert(key); stale_.erase(key); stale_static_.erase(key); stale_sticky_.erase(key); first_seen_.erase(key);
terrain_coverage_[key] = coverage;
rebuilt.push_back({cx, cy});
}
if (std::getenv("NG_PHYSPROF")) std::printf(" chunks %d..%d x %d..%d, rebuilt %zu, built %zu, fixtures total %zu, contacts %zu%c", cx0, cx1, cy0, cy1, rebuilt.size(), built_.size(), world_.fixtures.size(), world_.contacts.size(), 10);
// chunks far from the window go away
for (auto it = built_.begin(); it != built_.end();) {
const int cx = (int)(*it >> 32), cy = (int)(int32_t)(*it & 0xFFFFFFFF);
if (cx < floor_div(vx, kChunk) - 3 || cx > floor_div(vx + gw, kChunk) + 3 || cy < floor_div(vy, kChunk) - 3 || cy > floor_div(vy + gh, kChunk) + 3) { terrain_->remove_chunk(cx, cy); terrain_static_->remove_chunk(cx, cy); stale_.erase(*it); stale_static_.erase(*it); it = built_.erase(it); }
if (cx < floor_div(vx, kChunk) - 3 || cx > floor_div(vx + gw, kChunk) + 3 || cy < floor_div(vy, kChunk) - 3 || cy > floor_div(vy + gh, kChunk) + 3) { terrain_->remove_chunk(cx, cy); terrain_static_->remove_chunk(cx, cy); terrain_sticky_->remove_chunk(cx, cy); stale_.erase(*it); stale_static_.erase(*it); stale_sticky_.erase(*it); terrain_coverage_.erase(*it); it = built_.erase(it); }
else ++it;
}
for (auto it = first_seen_.begin(); it != first_seen_.end();) { // (a chunk that left the window before it was built starts its wait again when it comes back)
@@ -379,7 +419,7 @@ void PhysSystem::set_go_through_sand(int index, bool on) {
Spec& s = specs_[index];
s.go_through_sand = on;
if (s.cpu >= 0)
for (int fi : world_.bodies[s.cpu].fixtures) noita::cpu::set_fixture_filter(world_, fi, {world_.fixtures[fi].filter.category, (std::uint16_t)(on ? 0xffff & ~4 : 0xffff & ~kStaticOnlyCategory), world_.fixtures[fi].filter.group});
for (int fi : world_.bodies[s.cpu].fixtures) noita::cpu::set_fixture_filter(world_, fi, {world_.fixtures[fi].filter.category, (std::uint16_t)(on ? 0xffff & ~4 : 0xffff), world_.fixtures[fi].filter.group});
}
void PhysSystem::shove(double x, double y, double radius, double power) {
@@ -457,7 +497,7 @@ void PhysSystem::explosion_force(double x, double y, double radius, double fmin,
pending_explosions_.push_back({x, y, radius, fmin, fmax});
}
void PhysSystem::step(const uint32_t* cells, int vx, int vy, int gw, int gh) {
void PhysSystem::step(const uint32_t* cells, int vx, int vy, int gw, int gh, const uint32_t* terrain_age) {
using namespace noita::cpu;
++frame_;
const bool prof = std::getenv("NG_PHYSPROF") != nullptr;
@@ -465,7 +505,7 @@ void PhysSystem::step(const uint32_t* cells, int vx, int vy, int gw, int gh) {
auto lap = [&](const char* what) { if (prof) { const auto t1 = std::chrono::steady_clock::now(); std::printf(" %s %.2f ms%c", what, std::chrono::duration<double, std::milli>(t1 - t0).count(), 10); t0 = t1; } };
create_pending();
lap("create");
update_terrain(cells, vx, vy, gw, gh);
update_terrain(cells, vx, vy, gw, gh, terrain_age);
lap("terrain");
// explosions: the force of 00657960 on every body in range that the blast can reach (line of sight through the cells as the stand-in of the 360-bin gate)
PhysicsUnits units;
@@ -519,17 +559,24 @@ void PhysSystem::step(const uint32_t* cells, int vx, int vy, int gw, int gh) {
// bodies outside the simulated window have no terrain around them: they are frozen (and wake up when the window comes back)
for (Spec& s : specs_) {
if (s.cpu < 0) continue;
const Vec2d p = world_.bodies[s.cpu].sweep.center_end; // (the centre of mass: the radius is measured from it; the transform position is the anchor, a corner of a chunk's image)
const double px = p.x * kPixelsPerUnit, py = p.y * kPixelsPerUnit;
const double rr = (s.shape >= 0 ? shapes_[s.shape].radius : 8.0) + 4.0;
const auto& body = world_.bodies[s.cpu];
const auto& bounds = shapes_[s.shape].bounds;
std::array<double, 4> box = {1e30, 1e30, -1e30, -1e30};
for (double x : {bounds[0], bounds[2]}) for (double y : {bounds[1], bounds[3]}) {
const double wx = body.transform.position.x * kPixelsPerUnit + body.transform.cosine * (x - s.ax) - body.transform.sine * (y - s.ay);
const double wy = body.transform.position.y * kPixelsPerUnit + body.transform.sine * (x - s.ax) + body.transform.cosine * (y - s.ay);
box[0] = std::min(box[0], wx); box[1] = std::min(box[1], wy);
box[2] = std::max(box[2], wx); box[3] = std::max(box[3], wy);
}
bool covered = true; // every chunk under the body has its terrain fixtures
for (double ex : {-rr, rr}) for (double ey : {-rr, rr}) {
const int ccx = (int)std::floor((px + ex) / kChunk), ccy = (int)std::floor((py + ey) / kChunk);
for (double x : {box[0], box[2]}) for (double y : {box[1], box[3]}) {
const int ccx = (int)std::floor(x / kChunk), ccy = (int)std::floor(y / kChunk);
covered = covered && chunk_built(ccx, ccy);
}
const bool inside = covered && px >= vx + kFreezeMargin && py >= vy + kFreezeMargin && px <= vx + gw - kFreezeMargin && py <= vy + gh - kFreezeMargin;
const double mx = 1 + std::fabs(body.velocity.x) * kPixelsPerUnit / 60.0, my = 1 + std::fabs(body.velocity.y) * kPixelsPerUnit / 60.0;
const bool inside = covered && box[0] >= vx + mx && box[1] >= vy + my && box[2] <= vx + gw - mx && box[3] <= vy + gh - my;
// a frozen body is deactivated (no proxies, no contacts: neighbours cannot wake it) and reactivated when its terrain exists again
if (!inside && !s.frozen) { s.frozen = true; set_body_awake(world_, s.cpu, false); set_body_active(world_, s.cpu, false, [](WorldToi&, int) {}); }
if (!inside && !s.frozen) { s.awake_before_freeze = body.flags.awake; s.frozen = true; set_body_awake(world_, s.cpu, false); set_body_active(world_, s.cpu, false, [](WorldToi&, int) {}); }
else if (inside && s.frozen) { s.frozen = false; set_body_active(world_, s.cpu, true, [](WorldToi&, int) {}); set_body_awake(world_, s.cpu, true); }
}
if (std::getenv("NG_RELEASENAILS") && frame_ == 100) for (Spec& s : specs_) if (s.nail_on && s.cpu >= 0 && !s.frozen) { s.nail_on = false; set_world_body_type(world_, s.cpu, 2); } // debug
@@ -574,15 +621,16 @@ void PhysSystem::step(const uint32_t* cells, int vx, int vy, int gw, int gh) {
if (false && specs_.size() > 4 && specs_[4].cpu >= 0 && frame_ % 10 == 0) { const Spec& s4 = specs_[4]; const auto& b4 = world_.bodies[s4.cpu]; std::printf("spec4 frozen %d awake %d type %d y %.1f contacts %zu%c", s4.frozen, b4.flags.awake, b4.flags.type, b4.transform.position.y * 10, b4.contacts.size(), 10); }
apply_liquids(cells, vx, vy, gw, gh);
ContactListeners listeners;
listeners.begin = [&](WorldToi& w, int contact) {
listeners.pre_solve = [&](WorldToi& w, int contact, const ContactManifold& old) {
const auto& c = w.contacts[contact];
if (c.manifold.count <= 0 || contact_events_.size() >= 256) return;
if (!material_contact_added(c.manifold, old) || contact_events_.size() >= 8192) return; // native queue: 4096 contacts, two material sides
const int ba = w.fixtures[c.fixture_a].body, bb = w.fixtures[c.fixture_b].body;
// the contact point: the manifold keeps it local to the body that does not own the reference face (type 1: body B, type 2: body A)
const auto& pt = c.manifold.points[0].point;
const int host = c.manifold.type == 2 ? ba : bb;
const auto& t = w.bodies[host].transform;
const double wx = t.position.x + t.cosine * pt.x - t.sine * pt.y, wy = t.position.y + t.sine * pt.x + t.cosine * pt.y;
const auto& a = w.bodies[ba]; const auto& b = w.bodies[bb];
const SolverBody bodyA{{a.sweep.center_end, a.velocity, a.sweep.angle_end, a.angular_velocity}, a.sweep.local_center, a.inverse_mass, a.inverse_inertia};
const SolverBody bodyB{{b.sweep.center_end, b.velocity, b.sweep.angle_end, b.angular_velocity}, b.sweep.local_center, b.inverse_mass, b.inverse_inertia};
const SolverContact geometry{0, 1, c.manifold, w.fixtures[c.fixture_a].children[c.child_a].radius, w.fixtures[c.fixture_b].children[c.child_b].radius};
const auto point = world_contact_manifold(geometry, bodyA, bodyB).points[0]; // 009b5b30: actual surface contact, including skin radii
const double wx = point.x, wy = point.y;
const auto sa = cpu_to_spec_.find(ba), sb = cpu_to_spec_.find(bb);
const double va_x = w.bodies[ba].velocity.x, va_y = w.bodies[ba].velocity.y, vb_x = w.bodies[bb].velocity.x, vb_y = w.bodies[bb].velocity.y;
for (int side = 0; side < 2; ++side) {
@@ -620,6 +668,53 @@ void PhysSystem::step(const uint32_t* cells, int vx, int vy, int gw, int gh) {
};
lap("pre");
step_rigid_world(world_, kDt, listeners);
// 006f7f00 reads the body's velocity when the queued contact is dispatched
// after the physics job (0071a6b0), not its pre-solve contact-point speed.
for (auto& event : contact_events_) {
const auto& s = specs_[event.body];
if (s.cpu < 0) continue;
const auto& b = world_.bodies[s.cpu];
event.vx = b.velocity.x * kPixelsPerUnit / 60.0;
event.vy = b.velocity.y * kPixelsPerUnit / 60.0;
}
// 007440a0: only dirty/awake bodies reach this check; retaining their
// fixtures and pixels is essential (this is not a body-to-cell bake).
for (int i = 0; i < (int)specs_.size(); ++i) {
auto& s = specs_[i];
if (s.cpu < 0 || s.frozen) continue;
const auto& b = world_.bodies[s.cpu];
if (s.lifecycle.settle_static && (s.raster_dirty || b.flags.awake) && b.flags.type != 0 &&
frame_ - s.lifecycle.born > 600 && std::hypot(b.velocity.x, b.velocity.y) < 1.0)
set_static(i, true);
s.raster_dirty = false;
}
if (std::getenv("NG_CONTACTPROBE") && frame_ % 60 == 0) {
double depth = 0, terrainDepth = 0; int pairA = -1, pairB = -1, awake = 0, terrainBody = -1, terrainMaterial = -1;
for (const auto& c : world_.contacts) if (c.live && c.enabled && c.touching && c.manifold.count > 0) {
const auto& fa = world_.fixtures[c.fixture_a]; const auto& fb = world_.fixtures[c.fixture_b];
const bool bodyA = cpu_to_spec_.count(fa.body) != 0, bodyB = cpu_to_spec_.count(fb.body) != 0;
if (!bodyA && !bodyB) continue;
const auto& a = world_.bodies[fa.body]; const auto& b = world_.bodies[fb.body];
if (!a.active || !b.active) continue;
const SolverBody sa{{a.sweep.center_end, a.velocity, a.sweep.angle_end, a.angular_velocity}, a.sweep.local_center, a.inverse_mass, a.inverse_inertia};
const SolverBody sb{{b.sweep.center_end, b.velocity, b.sweep.angle_end, b.angular_velocity}, b.sweep.local_center, b.inverse_mass, b.inverse_inertia};
const SolverContact contact{0, 1, c.manifold, fa.children[c.child_a].radius, fb.children[c.child_b].radius};
const auto manifold = world_contact_manifold(contact, sa, sb);
for (int p = 0; p < c.manifold.count; ++p) {
const double d = -manifold.separations[p] * kPixelsPerUnit;
if (bodyA && bodyB && d > depth) {
depth = d; pairA = cpu_to_spec_.at(fa.body); pairB = cpu_to_spec_.at(fb.body);
awake = int(a.flags.awake) | (int(b.flags.awake) << 1);
} else if (bodyA != bodyB && d > terrainDepth) {
terrainDepth = d; terrainBody = cpu_to_spec_.at(bodyA ? fa.body : fb.body);
const int x = (int)std::floor(manifold.points[p].x * kPixelsPerUnit) - vx;
const int y = (int)std::floor(manifold.points[p].y * kPixelsPerUnit) - vy;
terrainMaterial = x >= 0 && y >= 0 && x < gw && y < gh ? int(cells[y * gw + x] & 0xFFFFu) : -1;
}
}
}
std::printf("contact probe %u: maximum body penetration %.3f px, bodies %d / %d, awake %d; terrain %.3f px, body %d, material %d\n", frame_, depth, pairA, pairB, awake, terrainDepth, terrainBody, terrainMaterial);
}
lap("box2d step");
if (prof) { int aw = 0, act = 0, dyn = 0; for (const auto& b : world_.bodies) { if (!b.live || b.flags.type != 2) continue; ++dyn; act += b.active; aw += b.active && b.flags.awake; } std::printf(" bodies: %d dynamic, %d active, %d awake%c", dyn, act, aw, 10); }
break_world_joints(world_, kPixelsPerUnit);
@@ -665,15 +760,30 @@ bool PhysSystem::body_state(int index, double* o) const {
}
void PhysSystem::set_body_state(int index, const double* v, int n) { // (b2Body::SetTransform: the origin follows the centre, the sweep starts there)
if (index < 0 || index >= (int)specs_.size() || !specs_[index].live || specs_[index].cpu < 0) return;
if (index < 0 || index >= (int)specs_.size() || !specs_[index].live) return;
Spec& s = specs_[index];
if (s.cpu < 0) {
// Fractures restore the pose before create_pending builds the replacement body.
if (n >= 3) s.angle = v[2];
if (n >= 2 && s.shape >= 0 && s.shape < (int)shapes_.size()) {
const auto& shape = shapes_[s.shape];
const double x = shape.com_x - s.ax, y = shape.com_y - s.ay;
const double c = std::cos(s.angle), sn = std::sin(s.angle);
s.ox = v[0] * kPixelsPerUnit - (c * x - sn * y);
s.oy = v[1] * kPixelsPerUnit - (sn * x + c * y);
}
if (n >= 5) { s.vx = v[3] / kUnitsPerFrameFromCells; s.vy = v[4] / kUnitsPerFrameFromCells; }
if (n >= 6) s.omega = v[5] / 60.0;
return;
}
auto& b = world_.bodies[specs_[index].cpu];
if (n >= 2) {
const double ang = n >= 3 ? v[2] : b.sweep.angle_end, c = std::cos(ang), sn = std::sin(ang);
const Vec2d lc = b.sweep.local_center;
b.transform.cosine = c; b.transform.sine = sn;
b.transform.position = Vec2d{v[0] - (c * lc.x - sn * lc.y), v[1] - (sn * lc.x + c * lc.y)};
b.sweep.center_start = b.sweep.center_end = Vec2d{v[0], v[1]};
b.sweep.angle_start = b.sweep.angle_end = ang;
const Vec2d origin{v[0] - (c * lc.x - sn * lc.y), v[1] - (sn * lc.x + c * lc.y)};
// SetTransform also moves the fixture proxies. Updating only the sweep
// leaves collision queries at the previous position after a script move.
noita::cpu::set_body_transform(world_, specs_[index].cpu, origin, ang);
}
if (n >= 5) b.velocity = Vec2d{v[3], v[4]};
if (n >= 6) b.angular_velocity = v[5];
@@ -719,6 +829,39 @@ void PhysSystem::set_static(int index, bool st) {
if (specs_[index].cpu >= 0) noita::cpu::set_world_body_type(world_, specs_[index].cpu, st ? 0 : 2);
}
void PhysSystem::set_material_lifecycle(int index, int material, bool convert, int target) {
if (index < 0 || index >= (int)specs_.size() || !specs_[index].live) return;
auto& state = specs_[index].lifecycle;
state.material = material; state.sleep_convert = convert; state.target = target;
}
void PhysSystem::set_lifecycle(int index, Lifecycle state) {
if (index >= 0 && index < (int)specs_.size() && specs_[index].live) specs_[index].lifecycle = state;
}
PhysSystem::Lifecycle PhysSystem::lifecycle(int index) const {
return index >= 0 && index < (int)specs_.size() ? specs_[index].lifecycle : Lifecycle{};
}
void PhysSystem::set_settle_static(int index, bool on) {
if (index >= 0 && index < (int)specs_.size() && specs_[index].live) specs_[index].lifecycle.settle_static = on;
}
std::vector<int> PhysSystem::terrain_conversion_candidates(int vx, int vy, int width, int height) {
const double x = vx + width * .5, y = vy + height * .5;
const double dx = x - conversion_x_, dy = y - conversion_y_;
if (dx * dx + dy * dy <= 512.0 * 512.0) return {};
conversion_x_ = x; conversion_y_ = y;
std::vector<int> out;
for (int i = 0; i < (int)specs_.size(); ++i) {
const auto& s = specs_[i];
if (s.cpu < 0 || !s.lifecycle.sleep_convert) continue;
const auto& b = world_.bodies[s.cpu];
if (b.flags.type == 0) continue;
const double bx = b.transform.position.x * kPixelsPerUnit - x, by = b.transform.position.y * kPixelsPerUnit - y;
const double distance2 = bx * bx + by * by;
const bool awake = s.frozen ? s.awake_before_freeze : b.flags.awake;
if (distance2 >= 350.0 * 350.0 && (!awake || distance2 >= 750.0 * 750.0)) out.push_back(i);
}
return out;
}
void PhysSystem::set_ai_force(int index, double fx, double fy, double torque) {
if (index < 0 || index >= (int)specs_.size() || !specs_[index].live) return;
Spec& s = specs_[index];
@@ -731,6 +874,76 @@ double PhysSystem::mass_of(int index) const {
return b.inverse_mass > 0 ? 1.0 / b.inverse_mass : 0.0;
}
std::vector<PhysSystem::DebugBody> PhysSystem::debug_bodies() const {
std::vector<DebugBody> out;
for (int i = 0; i < (int)specs_.size(); ++i) {
const auto& s = specs_[i];
if (!s.live || s.cpu < 0) continue;
const auto& b = world_.bodies[s.cpu];
out.push_back({i, s.cpu, s.shape, b.flags.type, s.group, b.flags.awake, b.active, s.frozen, s.bullet, s.nail_on,
b.sweep.center_end.x * kPixelsPerUnit, b.sweep.center_end.y * kPixelsPerUnit, b.sweep.angle_end,
b.velocity.x / kUnitsPerFrameFromCells, b.velocity.y / kUnitsPerFrameFromCells, b.angular_velocity / 60.0,
b.inverse_mass > 0 ? 1.0 / b.inverse_mass : 0});
}
return out;
}
std::vector<PhysSystem::DebugPolygon> PhysSystem::debug_polygons(const std::array<double, 4>* viewport) const {
std::vector<DebugPolygon> out;
for (int fi = 0; fi < (int)world_.fixtures.size(); ++fi) {
const auto& f = world_.fixtures[fi];
if (!f.live || f.body < 0 || !world_.bodies[f.body].live) continue;
const auto& t = world_.bodies[f.body].transform;
const auto it = cpu_to_spec_.find(f.body);
for (size_t ci = 0; ci < f.children.size(); ++ci) {
const auto& ch = f.children[ci];
// Synchronization already maintains conservative child bounds. Inactive
// bodies may lack current proxies, so leave those in the fallback path.
if (viewport && world_.bodies[f.body].active && ci < f.bounds.size()) {
const auto& box = f.bounds[ci];
if (box.upper.x * kPixelsPerUnit < (*viewport)[0] || box.lower.x * kPixelsPerUnit > (*viewport)[2] ||
box.upper.y * kPixelsPerUnit < (*viewport)[1] || box.lower.y * kPixelsPerUnit > (*viewport)[3]) continue;
}
if (viewport && (!world_.bodies[f.body].active || ci >= f.bounds.size())) {
// Frozen bodies have no active proxy. Compute a conservative box
// without allocating their vertex vectors or trusting stale bounds.
double x0 = 1e300, y0 = 1e300, x1 = -1e300, y1 = -1e300;
for (const auto& v : ch.vertices) {
const double x = (t.position.x + t.cosine * v.x - t.sine * v.y) * kPixelsPerUnit;
const double y = (t.position.y + t.sine * v.x + t.cosine * v.y) * kPixelsPerUnit;
x0 = std::min(x0, x); y0 = std::min(y0, y); x1 = std::max(x1, x); y1 = std::max(y1, y);
}
const double radius = ch.radius * kPixelsPerUnit;
if (x1 + radius < (*viewport)[0] || x0 - radius > (*viewport)[2] ||
y1 + radius < (*viewport)[1] || y0 - radius > (*viewport)[3]) continue;
}
DebugPolygon p{it == cpu_to_spec_.end() ? -1 : it->second, fi, f.filter.category, f.filter.mask, f.filter.group, f.sensor, ch.radius * kPixelsPerUnit, {}};
p.points.reserve(ch.vertices.size());
for (const auto& v : ch.vertices) p.points.push_back({(t.position.x + t.cosine * v.x - t.sine * v.y) * kPixelsPerUnit,
(t.position.y + t.sine * v.x + t.cosine * v.y) * kPixelsPerUnit});
out.push_back(std::move(p));
}
}
return out;
}
std::vector<PhysSystem::DebugContact> PhysSystem::debug_contacts() const {
using namespace noita::cpu;
std::vector<DebugContact> out;
for (const auto& c : world_.contacts) {
if (!c.live || !c.enabled || !c.touching || !c.manifold.count) continue;
const auto& fa = world_.fixtures[c.fixture_a]; const auto& fb = world_.fixtures[c.fixture_b];
const auto& a = world_.bodies[fa.body]; const auto& b = world_.bodies[fb.body];
if (!a.active || !b.active) continue;
const SolverBody sa{{a.sweep.center_end, a.velocity, a.sweep.angle_end, a.angular_velocity}, a.sweep.local_center, a.inverse_mass, a.inverse_inertia};
const SolverBody sb{{b.sweep.center_end, b.velocity, b.sweep.angle_end, b.angular_velocity}, b.sweep.local_center, b.inverse_mass, b.inverse_inertia};
const SolverContact sc{0, 1, c.manifold, fa.children[c.child_a].radius, fb.children[c.child_b].radius};
const auto m = world_contact_manifold(sc, sa, sb);
const auto ia = cpu_to_spec_.find(fa.body), ib = cpu_to_spec_.find(fb.body);
for (int k = 0; k < c.manifold.count; ++k) out.push_back({ia == cpu_to_spec_.end() ? -1 : ia->second, ib == cpu_to_spec_.end() ? -1 : ib->second,
m.points[k].x * kPixelsPerUnit, m.points[k].y * kPixelsPerUnit, m.separations[k] * kPixelsPerUnit});
}
return out;
}
Pose PhysSystem::pose(int index) const {
Pose p;
if (index < 0 || index >= (int)specs_.size()) return p;
+49 -11
View File
@@ -3,6 +3,7 @@
// of the props and the joints of the corpses. `GpuBodies` (src/physics/gpu_bodies.*) is the interface the game uses; it forwards everything here and mirrors the
// poses into the GPU buffers that the cell, entity and fire passes read.
#include <cstdint>
#include <array>
#include <functional>
#include <memory>
#include <unordered_map>
@@ -22,9 +23,10 @@ namespace ng::phys {
struct MaterialClass {
bool solid = false; // terrain for the bodies (static cells)
bool powder = false; // sand and other powders: they hold a nail but are not terrain
bool powder = false; // dynamic powder: terrain eligibility depends on its live settling state
bool liquid = false; // a liquid that is not a powder (the single probe of the water force of the bodies)
float friction = 0.3f, restitution = 0.2f; // solid_friction / solid_restitution (CellData defaults 0.3 / 0.2)
bool never_box2d = false, sticks_to_ceiling = false;
};
struct Pose {
@@ -36,7 +38,14 @@ struct Pose {
// An impact the game reacts to (fragile props): the speed change of the body at a contact, in cells per frame.
struct Impact { int body = -1; double speed = 0; };
// A new contact of a body (the contact listener's begin, 006f8c20): where, the body's linear velocity (cells per frame) and the closing speed along the normal (cells per frame).
// 006f8c20 -> 009b6070: effects use the first manifold point's added state,
// including a new feature on a persistent contact. Other newly added points do not trigger it.
inline bool material_contact_added(const noita::cpu::ContactManifold& current, const noita::cpu::ContactManifold& previous) {
if (current.count <= 0) return false;
for (int i = 0; i < previous.count; ++i) if (previous.points[i].feature_id == current.points[0].feature_id) return false;
return true;
}
// A newly added first contact point: where, post-step linear velocity and pre-solve closing/point speed.
struct ContactEvent { int body = -1, other = -1; double x = 0, y = 0, vx = 0, vy = 0, normal_speed = 0, mass = 0, point_speed = 0; }; // point_speed: the speed of the body at the contact point (linear + angular), pixels per second // mass: physics units (density x area in unit^2)
class PhysSystem {
@@ -44,11 +53,12 @@ public:
PhysSystem();
void set_materials(std::vector<MaterialClass> table);
int add_shape(const RigidShape& s); // same numbering as GpuBodies::add_shape
int add_shape(const RigidShape& s, int reuse = -1); // same numbering as GpuBodies::add_shape
// `index` is the slot of the GpuBodies mirror. The Box2D body is created at the next step, after the setters below have run (the mass depends on the density).
void add_body(int index, int shape, double anchor_x, double anchor_y, double origin_x, double origin_y);
void remove_body(int index);
void set_motion(int index, double vx, double vy, double omega); // cells and radians per frame
void set_bullet(int index, bool on); // continuous collision with other dynamic debris
void set_surface(int index, double friction, double restitution);
void set_density(int index, double density);
void set_group(int index, double group);
@@ -80,8 +90,9 @@ public:
// b2Body::ApplyLinearImpulse(impulse, point, wake) on body `index`: impulse in kg units / s, the point in pixels (a projectile's hit, 00c073b0)
void impulse_at(int index, double x, double y, double ix, double iy);
// One frame. `cells` is the cell grid of the simulated window (material in the low 16 bits), `view` its world origin.
void step(const uint32_t* cells, int view_x, int view_y, int grid_w, int grid_h);
// One frame. `terrain_age` holds the GPU material stamp (high 16) / rest age (low 8).
// Without live state, dynamic powder is excluded; static cells still collide.
void step(const uint32_t* cells, int view_x, int view_y, int grid_w, int grid_h, const uint32_t* terrain_age = nullptr);
std::unordered_map<int64_t, int> first_seen_; // (update_terrain: the frame a chunk not yet built was first inside the window)
Pose pose(int index) const;
// PhysicsAIComponent (00bce0a0): a force (physics units: mass x units / s^2) and a torque added to the body in the next step, set every frame by the host
@@ -98,21 +109,42 @@ public:
bool gravity_scale(int index, double& g) const;
void set_gravity_scale(int index, double g);
void set_static(int index, bool is_static);
// Native body record +0x29/+0x31/+0x34: distant cell conversion and old-debris settling are separate paths.
struct Lifecycle { int material = -1, target = -1; bool sleep_convert = false, settle_static = false; uint32_t born = 0; };
void set_material_lifecycle(int index, int material, bool sleep_convert, int target);
void set_lifecycle(int index, Lifecycle state);
Lifecycle lifecycle(int index) const;
void set_settle_static(int index, bool on);
// Returns candidates without removing them: the host first persists their remaining pixels.
std::vector<int> terrain_conversion_candidates(int view_x, int view_y, int width, int height);
double mass_of(int index) const; // physics units (0: none or static)
bool nailed(int index) const { return index >= 0 && index < (int)specs_.size() && specs_[index].nail_on; }
std::vector<ContactEvent>& contacts() { return contact_events_; } // new contacts since the last clear
std::vector<Impact>& impacts() { return impacts_; } // since the last call to clear
int terrain_fixtures() const { return terrain_fixtures_; }
// Diagnostic copies use the solver's actual fixture transforms, in world pixels.
struct DebugBody {
int index, cpu, shape, type, group;
bool awake, active, frozen, bullet, nailed;
double x, y, angle, vx, vy, omega, mass;
};
struct DebugPolygon { int body, fixture, category, mask, group; bool sensor; double radius; std::vector<std::array<double, 2>> points; };
struct DebugContact { int a, b; double x, y, separation; };
std::vector<DebugBody> debug_bodies() const;
// Optional pixel-space viewport for drawing; null retains the complete dump.
std::vector<DebugPolygon> debug_polygons(const std::array<double, 4>* viewport = nullptr) const;
std::vector<DebugContact> debug_contacts() const;
private:
struct ShapeData {
std::vector<ConvexPoly> polys; // image pixels
double com_x = 0, com_y = 0, mass = 1, radius = 1;
std::array<double, 4> bounds{}; // min x/y, max x/y in image pixels
std::vector<std::pair<double, double>> samples; // outline samples relative to the pixel centre of mass
};
struct Spec {
int shape = -1;
double ax = 0, ay = 0, ox = 0, oy = 0;
double ax = 0, ay = 0, ox = 0, oy = 0, angle = 0;
double vx = 0, vy = 0, omega = 0;
double friction = 0.3, restitution = 0.2, density = 1.0;
int group = 0;
@@ -126,27 +158,32 @@ private:
bool nail_surface = false, nail_anchored = false; double nail_rx = 0, nail_ry = -10; int nail_ax = 0, nail_ay = 0;
int cpu = -1;
bool live = false, frozen = false;
bool awake_before_freeze = true, raster_dirty = true;
Lifecycle lifecycle;
double ai_fx = 0, ai_fy = 0, ai_torque = 0; uint32_t ai_frame = 0; bool ai_set = false;
double sf_x = 0, sf_y = 0; // forces of scripts (PhysicsApplyForce) for the next step
struct Joint { int other; double ix, iy, force; int kind; int tag = -1; };
std::vector<Joint> joints;
};
void create_pending();
void update_terrain(const uint32_t* cells, int vx, int vy, int gw, int gh);
void update_terrain(const uint32_t* cells, int vx, int vy, int gw, int gh, const uint32_t* terrain_age);
bool chunk_built(int cx, int cy) const { return built_.count(((int64_t)cx << 32) ^ (uint32_t)cy) != 0; }
std::unordered_map<int64_t, std::array<int, 4>> terrain_coverage_;
void apply_liquids(const uint32_t* cells, int vx, int vy, int gw, int gh);
std::vector<MaterialClass> materials_;
std::vector<ShapeData> shapes_;
std::vector<JointBreak> joint_breaks_;
std::vector<Spec> specs_;
noita::cpu::WorldToi world_;
std::unique_ptr<TerrainPhysics> terrain_; // everything solid for the bodies (static cells and powders)
std::unique_ptr<TerrainPhysics> terrain_static_; // the static cells alone: the terrain of the bodies that go through sand
std::vector<uint8_t> solid_lut_; // material id -> 1 when the cell is terrain for the bodies
std::unique_ptr<TerrainPhysics> terrain_; // native class 2: settled loose powder (category 4)
std::unique_ptr<TerrainPhysics> terrain_static_; // fixed class-1 terrain (category 1)
std::unique_ptr<TerrainPhysics> terrain_sticky_; // mobile/attached class-1 powder, separate from fixed terrain
std::vector<uint8_t> solid_lut_; // material id -> 0 excluded, 1 static, 2 live powder classification required
std::vector<uint8_t> cur_, prev_; // solid mask of the window now and at the last step
int prev_vx_ = 0, prev_vy_ = 0, prev_w_ = 0, prev_h_ = 0;
std::unordered_set<int64_t> built_; // chunks that have terrain fixtures
std::unordered_map<int64_t, int> stale_; // built chunks whose cells changed, not rebuilt yet: the frame of the first powder-only change (-1: a solid one)
std::unordered_set<int64_t> stale_; // changed chunks, rebuilt before a nearby body steps
std::unordered_set<int64_t> stale_sticky_; // changed attached/settled class-1 powder
std::unordered_set<int64_t> stale_static_; // (and their static fixtures too)
struct ExplosionRequest { double x, y, radius, fmin, fmax; };
std::vector<ExplosionRequest> pending_explosions_;
@@ -157,6 +194,7 @@ private:
std::unordered_map<int, int> joint_tags_; // add_joint tag -> world joint
int terrain_fixtures_ = 0;
uint32_t frame_ = 0;
double conversion_x_ = 0, conversion_y_ = 0;
};
} // namespace ng::phys
+46
View File
@@ -1,4 +1,5 @@
#include "phys/terrain_outline.h"
#include "clipper.hpp"
#include <algorithm>
#include <cmath>
@@ -165,6 +166,51 @@ ChunkOutline trace_chunk(const SolidFn& solid, int cx, int cy, float tolerance)
return out;
}
ChunkOutline trace_powder_chunk(const SolidFn& solid, int cx, int cy) {
// 008a9250 traces the chunk's matrix; 00810950 offsets each contour
// separately, retains the first result, and bridges its x seams explicitly.
const int x0 = cx * kChunk, y0 = cy * kChunk;
std::vector<uint8_t> mask(kChunk * kChunk);
for (int y = 0; y < kChunk; ++y) for (int x = 0; x < kChunk; ++x)
mask[y * kChunk + x] = solid(x0 + x, y0 + y);
ChunkOutline out;
for (const auto& loop : trace_mask(mask.data(), kChunk, kChunk, 1.5f)) {
if (loop.size() < 4) continue;
ClipperLib::Polygon polygon;
IPoint anchors[2]{};
bool found[2] = {}, used[2] = {};
for (size_t i = 0; i + 1 < loop.size(); ++i) {
const IPoint p{loop[i].x + x0, loop[i].y + y0};
polygon.emplace_back(p.x, p.y);
if ((p.x & 63) != 0) continue;
for (int side = 0; side < 2; ++side) {
const auto& a = anchors[side];
if (!found[side] || (side == 0 ? p.x < a.x : p.x > a.x) || (p.x == a.x && p.y < a.y)) {
anchors[side] = p; found[side] = true;
}
}
}
ClipperLib::Polygons inset;
ClipperLib::OffsetPolygons({polygon}, inset, -4.0, ClipperLib::jtSquare, 0.0, true);
if (inset.empty() || inset.front().empty()) continue;
std::vector<IPoint> line;
for (const auto& v : inset.front()) {
const IPoint p{(int)v.X, (int)v.Y};
for (int side = 0; side < 2; ++side) {
const auto& a = anchors[side];
if (found[side] && !used[side] && std::abs(p.x - a.x) <= 5 && std::abs(p.y - a.y) <= 5) {
out.connectors.push_back({{a.x, std::max(a.y, p.y)}, p});
used[side] = true;
}
}
line.push_back(p); // retain Clipper winding and unsnapped offset vertices
}
line.push_back(line.front());
out.lines.push_back(std::move(line));
}
return out;
}
std::vector<std::vector<IPoint>> trace_mask(const uint8_t* mask, int w, int h, float tolerance, std::vector<bool>* is_hole) {
SolidFn solid = [&](int x, int y) { return x >= 0 && y >= 0 && x < w && y < h && mask[(size_t)y * w + x] != 0; };
auto owned = [](int, int, int) { return true; };
+4
View File
@@ -20,12 +20,16 @@ constexpr int kChunk = 64; // `(x & 0x3f) == 0` in 00810950: the terrain physi
// of a world tile the contours without duplicates and neighbouring chunks meet at the same lattice points.
struct ChunkOutline {
std::vector<std::vector<IPoint>> lines;
std::vector<std::vector<IPoint>> connectors; // 00810950: separate two-point seam fixtures
};
// Traces the contours around chunk (cx, cy) (cells [cx*64, cx*64+64) x [cy*64, cy*64+64)). `tolerance` is the Douglas-Peucker distance in cells (1 as in the
// pixel-body contours, 0095bcf0).
ChunkOutline trace_chunk(const SolidFn& solid, int cx, int cy, float tolerance = 1.0f);
// 00810950: settled powder contours are inset four pixels with Clipper.
ChunkOutline trace_powder_chunk(const SolidFn& solid, int cx, int cy);
// Contours of a whole mask (tests and pixel bodies): the same tracing on the rectangle [0,w) x [0,h) of `mask` (non-zero = solid), without chunk splitting.
std::vector<std::vector<IPoint>> trace_mask(const uint8_t* mask, int w, int h, float tolerance, std::vector<bool>* is_hole = nullptr);
+31 -23
View File
@@ -14,12 +14,18 @@ using noita::cpu::Vec2d;
// (the solid side, n = (dy, -dx) / len) with no extension along the segment (the extension argument is 0 at both call sites in 008102c0). The thickness is
// 0.2 * 0.5 = 0.1 unit (one cell); segments shorter than 0.5 unit and segments in the bottom 0.4 unit of the chunk (local y >= 6.0) are drawn with
// 0.2 * 0.2 * 0.25 = 0.01 unit.
bool segment_quad(Vec2d p, Vec2d q, double chunk_bottom_local_y, std::vector<Vec2d>& out) {
bool segment_quad(Vec2d p, Vec2d q, double chunk_bottom_local_y, bool powder, bool connector, std::vector<Vec2d>& out) {
const double dx = q.x - p.x, dy = q.y - p.y, len2 = dx * dx + dy * dy;
if (len2 <= 0.0) return false;
const double len = std::sqrt(len2);
const bool thin = (p.y >= chunk_bottom_local_y && q.y >= chunk_bottom_local_y) || len2 < 0.25;
const double t = thin ? 0.01 : 0.1;
// 0080f9f0's class-2 flag accepts only upward-facing long surfaces.
// Short/bottom segments use 0080fc20, which has no angle filter.
if (powder && !thin && !connector) {
const double angle = std::atan2(-dx, dy);
if (angle < -3.1415 || angle >= 0.0) return false;
}
const double t = connector ? 0.05 : thin ? 0.01 : 0.1;
const double nx = dy / len * t, ny = -dx / len * t;
out = {p, q, {q.x + nx, q.y + ny}, {p.x + nx, p.y + ny}};
double area2 = 0;
@@ -44,7 +50,7 @@ void TerrainPhysics::remove_chunk(int cx, int cy) {
int TerrainPhysics::rebuild_chunk(const SolidFn& solid, const CoefficientFn& coeff, int cx, int cy) {
using namespace noita::cpu;
ChunkOutline outline = trace_chunk(solid, cx, cy);
ChunkOutline outline = category_ == 4 ? trace_powder_chunk(solid, cx, cy) : trace_chunk(solid, cx, cy, 1.5f);
int body = chunk_body(cx, cy);
if (body < 0) {
BodyDefinition def;
@@ -59,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<Vec2d> 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<Vec2d> 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;
+3 -4
View File
@@ -10,7 +10,6 @@
namespace ng::phys {
constexpr std::uint16_t kStaticOnlyCategory = 0x4000; // (not an engine category: the second terrain of the static cells, see PhysSystem)
constexpr double kPixelsPerUnit = 10.0; // 00718740: one physics unit is ten cells (`mConverts.rate` = W / (W/10)) in both axes
struct TerrainCoefficients { float friction = 0.3f, restitution = 0.2f; }; // CellData defaults solid_friction / solid_restitution
@@ -18,8 +17,8 @@ using CoefficientFn = std::function<TerrainCoefficients(int cellX, int cellY)>;
class TerrainPhysics {
public:
// category: the fixture category of this terrain (4 = 008102c0's, everything solid for the bodies; kStaticOnlyCategory = the static cells alone, for the bodies that go through sand)
explicit TerrainPhysics(noita::cpu::WorldToi& world, std::uint16_t category = 4) : w_(world), category_(category) {}
// Native terrain classes: category 1 = static cells; category 4 = settled powder.
explicit TerrainPhysics(noita::cpu::WorldToi& world, std::uint16_t category = 1) : w_(world), category_(category) {}
// Rebuilds the fixtures of chunk (cx, cy) from the solid mask. Returns the number of segment fixtures.
int rebuild_chunk(const SolidFn& solid, const CoefficientFn& coeff, int cx, int cy);
void remove_chunk(int cx, int cy);
@@ -28,7 +27,7 @@ public:
private:
static int64_t key(int cx, int cy) { return ((int64_t)cx << 32) ^ (uint32_t)cy; }
noita::cpu::WorldToi& w_;
std::uint16_t category_ = 4;
std::uint16_t category_ = 1;
std::unordered_map<int64_t, int> bodies_;
};
+68 -5
View File
@@ -19,22 +19,46 @@ void GpuBodies::attach(void* bodyBuffer, uint32_t* shapeWords) {
words_ = shapeWords;
next_ = (size_t)kMaxShapes * kShapeHeaderWords;
shapes_ = 0;
shape_epoch_ = 0; shape_live_.clear();
info_.clear();
}
int GpuBodies::add_shape(const RigidShape& s) {
const size_t need = (size_t)s.w * s.h + s.samples.size() * 2;
if (shapes_ >= kMaxShapes || next_ + need > kShapeWords) return -1;
uint32_t* h = words_ + (size_t)shapes_ * kShapeHeaderWords;
int index = -1;
for (int k = 0; k < shapes_; ++k) if (!shape_live_[k]) { index = k; break; }
if ((index < 0 && shapes_ >= kMaxShapes) || next_ + need > kShapeWords) {
// Dead bodies recycled their body slots but permanently consumed shape
// slots and mask storage. Reclaim both, retaining every live/pending body.
std::vector<uint8_t> used(shapes_);
for (int k = 0; k < *count_; ++k) if (!(bodies_[k].i[2] & 2)) used[bodies_[k].i[0]] = 1;
std::vector<uint32_t> packed;
const size_t base = (size_t)kMaxShapes * kShapeHeaderWords;
for (int k = 0; k < shapes_; ++k) if (used[k]) {
uint32_t* h = words_ + (size_t)k * kShapeHeaderWords;
const size_t pixels = (size_t)h[0] * h[1], samples = (size_t)h[4] * 2;
const size_t mask = packed.size(); packed.insert(packed.end(), words_ + h[2], words_ + h[2] + pixels);
const size_t points = packed.size(); packed.insert(packed.end(), words_ + h[3], words_ + h[3] + samples);
h[2] = (uint32_t)(base + mask); h[3] = (uint32_t)(base + points);
} else std::memset(words_ + (size_t)k * kShapeHeaderWords, 0, kShapeHeaderWords * 4);
std::copy(packed.begin(), packed.end(), words_ + base);
next_ = base + packed.size(); shape_live_ = std::move(used); ++shape_epoch_;
index = -1;
for (int k = 0; k < shapes_; ++k) if (!shape_live_[k]) { index = k; break; }
}
if ((index < 0 && shapes_ >= kMaxShapes) || next_ + need > kShapeWords) return -1;
if (index < 0) { index = shapes_++; shape_live_.push_back(0); info_.push_back({}); }
shape_live_[index] = 1;
uint32_t* h = words_ + (size_t)index * kShapeHeaderWords;
const size_t maskOff = next_, sampOff = next_ + (size_t)s.w * s.h;
for (size_t k = 0; k < (size_t)s.w * s.h; ++k) words_[maskOff + k] = s.mask[k];
for (size_t k = 0; k < s.samples.size(); ++k) { words_[sampOff + 2 * k] = bits((float)s.samples[k].first); words_[sampOff + 2 * k + 1] = bits((float)s.samples[k].second); }
h[0] = (uint32_t)s.w; h[1] = (uint32_t)s.h; h[2] = (uint32_t)maskOff; h[3] = (uint32_t)sampOff; h[4] = (uint32_t)s.samples.size();
h[5] = bits((float)s.com_x); h[6] = bits((float)s.com_y); h[7] = bits((float)s.mass); h[8] = bits((float)s.inertia); h[9] = bits((float)s.radius);
next_ += need;
info_.push_back({(float)s.radius, (float)s.mass, (float)s.com_x, (float)s.com_y});
if (phys_) phys_->add_shape(s);
return shapes_++;
info_[index] = {(float)s.radius, (float)s.mass, (float)s.com_x, (float)s.com_y};
if (phys_) phys_->add_shape(s, index);
return index;
}
int GpuBodies::add_body(int shape, int slot, double ax, double ay, double ox, double oy) {
@@ -77,6 +101,27 @@ bool GpuBodies::add_joint(int a, int b, double px, double py, double break_force
return true;
}
bool GpuBodies::has_body_capacity() const {
if (*count_ < kMaxBodies) return true;
for (int k = 0; k < *count_; ++k) if (bodies_[k].i[2] & 2) return true;
return false;
}
bool GpuBodies::has_shape_capacity(int width, int height) const {
// make_shape keeps at most 127 outline samples. Count storage after the
// same live-shape compaction as add_shape, without modifying the buffers.
const size_t need = (size_t)width * height + 254;
if (shapes_ < kMaxShapes && next_ + need <= kShapeWords) return true;
std::vector<uint8_t> used(shapes_);
for (int k = 0; k < *count_; ++k) if (!(bodies_[k].i[2] & 2)) used[bodies_[k].i[0]] = 1;
int live = 0; size_t words = (size_t)kMaxShapes * kShapeHeaderWords;
for (int k = 0; k < shapes_; ++k) if (used[k]) {
const uint32_t* h = words_ + (size_t)k * kShapeHeaderWords;
words += (size_t)h[0] * h[1] + (size_t)h[4] * 2; ++live;
}
return live < kMaxShapes && words + need <= kShapeWords;
}
void GpuBodies::remove_slot(int slot) {
for (int k = 0; k < *count_; ++k) if (bodies_[k].i[1] == slot) { bodies_[k].i[2] |= 2; if (phys_) phys_->remove_body(k); }
}
@@ -171,15 +216,33 @@ void GpuBodies::set_surface(int body, double friction, double restitution) {
}
void GpuBodies::set_props(int body, double ld, double ad, bool sleep, bool fixed, bool bullet, bool stat) {
if (bullet) bodies_[body].i[2] |= kContinuousCollision; else bodies_[body].i[2] &= ~kContinuousCollision;
bodies_[body].d[1] = (float)ld; bodies_[body].d[2] = (float)ad;
if (phys_) phys_->set_props(body, ld, ad, sleep, fixed, bullet, stat);
}
void GpuBodies::set_bullet(int body, bool on) {
if (on) bodies_[body].i[2] |= kContinuousCollision; else bodies_[body].i[2] &= ~kContinuousCollision;
if (phys_) phys_->set_bullet(body, on);
}
void GpuBodies::set_density(int body, double density) {
bodies_[body].q[2] = (float)density;
if (phys_) phys_->set_density(body, density);
}
void GpuBodies::inherit_material(int body, const GpuBody& source) {
set_surface(body, source.v[3], source.v[2]);
set_density(body, source.q[2]);
set_group(body, source.m[0], source.m[1] > .5f);
bodies_[body].d[1] = source.d[1]; bodies_[body].d[2] = source.d[2];
bodies_[body].i[2] = (bodies_[body].i[2] & ~(512 | 1024 | 65536)) | (source.i[2] & (512 | 1024 | 65536));
if (phys_) {
phys_->set_go_through_sand(body, (source.i[2] & 512) != 0);
phys_->set_damping(body, source.d[1], source.d[2], true);
}
}
void GpuBodies::sync_from_physics() {
if (!phys_) return;
for (int k = 0; k < *count_; ++k) {
+9
View File
@@ -29,6 +29,7 @@ static_assert(sizeof(GpuBody) == 224, "GpuBody layout");
class GpuBodies {
public:
static constexpr int kMaxBodies = 1024, kMaxShapes = 512, kShapeHeaderWords = 16;
static constexpr int kContinuousCollision = 2048; // 1024 is reserved for character pass-through
static constexpr size_t kBodyBytes = 16 + sizeof(GpuBody) * kMaxBodies;
static constexpr size_t kShapeWords = 1u << 20;
@@ -41,13 +42,18 @@ public:
int add_shape(const RigidShape& s); // -1 when the shape table is full
int add_body(int shape, int slot, double anchor_x, double anchor_y, double ox, double oy); // body index or -1
int count() const { return *count_; }
bool has_body_capacity() const;
bool has_shape_capacity(int width, int height) const; // conservative preflight before extracting terrain
void remove_slot(int slot);
void set_group(int body, double group, bool passable);
void set_motion(int body, double vx, double vy, double omega);
void set_bullet(int body, bool on);
void inherit_material(int body, const GpuBody& source); // fracture: preserve density and collision response
void set_fragile(int body, double impact_speed) { bodies_[body].n[0] = (float)impact_speed; } // damaged by impacts faster than this (cells per frame)
// hangs on the wall at that point; break_force is the PhysicsJoint2Component multiplier (0 = only the loss of the surface lets it fall)
void set_nail(int body, double x, double y, double radius, double break_force = 0.0);
void set_surface_nail(int body, double x, double y, double ray_x, double ray_y, double break_force);
void set_material_reactions(int body, bool enabled) { if (enabled) bodies_[body].i[2] &= ~65536; else bodies_[body].i[2] |= 65536; }
void set_char_pass(int body) { bodies_[body].i[2] |= 1024; } // flag 1024: the material is no platform (platform_type not 1 / 2): the player and the creatures pass (rigid_raster.comp marks its cells)
void set_go_through_sand(int body); // flag 512: the body sinks through powders (meets the static terrain only, pushes no grains) // hangs on the wall at that point; break_force is the PhysicsJoint2Component multiplier (0 = only the loss of the surface lets it fall)
void wake(double x, double y, double radius);
@@ -64,6 +70,7 @@ public:
void set_burning(int body, double frames) { bodies_[body].m[2] = (float)frames; }
const uint32_t* shape_words() const { return words_; } // the mapped shape table (headers of kShapeHeaderWords words: w, h, mask offset, ..., pixel count at 7)
size_t shape_words_used() const { return next_; } // words of the shape table in use (header area + data), for incremental uploads
uint64_t shape_epoch() const { return shape_epoch_; } // cache invalidation / full GPU upload after compaction
private:
phys::PhysSystem* phys_ = nullptr;
@@ -72,6 +79,8 @@ private:
uint32_t* words_ = nullptr;
size_t next_ = 0;
int shapes_ = 0;
uint64_t shape_epoch_ = 0;
std::vector<uint8_t> shape_live_;
struct Info { float radius, mass, com_x, com_y; };
std::vector<Info> info_;
};
+37 -8
View File
@@ -18,6 +18,7 @@ std::vector<int> matching(const MaterialDb& db, const std::string& pat) {
if (std::find(m.tags.begin(), m.tags.end(), tag) != m.tags.end()) r.push_back(m.index);
} else if (const Material* m = db.find(pat)) {
r.push_back(m->index);
if (db.loose_twin(m->index) >= 0) r.push_back(db.loose_twin(m->index));
}
return r;
}
@@ -29,25 +30,50 @@ int output_of(const MaterialDb& db, const std::string& pat, int input) {
if (pat.front() == '[') {
const size_t close = pat.find(']');
if (close == std::string::npos || close + 1 >= pat.size()) return -1;
const Material* m = db.find(db.materials()[input].name + pat.substr(close + 1));
return m ? m->index : -1;
const Material* m = db.find(db.materials()[db.canonical(input)].name + pat.substr(close + 1));
return m ? (m->index == db.canonical(input) ? input : m->index) : -1;
}
const Material* m = db.find(pat);
return m ? m->index : -1;
return m ? (m->index == db.canonical(input) ? input : m->index) : -1;
}
} // namespace
std::vector<uint32_t> build_thermal_conversions(const MaterialDb& db) {
std::vector<uint32_t> result(db.materials().size() * 2u, 0u);
for (const Material& m : db.materials()) {
if (db.canonical(m.index) != m.index) continue;
auto target = [&](const char* key) { const auto it = m.attrs.find(key); const auto* t = it == m.attrs.end() ? nullptr : db.find(it->second); return t && t->index > 0 ? uint32_t(t->index + 1) : 0u; };
const uint32_t cold = target("cold_freezes_to_material");
result[2u * m.index] = cold | (uint32_t(std::clamp(attr_int(m, "cold_freezes_chance_rev", 100), 0, 32767)) << 16);
if (cold != 0u && attr_int(m, "cold_freezes_to_dont_do_reverse_reaction", 0) == 0)
result[2u * (cold - 1u) + 1u] = (result[2u * (cold - 1u) + 1u] & 0xFFFF0000u) | uint32_t(m.index + 1);
// The target may be finalized later; its explicit (including empty) warm
// target then overwrites the reverse assignment, just as in 00707990.
result[2u * m.index + 1u] = target("warmth_melts_to_material") | (uint32_t(std::clamp(attr_int(m, "warmth_melts_chance_rev", 100), 0, 32767)) << 16);
}
for (const Material& m : db.materials()) {
if (db.canonical(m.index) == m.index) continue;
result[2u * m.index] = result[2u * db.canonical(m.index)];
result[2u * m.index + 1u] = result[2u * db.canonical(m.index) + 1u];
}
return result;
}
ReactionTables build_reaction_tables(const MaterialDb& db) {
ReactionTables t;
t.n = (int)db.materials().size();
struct Entry { uint32_t outs, prob, third, blob, entity; }; // prob: probability in 1/65536 | direction code << 16 (0 any, 1 top, 2 bottom, 3 left, 4 right)
struct Entry { uint32_t outs, prob, third, blob, entity; }; // prob: XML percent * 100 | direction code << 16 (0 any, 1 top, 2 bottom, 3 left, 4 right)
const uint32_t stride = uint32_t(t.n) * (t.n + 1u);
std::map<uint32_t, std::vector<Entry>> lists; // key a * n + b
for (const Reaction& r : db.reactions()) {
auto life = r.attrs.find("req_lifetime");
const auto fast = r.attrs.find("fast_reaction");
const uint32_t base = fast != r.attrs.end() && std::atoi(fast->second.c_str()) != 0 ? stride : 0;
const std::vector<int> A = matching(db, r.in[0]), B = matching(db, r.in[1]);
std::vector<int> C = r.in[2].empty() ? std::vector<int>{-1} : matching(db, r.in[2]); // third input (the cell next to the first one), air included
if (r.in[2] == "air") C = {0};
C.erase(std::remove_if(C.begin(), C.end(), [&](int m) { return m >= 0 && db.canonical(m) != m; }), C.end());
if (A.empty() || B.empty() || C.empty()) continue;
if (!r.in[2].empty()) ++t.three_input;
uint32_t prob = (uint32_t)std::min(65535.0, std::max(0.0, std::floor(r.probability * 100.0 + 0.5))); // the native threshold: R = next15 % 10001 is accepted when R <= threshold (0070eaf0), the probability is in percent
@@ -75,15 +101,18 @@ ReactionTables build_reaction_tables(const MaterialDb& db) {
const int o1 = output_of(db, r.out[0], a), o2 = output_of(db, r.out[1], b), o3 = c < 0 ? -1 : output_of(db, r.out[2], c);
const uint32_t outA = o1 < 0 ? 0xFFFFu : (uint32_t)o1, outB = o2 < 0 ? 0xFFFFu : (uint32_t)o2, outC = o3 < 0 ? 0xFFFFu : (uint32_t)o3;
const uint32_t third = (c < 0 ? 0xFFFFu : (uint32_t)c) | outC << 16;
lists[(uint32_t)a * t.n + b].push_back({outA | outB << 16, prob | dir << 16, third, blob, entityIdx});
if (a != b) lists[(uint32_t)b * t.n + a].push_back({outB | outA << 16, prob | rdir << 16, third, rblob, entityIdx | 0x80000000u}); // bit 31 of the entity word: the record is the mirrored one (the first input is the second material)
lists[base + (uint32_t)a * t.n + b].push_back({outA | outB << 16, prob | dir << 16, third, blob, entityIdx});
if (a != b) lists[base + (uint32_t)b * t.n + a].push_back({outB | outA << 16, prob | rdir << 16, third, rblob, entityIdx | 0x80000000u}); // bit 31 of the entity word: the record is the mirrored one (the first input is the second material)
}
}
t.head.assign((size_t)t.n * t.n + t.n, 0u); // then per material the largest probability of a reaction it starts (CellData + 0x22c)
t.head.assign(2u * stride + t.n, 0u);
for (int m = 0; m < t.n; ++m) t.head[2u * stride + m] = uint32_t(db.canonical(m)); // then per material the largest probability of a reaction it starts (CellData + 0x22c)
for (auto& [key, v] : lists) {
if (v.size() > 255) v.resize(255);
t.head[key] = (uint32_t)(t.rec.size() / 5) << 8 | (uint32_t)v.size();
for (const Entry& e : v) t.head[(size_t)t.n * t.n + key / t.n] = std::max(t.head[(size_t)t.n * t.n + key / t.n], e.prob & 0xFFFFu);
const uint32_t base = key >= stride ? stride : 0;
const size_t threshold = base + (size_t)t.n * t.n + (key - base) / t.n;
for (const Entry& e : v) t.head[threshold] = std::max(t.head[threshold], e.prob & 0xFFFFu);
for (const Entry& e : v) { t.rec.push_back(e.outs); t.rec.push_back(e.prob); t.rec.push_back(e.third); t.rec.push_back(e.blob); t.rec.push_back(e.entity); }
t.entries += (int)v.size();
}
+6 -2
View File
@@ -15,10 +15,12 @@ namespace ng {
struct ReactionTables {
int n = 0; // number of materials
// head[a * n + b] = (offset << 8) | count into `rec` (count <= 255)
// Two blocks of n*n pair heads followed by n thresholds: combined, then fast.
// Finally n canonical material IDs (loose twins retain native CellData identity).
// head[base + a * n + b] = (offset << 8) | count into `rec` (count <= 255)
std::vector<uint32_t> head;
// five words per entry (the fourth: blob radius of side a | radius of side b << 8 | restrict a << 16 | restrict b << 17 | destroy_horizontally_lonely_pixels << 19 | req_lifetime << 20;
// the fifth: index + 1 into `entities` of the entity the reaction spawns at the first cell, 0 = none): (out_a | out_b << 16) with 0xFFFF = unchanged; the probability per attempt in 1/65536 | direction << 16;
// the fifth: index + 1 into `entities` of the entity the reaction spawns at the first cell, 0 = none): (out_a | out_b << 16) with 0xFFFF = unchanged; the native probability threshold (XML percent * 100) | direction << 16;
// the third input (0xFFFF = none) | out_c << 16 (0xFFFF = unchanged) | bit 31 = the third cell is looked up around the cell of the second slot
std::vector<uint32_t> rec;
std::vector<std::string> entities; // paths of the entities of the reactions (`entity=`)
@@ -26,5 +28,7 @@ struct ReactionTables {
};
ReactionTables build_reaction_tables(const MaterialDb& db);
// Material finalization order and reverse cold conversion (00707990).
std::vector<uint32_t> build_thermal_conversions(const MaterialDb& db);
} // namespace ng
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <cmath>
#include <map>
#include <string>
namespace ng {
// data/scripts/biomes/temple_shared.lua: temple_pos_to_id and
// temple_should_we_spawn_checkers. Deferred spawns must use the current run state.
inline bool temple_breach_check_retired(double x, double y, const std::map<std::string, std::string>& globals) {
const auto room = std::to_string((int)std::floor((x + 32 * 512) / (64 * 512))) + "_" +
std::to_string((int)std::floor(y / 512));
for (const char* prefix : {"TEMPLE_COLLAPSED_", "TEMPLE_LEAKED_"}) {
const auto value = globals.find(prefix + room);
if (value != globals.end() && value->second == "1") return true;
}
return false;
}
} // namespace ng