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