cb73cb7ccc
Co-Authored-By: Codex <noreply@openai.com>
875 lines
34 KiB
Rust
875 lines
34 KiB
Rust
//! What the game's blocks are to the simulation (issues #42, #43): a cache of
|
|
//! 16-block sections answered by `render.sections` (a bit per block that
|
|
//! stops a projectile), a swept segment through it, and the bookkeeping that
|
|
//! keeps the questions few: the sections along a flight asked for ahead of
|
|
//! time, at most 64 a tick, nearest need first; changes of the world marking
|
|
//! sections stale; a bounded cache. A chunk the game doesn't have answers
|
|
//! `Unknown`, which is never the same as air: the callers fall back to the
|
|
//! height of the land there and the counters say how often.
|
|
use glam::DVec3;
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
/// A section: block coordinates divided by 16.
|
|
pub type Sec = (i32, i32, i32);
|
|
|
|
/// Sections asked in one tick (the game's budget).
|
|
pub const PER_TICK: usize = 64;
|
|
/// Sections kept; the least recently used go first beyond it.
|
|
const MAX_SECTIONS: usize = 12_000;
|
|
/// Ticks before an unanswered or unknown section is asked again.
|
|
const RETRY: u64 = 60;
|
|
/// Ticks after which a section in use is read again, whether or not a change was reported (#62).
|
|
const RECHECK: u64 = 1200;
|
|
/// Longest stretch of a flight asked for ahead, in blocks.
|
|
const AHEAD_MAX: f64 = 192.0;
|
|
/// Questions waiting: more are dropped (they are asked again when needed).
|
|
const QUEUE_MAX: usize = 512;
|
|
|
|
/// What a section holds, as the game answered.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum Fill {
|
|
/// The game has no such chunk.
|
|
Unknown,
|
|
Empty,
|
|
Full,
|
|
/// 512 bytes, one bit per block, index `(y & 15) * 256 + (z & 15) * 16 + (x & 15)`.
|
|
Mixed(Box<[u8; 512]>),
|
|
}
|
|
|
|
/// An answer for a section, as the game gives it.
|
|
pub struct SectionAnswer {
|
|
pub sec: Sec,
|
|
pub fill: Fill,
|
|
}
|
|
|
|
struct Entry {
|
|
fill: Fill,
|
|
/// A block changed (or may have) since it was read: asked again when next used.
|
|
stale: bool,
|
|
/// The tick it was last used in a sweep.
|
|
used: u64,
|
|
/// The tick it was answered.
|
|
since: u64,
|
|
}
|
|
|
|
/// How a swept segment ended.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum Sweep {
|
|
/// Every block along it is known and none stops it.
|
|
Clear,
|
|
/// The first solid block: where the segment meets it, which way the face looks, and the block.
|
|
Hit { t: f64, point: DVec3, normal: [i32; 3], block: [i32; 3] },
|
|
/// The walk reached blocks the game doesn't know (before any solid one), `t` of the way. `pending`: the
|
|
/// section hasn't been answered yet (it is asked for, urgently); else the game said it has no chunk there.
|
|
Unknown { t: f64, pending: bool },
|
|
/// A solid block was met at `t` and its exact shape is being asked for (#63): the flight holds at its face.
|
|
Wait { t: f64 },
|
|
}
|
|
|
|
impl Sweep {
|
|
pub fn hit(&self) -> Option<(f64, DVec3)> {
|
|
match *self {
|
|
Sweep::Hit { t, point, .. } => Some((t, point)),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Counters for the report: questions, unknowns, memory, cost.
|
|
#[derive(Clone, Copy, Default, Debug)]
|
|
pub struct WorldStats {
|
|
pub sweeps: u64,
|
|
pub cells: u64,
|
|
pub hits: u64,
|
|
pub unknown_sweeps: u64,
|
|
pub sections_asked: u64,
|
|
pub sections_answered: u64,
|
|
pub sections_unknown: u64,
|
|
pub invalidated: u64,
|
|
pub evicted: u64,
|
|
pub world_changes: u64,
|
|
/// Steps cut short at unread blocks (#61), and steps let through after waiting for too long.
|
|
pub held: u64,
|
|
pub passed_unknown: u64,
|
|
/// Sections dropped or re-read for a chunk's unload or replacement, or for age (#62).
|
|
pub aged: u64,
|
|
pub columns_changed: u64,
|
|
/// Blasts asked in the blocks, and what became of them (`Settled`).
|
|
pub blasts_asked: u64,
|
|
pub blasts_confirmed: u64,
|
|
pub blasts_unchanged: u64,
|
|
pub blasts_unloaded: u64,
|
|
/// Answers dropped for being to a question of another world or dimension.
|
|
pub late_dropped: u64,
|
|
}
|
|
|
|
/// What the game's exact ray said of a flight's step, once it answered (#63).
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum Exact {
|
|
Pending,
|
|
/// The collision shapes leave the way open.
|
|
Clear,
|
|
/// Where the shapes stop it: the point, the face and the block.
|
|
Hit { point: DVec3, normal: [i32; 3], block: [i32; 3] },
|
|
}
|
|
|
|
struct RayEntry {
|
|
id: u32,
|
|
a: DVec3,
|
|
b: DVec3,
|
|
state: Exact,
|
|
asked: u64,
|
|
/// When this wait began: re-asking for a changed step does not restart the clock (#73).
|
|
first: u64,
|
|
/// No usable exact answer (timed out, or no verdict): the solid cube of the sections stands.
|
|
cube: bool,
|
|
}
|
|
|
|
/// The game's verdict on an exact ray, as `render.rays` gives it.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct RayReply {
|
|
pub id: u32,
|
|
pub clear: bool,
|
|
pub hit: bool,
|
|
pub fraction: f64,
|
|
pub normal: [i32; 3],
|
|
pub block: [i32; 3],
|
|
}
|
|
|
|
/// Ticks an exact ray may take to be answered before the block counts as the solid cube the sections say.
|
|
const RAY_WAIT: u64 = 30;
|
|
/// Exact rays asked in one tick (the game's budget is 256).
|
|
/// How many steps long the exact ray is asked.
|
|
const RAY_REACH: f64 = 3.0;
|
|
const RAYS_PER_TICK: usize = 128;
|
|
|
|
/// Whether step `a`..`b` lies on the asked segment `a0`..`b0` (same line, same way, inside it, give or take a
|
|
/// hair): the fractions of its ends along the asked one.
|
|
fn within(a0: DVec3, b0: DVec3, a: DVec3, b: DVec3) -> Option<(f64, f64)> {
|
|
const EPS: f64 = 1e-3;
|
|
let d0 = b0 - a0;
|
|
let l2 = d0.length_squared();
|
|
if l2 < 1e-12 {
|
|
return (a.distance(a0) < EPS && b.distance(b0) < EPS).then_some((0.0, 1.0));
|
|
}
|
|
let (sa, sb) = ((a - a0).dot(d0) / l2, (b - a0).dot(d0) / l2);
|
|
let len = l2.sqrt();
|
|
let tol = EPS / len;
|
|
if sa < -tol || sb > 1.0 + tol || sb <= sa || a.distance(a0 + d0 * sa) > EPS || b.distance(a0 + d0 * sb) > EPS {
|
|
return None;
|
|
}
|
|
Some((sa.max(0.0), sb.min(1.0)))
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct RayDesk {
|
|
next: u32,
|
|
/// By flight and block: a flight that meets the same block again finds its answer.
|
|
entries: HashMap<(u32, [i32; 3]), RayEntry>,
|
|
by_id: HashMap<u32, (u32, [i32; 3])>,
|
|
out: Vec<(u32, DVec3, DVec3)>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct WorldCache {
|
|
rays: RayDesk,
|
|
secs: HashMap<Sec, Entry>,
|
|
urgent: Vec<Sec>,
|
|
ahead: Vec<Sec>,
|
|
queued: HashSet<Sec>,
|
|
/// Sections asked, and when.
|
|
asked: HashMap<Sec, u64>,
|
|
epoch: u32,
|
|
/// The game's count of block changes, as of the last note, and the one the section entries were read at.
|
|
revision: u64,
|
|
/// The last `solid` that could not tell: the section was not answered yet.
|
|
unread_pending: bool,
|
|
/// Sweeps made as a look ahead (flight planning) ask for sections after the urgent ones.
|
|
quiet: bool,
|
|
/// Columns (section x, z) where blocks changed since the land heights last looked (#64); `all`: too many to say.
|
|
pub(super) changed_cols: HashSet<(i32, i32)>,
|
|
pub(super) changed_all: bool,
|
|
/// Another world or dimension opened since the land heights last looked: they are all void.
|
|
pub(super) epoch_reset: bool,
|
|
/// Blasts the mod asked the game to make in the blocks (`Out.commands`), waiting for the game's word that
|
|
/// blocks changed there. The game's damage (the simulation's own) and the geometry the game confirms
|
|
/// are two things: this is the second (#90). Singleplayer: the client's world is the authority.
|
|
expected: Vec<Expected>,
|
|
pub stats: WorldStats,
|
|
}
|
|
|
|
/// A blast asked for: the sections it reaches (lowest, highest) and the tick it was asked.
|
|
struct Expected {
|
|
lo: Sec,
|
|
hi: Sec,
|
|
at: DVec3,
|
|
since: u64,
|
|
}
|
|
|
|
/// Ticks the game has to report a block changed after a blast was asked for (the fuse is 0; commands run 64 a tick).
|
|
pub const CONFIRM_TICKS: u64 = 100;
|
|
|
|
/// What became of a blast asked for in the blocks.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum Settled {
|
|
/// The game reported blocks changed in the sections it reaches.
|
|
Confirmed,
|
|
/// The chunk was there and nothing changed: nothing to break, a rule or the server's rights said no.
|
|
Unchanged,
|
|
/// There was no chunk to change.
|
|
Unloaded,
|
|
}
|
|
|
|
fn sec_of(x: i32, y: i32, z: i32) -> Sec {
|
|
(x >> 4, y >> 4, z >> 4)
|
|
}
|
|
|
|
impl WorldCache {
|
|
/// Memory the cached sections hold, about.
|
|
pub fn bytes(&self) -> usize {
|
|
self.secs
|
|
.values()
|
|
.map(|e| 48 + if matches!(e.fill, Fill::Mixed(_)) { 512 } else { 0 })
|
|
.sum()
|
|
}
|
|
pub fn len(&self) -> usize {
|
|
self.secs.len()
|
|
}
|
|
pub fn is_empty(&self) -> bool {
|
|
self.secs.is_empty()
|
|
}
|
|
/// The game's note of changes: another world voids everything; changed
|
|
/// sections (or too many to say) are read again when next used.
|
|
pub fn changed(&mut self, epoch: u32, overflow: bool, sections: &[Sec]) {
|
|
self.changed_rev(epoch, self.revision, overflow, sections);
|
|
}
|
|
|
|
/// The same with the game's change counter: a jump in it that the listed sections don't explain
|
|
/// (changes the game didn't list, a chunk replaced behind the subscription's back) makes every section
|
|
/// read before it due for a new read when it is next used (#62).
|
|
pub fn changed_rev(&mut self, epoch: u32, revision: u64, overflow: bool, sections: &[Sec]) {
|
|
let unexplained = self.epoch == epoch && revision != self.revision && sections.is_empty() && !overflow;
|
|
self.revision = revision;
|
|
if unexplained {
|
|
// Not named, so not known which: a light update alone doesn't move the counter, a block change does.
|
|
for e in self.secs.values_mut() {
|
|
e.stale = true;
|
|
}
|
|
self.stats.invalidated += self.secs.len() as u64;
|
|
}
|
|
if epoch != self.epoch {
|
|
self.rays = RayDesk { next: self.rays.next, ..RayDesk::default() };
|
|
if self.epoch != 0 {
|
|
self.epoch_reset = true;
|
|
self.expected.clear();
|
|
self.secs.clear();
|
|
self.urgent.clear();
|
|
self.ahead.clear();
|
|
self.queued.clear();
|
|
self.asked.clear();
|
|
self.changed_all = true;
|
|
}
|
|
self.epoch = epoch;
|
|
}
|
|
if overflow || unexplained {
|
|
self.changed_all = true;
|
|
self.drop_rays(|_| true);
|
|
}
|
|
if overflow {
|
|
for e in self.secs.values_mut() {
|
|
e.stale = true;
|
|
}
|
|
self.stats.invalidated += self.secs.len() as u64;
|
|
}
|
|
if !sections.is_empty() {
|
|
let set: HashSet<Sec> = sections.iter().copied().collect();
|
|
self.drop_rays(|b| set.contains(&sec_of(b[0], b[1], b[2])));
|
|
}
|
|
if !self.expected.is_empty() {
|
|
let before = self.expected.len();
|
|
self.expected.retain(|e| !sections.iter().any(|s| (e.lo.0..=e.hi.0).contains(&s.0) && (e.lo.1..=e.hi.1).contains(&s.1) && (e.lo.2..=e.hi.2).contains(&s.2)));
|
|
self.stats.blasts_confirmed += (before - self.expected.len()) as u64;
|
|
}
|
|
for s in sections {
|
|
self.note_column(s.0, s.2);
|
|
if let Some(e) = self.secs.get_mut(s) {
|
|
e.stale = true;
|
|
self.stats.invalidated += 1;
|
|
}
|
|
}
|
|
self.stats.world_changes += sections.len() as u64;
|
|
}
|
|
|
|
/// The game's epoch as last told: another world or dimension opening changes it.
|
|
pub fn epoch(&self) -> u32 {
|
|
self.epoch
|
|
}
|
|
|
|
/// A blast of `radius` blocks was asked of the game at `at`: expect the game to report blocks changed there.
|
|
pub fn expect_blast(&mut self, at: DVec3, radius: f64, now: u64) {
|
|
let (lo, hi) = (at - DVec3::splat(radius), at + DVec3::splat(radius));
|
|
let sec = |p: DVec3| sec_of(p.x.floor() as i32, p.y.floor() as i32, p.z.floor() as i32);
|
|
if self.expected.len() < 256 {
|
|
self.expected.push(Expected { lo: sec(lo), hi: sec(hi), at, since: now });
|
|
}
|
|
self.stats.blasts_asked += 1;
|
|
}
|
|
|
|
/// The blasts that the game did not report in `CONFIRM_TICKS`, taken: where each was.
|
|
pub fn overdue_blasts(&mut self, now: u64) -> Vec<DVec3> {
|
|
let (late, keep): (Vec<_>, Vec<_>) = std::mem::take(&mut self.expected).into_iter().partition(|e| now.saturating_sub(e.since) >= CONFIRM_TICKS);
|
|
self.expected = keep;
|
|
late.into_iter().map(|e| e.at).collect()
|
|
}
|
|
|
|
/// What became of a blast that was not reported.
|
|
pub fn note_settled(&mut self, how: Settled) {
|
|
match how {
|
|
Settled::Confirmed => self.stats.blasts_confirmed += 1,
|
|
Settled::Unchanged => self.stats.blasts_unchanged += 1,
|
|
Settled::Unloaded => self.stats.blasts_unloaded += 1,
|
|
}
|
|
}
|
|
|
|
fn note_column(&mut self, sx: i32, sz: i32) {
|
|
if self.changed_cols.len() >= 4096 {
|
|
self.changed_all = true;
|
|
} else {
|
|
self.changed_cols.insert((sx, sz));
|
|
}
|
|
}
|
|
|
|
/// Chunk columns (x, z in chunks) the game unloaded or replaced (#62): what is known of their sections is
|
|
/// void (`Unknown`) until they are read again. A new chunk packet replaces the blocks wholesale, which no
|
|
/// per-block note says.
|
|
pub fn columns_changed(&mut self, columns: &[(i32, i32)]) {
|
|
if columns.is_empty() {
|
|
return;
|
|
}
|
|
let set: HashSet<(i32, i32)> = columns.iter().copied().collect();
|
|
let mut n = 0u64;
|
|
for &(cx, cz) in columns {
|
|
self.note_column(cx, cz);
|
|
}
|
|
for (s, e) in self.secs.iter_mut() {
|
|
if set.contains(&(s.0, s.2)) {
|
|
e.fill = Fill::Unknown;
|
|
e.stale = true;
|
|
e.since = 0;
|
|
n += 1;
|
|
}
|
|
}
|
|
self.drop_rays(|b| set.contains(&(b[0] >> 4, b[2] >> 4)));
|
|
self.stats.invalidated += n;
|
|
self.stats.columns_changed += columns.len() as u64;
|
|
}
|
|
|
|
/// An explosion is about to change the land: the sections round it are read again.
|
|
pub fn touched(&mut self, at: DVec3, radius: f64) {
|
|
let lo = sec_of((at.x - radius).floor() as i32, (at.y - radius).floor() as i32, (at.z - radius).floor() as i32);
|
|
let hi = sec_of((at.x + radius).floor() as i32, (at.y + radius).floor() as i32, (at.z + radius).floor() as i32);
|
|
self.drop_rays(|b| {
|
|
let s = sec_of(b[0], b[1], b[2]);
|
|
(lo.0..=hi.0).contains(&s.0) && (lo.1..=hi.1).contains(&s.1) && (lo.2..=hi.2).contains(&s.2)
|
|
});
|
|
for x in lo.0..=hi.0 {
|
|
for y in lo.1..=hi.1 {
|
|
for z in lo.2..=hi.2 {
|
|
if let Some(e) = self.secs.get_mut(&(x, y, z)) {
|
|
e.stale = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn answer(&mut self, answers: Vec<SectionAnswer>, now: u64) {
|
|
for a in answers {
|
|
self.asked.remove(&a.sec);
|
|
self.stats.sections_answered += 1;
|
|
if a.fill == Fill::Unknown {
|
|
self.stats.sections_unknown += 1;
|
|
}
|
|
self.secs.insert(a.sec, Entry { fill: a.fill, stale: false, used: now, since: now });
|
|
}
|
|
if self.secs.len() > MAX_SECTIONS {
|
|
// Whatever wasn't used for a minute goes; if that's not enough, the older half.
|
|
self.secs.retain(|_, e| now.saturating_sub(e.used) < 1200);
|
|
if self.secs.len() > MAX_SECTIONS {
|
|
let mut used: Vec<u64> = self.secs.values().map(|e| e.used).collect();
|
|
used.sort_unstable();
|
|
let cut = used[used.len() / 2];
|
|
self.secs.retain(|_, e| e.used >= cut);
|
|
}
|
|
self.stats.evicted += 1;
|
|
}
|
|
// Forget questions the game never answered, so they can be asked again.
|
|
if now % 64 == 0 {
|
|
self.asked.retain(|_, t| now.saturating_sub(*t) < RETRY);
|
|
}
|
|
}
|
|
|
|
/// The sections to ask this tick (at most [`PER_TICK`]): the ones a sweep
|
|
/// needed first, then those along flights.
|
|
pub fn take_wanted(&mut self, now: u64) -> Vec<Sec> {
|
|
let mut out = Vec::new();
|
|
for list in [&mut self.urgent, &mut self.ahead] {
|
|
let room = PER_TICK - out.len();
|
|
let n = room.min(list.len());
|
|
out.extend(list.drain(..n));
|
|
}
|
|
for s in &out {
|
|
self.queued.remove(s);
|
|
self.asked.insert(*s, now);
|
|
}
|
|
self.stats.sections_asked += out.len() as u64;
|
|
out
|
|
}
|
|
|
|
fn want(&mut self, s: Sec, urgent: bool, now: u64) {
|
|
if self.queued.contains(&s) {
|
|
// Already waiting as a look-ahead, and a step needs it now: it moves up (#61).
|
|
if urgent && let Some(i) = self.ahead.iter().position(|a| *a == s) {
|
|
self.ahead.swap_remove(i);
|
|
self.urgent.push(s);
|
|
}
|
|
return;
|
|
}
|
|
if self.asked.get(&s).is_some_and(|&t| now.saturating_sub(t) < RETRY) {
|
|
return;
|
|
}
|
|
if self.urgent.len() + self.ahead.len() >= QUEUE_MAX {
|
|
// Full: an urgent need pushes a look-ahead out, a look-ahead waits.
|
|
if !urgent || self.ahead.is_empty() {
|
|
return;
|
|
}
|
|
let gone = self.ahead.swap_remove(0);
|
|
self.queued.remove(&gone);
|
|
}
|
|
if urgent {
|
|
self.urgent.push(s);
|
|
} else {
|
|
self.ahead.push(s);
|
|
}
|
|
self.queued.insert(s);
|
|
}
|
|
|
|
/// Asks for the sections along a flight from `pos` at `vel` a tick, `ticks` ahead.
|
|
pub fn look_ahead(&mut self, pos: DVec3, vel: DVec3, ticks: f64, now: u64) {
|
|
let mut d = vel * ticks;
|
|
let len = d.length();
|
|
if len < 1e-6 {
|
|
return;
|
|
}
|
|
if len > AHEAD_MAX {
|
|
d *= AHEAD_MAX / len;
|
|
}
|
|
// A section at a time along the segment (a section is 16 wide: half-section steps meet each).
|
|
let steps = (d.length() / 8.0).ceil().max(1.0) as i32;
|
|
let mut last = None;
|
|
for i in 0..=steps {
|
|
let p = pos + d * (i as f64 / steps as f64);
|
|
let s = sec_of(p.x.floor() as i32, p.y.floor() as i32, p.z.floor() as i32);
|
|
if last == Some(s) {
|
|
continue;
|
|
}
|
|
last = Some(s);
|
|
self.touch(s, false, now);
|
|
}
|
|
}
|
|
|
|
/// Touches a section the way a sweep or a look needs it: used now, and
|
|
/// asked again when stale or (after a while) unknown.
|
|
fn touch(&mut self, s: Sec, urgent: bool, now: u64) -> bool {
|
|
let Some(e) = self.secs.get_mut(&s) else {
|
|
self.want(s, urgent, now);
|
|
return false;
|
|
};
|
|
e.used = now;
|
|
// Whatever is in use is read again now and then, bounded: a change nobody told of ends up seen (#62).
|
|
let aged = e.fill != Fill::Unknown && now.saturating_sub(e.since) >= RECHECK;
|
|
if aged {
|
|
self.stats.aged += 1;
|
|
}
|
|
let refresh = e.stale || aged || (e.fill == Fill::Unknown && now.saturating_sub(e.since) >= RETRY);
|
|
if refresh {
|
|
self.want(s, urgent, now);
|
|
}
|
|
true
|
|
}
|
|
|
|
/// Whether the block at `(x, y, z)` stops a projectile: `None` where the
|
|
/// game hasn't said. `last` is the section just touched.
|
|
fn solid(&mut self, last: &mut Option<Sec>, x: i32, y: i32, z: i32, now: u64) -> Option<bool> {
|
|
let s = sec_of(x, y, z);
|
|
if *last != Some(s) {
|
|
*last = Some(s);
|
|
self.touch(s, !self.quiet, now);
|
|
}
|
|
self.unread_pending = false;
|
|
let Some(entry) = self.secs.get(&s) else {
|
|
self.unread_pending = true;
|
|
return None;
|
|
};
|
|
match &entry.fill {
|
|
Fill::Unknown => {
|
|
// Asked and answered unknown: no chunk there. Asked again (or not at all yet): waiting.
|
|
self.unread_pending = entry.stale && entry.since == 0;
|
|
None
|
|
}
|
|
Fill::Empty => Some(false),
|
|
Fill::Full => Some(true),
|
|
Fill::Mixed(bits) => {
|
|
let i = ((y & 15) << 8 | (z & 15) << 4 | (x & 15)) as usize;
|
|
Some(bits[i >> 3] >> (i & 7) & 1 != 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Whether the block is known to hold no solid block, with the sky above it
|
|
/// in view: used to tell a fluid surface from a roof.
|
|
pub fn is_known_open(&mut self, x: i32, y: i32, z: i32, now: u64) -> bool {
|
|
self.solid(&mut None, x, y, z, now) == Some(false)
|
|
}
|
|
|
|
/// Whether the block is known to be solid.
|
|
pub fn is_known_solid(&mut self, x: i32, y: i32, z: i32, now: u64) -> bool {
|
|
self.solid(&mut None, x, y, z, now) == Some(true)
|
|
}
|
|
|
|
/// A sweep made to plan a flight, not to move one: the sections it needs are asked after the urgent ones,
|
|
/// and a start inside a block is not a hit.
|
|
pub fn probe(&mut self, a: DVec3, b: DVec3, now: u64) -> Sweep {
|
|
self.quiet = true;
|
|
let r = self.sweep(a, b, true, now);
|
|
self.quiet = false;
|
|
r
|
|
}
|
|
|
|
/// A step of flight `key` from `a` to `b` that met a solid block (`hit`): the occupancy says "stops
|
|
/// something", the shape may leave room (slabs, stairs, fences, panes). The block's exact shapes are asked
|
|
/// of the game (a ray over the step) and the flight holds at the face meanwhile; a `Hit` comes back as the
|
|
/// exact contact, a `Clear` lets this flight through that block. An answer that doesn't come in
|
|
/// [`RAY_WAIT`] ticks, or `unknown`, leaves the hit as the solid cube.
|
|
pub fn narrow(&mut self, key: u32, hit: Sweep, a: DVec3, b: DVec3, now: u64) -> Sweep {
|
|
let Sweep::Hit { t, block, .. } = hit else { return hit };
|
|
let k = (key, block);
|
|
let mut first = now;
|
|
if let Some(e) = self.rays.entries.get_mut(&k) {
|
|
if e.cube {
|
|
return hit;
|
|
}
|
|
// An answer is for the segment it was asked of: it serves this step only if the step lies on it.
|
|
if let Some((sa, sb)) = within(e.a, e.b, a, b) {
|
|
match e.state {
|
|
Exact::Pending => {
|
|
if now.saturating_sub(e.first) > RAY_WAIT {
|
|
e.cube = true;
|
|
return hit;
|
|
}
|
|
return Sweep::Wait { t };
|
|
}
|
|
Exact::Clear => return Sweep::Clear,
|
|
Exact::Hit { point, normal, block } => {
|
|
let d0 = e.b - e.a;
|
|
let f_old = ((point - e.a).dot(d0) / d0.length_squared().max(1e-12)).clamp(0.0, 1.0);
|
|
let f = (f_old - sa) / (sb - sa).max(1e-12);
|
|
if f > 1.0 {
|
|
// The shape's face is beyond this step: nothing is met yet.
|
|
return Sweep::Clear;
|
|
}
|
|
let f = f.max(0.0);
|
|
return Sweep::Hit { t: f, point: a + (b - a) * f, normal, block };
|
|
}
|
|
}
|
|
}
|
|
// A different step: the old answer says nothing of it. Ask again; an unanswered wait keeps its clock.
|
|
if e.state == Exact::Pending {
|
|
first = e.first;
|
|
if now.saturating_sub(first) > RAY_WAIT {
|
|
e.cube = true;
|
|
return hit;
|
|
}
|
|
}
|
|
let old = e.id;
|
|
self.rays.by_id.remove(&old);
|
|
self.rays.out.retain(|r| r.0 != old);
|
|
} else if self.rays.entries.len() >= 4096 {
|
|
return hit;
|
|
}
|
|
self.rays.next = self.rays.next.wrapping_add(1);
|
|
let id = self.rays.next;
|
|
// Asked over a longer stretch than the step: a flight held at the face tries again from there with a
|
|
// step of about the same line, which the answer then covers. What lies beyond the step is mapped back
|
|
// when it is used (a face past the step is no contact).
|
|
let far = a + (b - a) * RAY_REACH;
|
|
self.rays.entries.insert(k, RayEntry { id, a, b: far, state: Exact::Pending, asked: now, first, cube: false });
|
|
self.rays.by_id.insert(id, k);
|
|
self.rays.out.push((id, a, far));
|
|
Sweep::Wait { t }
|
|
}
|
|
|
|
/// Forgets the exact answers (and questions) for blocks where `gone` says the geometry may have changed:
|
|
/// a late reply to a forgotten question is dropped, the next step asks anew (#73).
|
|
fn drop_rays(&mut self, gone: impl Fn(&[i32; 3]) -> bool) {
|
|
let dead: Vec<(u32, [i32; 3])> = self.rays.entries.keys().filter(|k| gone(&k.1)).copied().collect();
|
|
for k in dead {
|
|
if let Some(e) = self.rays.entries.remove(&k) {
|
|
self.rays.by_id.remove(&e.id);
|
|
self.rays.out.retain(|r| r.0 != e.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The exact rays to ask this tick: id, start, end.
|
|
pub fn take_rays(&mut self, now: u64) -> Vec<(u32, DVec3, DVec3)> {
|
|
let n = RAYS_PER_TICK.min(self.rays.out.len());
|
|
let out: Vec<_> = self.rays.out.drain(..n).collect();
|
|
if now % 128 == 0 {
|
|
let dead: Vec<_> = self.rays.entries.iter().filter(|(_, e)| now.saturating_sub(e.asked) > 600).map(|(k, _)| *k).collect();
|
|
for k in dead {
|
|
if let Some(e) = self.rays.entries.remove(&k) {
|
|
self.rays.by_id.remove(&e.id);
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// The game's answers to the exact rays.
|
|
pub fn answer_rays(&mut self, replies: &[RayReply]) {
|
|
for r in replies {
|
|
let Some(k) = self.rays.by_id.get(&r.id).copied() else { continue };
|
|
let Some(e) = self.rays.entries.get_mut(&k) else { continue };
|
|
if e.cube {
|
|
continue;
|
|
}
|
|
e.state = if r.clear {
|
|
Exact::Clear
|
|
} else if r.hit {
|
|
Exact::Hit { point: e.a + (e.b - e.a) * r.fraction, normal: r.normal, block: r.block }
|
|
} else {
|
|
// No verdict: the solid cube the sections said.
|
|
e.cube = true;
|
|
Exact::Hit { point: e.a, normal: [0; 3], block: k.1 }
|
|
};
|
|
}
|
|
}
|
|
|
|
/// The first solid block along `a` to `b`, by walking the blocks it
|
|
/// crosses; `skip_start` ignores the block it starts in (a launch from a
|
|
/// pad that is part of the land).
|
|
pub fn sweep(&mut self, a: DVec3, b: DVec3, skip_start: bool, now: u64) -> Sweep {
|
|
self.stats.sweeps += 1;
|
|
let d = b - a;
|
|
let mut pos = [a.x.floor() as i32, a.y.floor() as i32, a.z.floor() as i32];
|
|
let dv = [d.x, d.y, d.z];
|
|
let av = [a.x, a.y, a.z];
|
|
let step: [i32; 3] = dv.map(|v| if v > 0.0 { 1 } else { -1 });
|
|
let mut t_max = [f64::INFINITY; 3];
|
|
let mut t_delta = [f64::INFINITY; 3];
|
|
for i in 0..3 {
|
|
if dv[i] != 0.0 {
|
|
let edge = f64::from(pos[i]) + if dv[i] > 0.0 { 1.0 } else { 0.0 };
|
|
t_max[i] = (edge - av[i]) / dv[i];
|
|
t_delta[i] = (1.0 / dv[i]).abs();
|
|
}
|
|
}
|
|
let mut last: Option<Sec> = None;
|
|
let mut face = [0i32; 3];
|
|
let mut t = 0.0f64;
|
|
let mut first = true;
|
|
for _ in 0..4096 {
|
|
self.stats.cells += 1;
|
|
match self.solid(&mut last, pos[0], pos[1], pos[2], now) {
|
|
None => {
|
|
self.stats.unknown_sweeps += 1;
|
|
return Sweep::Unknown { t, pending: self.unread_pending };
|
|
}
|
|
Some(true) if !(first && skip_start) => {
|
|
self.stats.hits += 1;
|
|
return Sweep::Hit { t, point: a + d * t, normal: face, block: pos };
|
|
}
|
|
_ => {}
|
|
}
|
|
first = false;
|
|
let axis = if t_max[0] < t_max[1] {
|
|
if t_max[0] < t_max[2] { 0 } else { 2 }
|
|
} else if t_max[1] < t_max[2] {
|
|
1
|
|
} else {
|
|
2
|
|
};
|
|
t = t_max[axis];
|
|
if t > 1.0 {
|
|
return Sweep::Clear;
|
|
}
|
|
t_max[axis] += t_delta[axis];
|
|
pos[axis] += step[axis];
|
|
face = [0; 3];
|
|
face[axis] = -step[axis];
|
|
}
|
|
self.stats.unknown_sweeps += 1;
|
|
Sweep::Unknown { t, pending: false }
|
|
}
|
|
}
|
|
|
|
/// Where the game's blocks come from in tests and the native runner: any
|
|
/// world that can say what a block is.
|
|
pub trait WorldSource {
|
|
/// Whether the block stops a projectile; `None` where there is no chunk.
|
|
fn solid(&self, x: i32, y: i32, z: i32) -> Option<bool>;
|
|
/// The collision boxes of the block (`[x0, y0, z0, x1, y1, z1]` inside the block): a full cube where
|
|
/// it is solid, unless the world has finer shapes.
|
|
fn boxes(&self, x: i32, y: i32, z: i32) -> Vec<[f64; 6]> {
|
|
if self.solid(x, y, z) == Some(true) { vec![[0.0, 0.0, 0.0, 1.0, 1.0, 1.0]] } else { Vec::new() }
|
|
}
|
|
}
|
|
|
|
/// The first entry of segment `a` to `b` into the box (slab test): the fraction and the axis.
|
|
fn enter_box(a: DVec3, d: DVec3, lo: DVec3, hi: DVec3) -> Option<(f64, usize)> {
|
|
let (mut t0, mut t1, mut axis) = (0.0f64, 1.0f64, usize::MAX);
|
|
for i in 0..3 {
|
|
let (o, v) = (a[i], d[i]);
|
|
if v.abs() < 1e-12 {
|
|
if o < lo[i] || o > hi[i] {
|
|
return None;
|
|
}
|
|
} else {
|
|
let (mut n, mut f) = ((lo[i] - o) / v, (hi[i] - o) / v);
|
|
if n > f {
|
|
std::mem::swap(&mut n, &mut f);
|
|
}
|
|
if n > t0 {
|
|
t0 = n;
|
|
axis = i;
|
|
}
|
|
t1 = t1.min(f);
|
|
if t0 > t1 {
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
Some((t0, axis))
|
|
}
|
|
|
|
/// A [`WorldSource`] as the game would answer `render.query-sections` with it.
|
|
pub fn section_of(source: &dyn WorldSource, s: Sec) -> Fill {
|
|
let mut bits = Box::new([0u8; 512]);
|
|
let (mut set, mut known) = (0u32, 0u32);
|
|
for i in 0..4096usize {
|
|
let (x, y, z) = (s.0 * 16 + (i & 15) as i32, s.1 * 16 + (i >> 8) as i32, s.2 * 16 + ((i >> 4) & 15) as i32);
|
|
match source.solid(x, y, z) {
|
|
Some(true) => {
|
|
bits[i >> 3] |= 1 << (i & 7);
|
|
set += 1;
|
|
known += 1;
|
|
}
|
|
Some(false) => known += 1,
|
|
None => {}
|
|
}
|
|
}
|
|
match (known, set) {
|
|
(0, _) => Fill::Unknown,
|
|
(_, 0) if known == 4096 => Fill::Empty,
|
|
(_, 4096) => Fill::Full,
|
|
_ => Fill::Mixed(bits),
|
|
}
|
|
}
|
|
|
|
impl WorldCache {
|
|
/// Read-only occupancy at the instant of a blast. Some(true) is known cover,
|
|
/// Some(false) a fully known open segment, None unresolved geometry. No query,
|
|
/// chunk load or later reply can change damage already committed for this blast.
|
|
/// This deliberately treats partial collision blocks as occupied cubes.
|
|
pub fn blast_cover(&self, a: DVec3, b: DVec3, now: u64) -> Option<bool> {
|
|
let d = b - a;
|
|
let mut p = [a.x.floor() as i32, a.y.floor() as i32, a.z.floor() as i32];
|
|
let av = [a.x, a.y, a.z];
|
|
let dv = [d.x, d.y, d.z];
|
|
let step = dv.map(|v| if v > 0.0 { 1 } else { -1 });
|
|
let mut next = [f64::INFINITY; 3];
|
|
let mut delta = [f64::INFINITY; 3];
|
|
for i in 0..3 {
|
|
if dv[i] != 0.0 {
|
|
let edge = f64::from(p[i]) + if dv[i] > 0.0 { 1.0 } else { 0.0 };
|
|
next[i] = (edge - av[i]) / dv[i];
|
|
delta[i] = (1.0 / dv[i]).abs();
|
|
}
|
|
}
|
|
let mut unknown = false;
|
|
for n in 0..256 {
|
|
let fill = self.secs.get(&sec_of(p[0], p[1], p[2]))
|
|
.filter(|e| !e.stale && now.saturating_sub(e.since) < RECHECK)
|
|
.map(|e| &e.fill);
|
|
let solid = match fill {
|
|
Some(Fill::Empty) => Some(false),
|
|
Some(Fill::Full) => Some(true),
|
|
Some(Fill::Mixed(bits)) => {
|
|
let i = ((p[1] & 15) << 8 | (p[2] & 15) << 4 | (p[0] & 15)) as usize;
|
|
Some(bits[i >> 3] >> (i & 7) & 1 != 0)
|
|
}
|
|
_ => None,
|
|
};
|
|
match solid {
|
|
// Ignore the struck/origin voxel, not the next wall behind it.
|
|
Some(true) if n > 0 => return Some(true),
|
|
None => unknown = true,
|
|
_ => {}
|
|
}
|
|
let axis = if next[0] < next[1] {
|
|
if next[0] < next[2] { 0 } else { 2 }
|
|
} else if next[1] < next[2] { 1 } else { 2 };
|
|
if next[axis] > 1.0 { return if unknown { None } else { Some(false) }; }
|
|
next[axis] += delta[axis];
|
|
p[axis] += step[axis];
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Answers the exact rays of the last tick from a synthetic world, as the game does one tick later.
|
|
pub fn serve_rays(&mut self, source: &dyn WorldSource, rays: Vec<(u32, DVec3, DVec3)>) {
|
|
let replies: Vec<RayReply> = rays
|
|
.into_iter()
|
|
.map(|(id, a, b)| {
|
|
let d = b - a;
|
|
let mut best: Option<(f64, usize, [i32; 3])> = None;
|
|
let (lo, hi) = (a.min(b).floor(), a.max(b).floor());
|
|
for x in lo.x as i32..=hi.x as i32 {
|
|
for y in lo.y as i32..=hi.y as i32 {
|
|
for z in lo.z as i32..=hi.z as i32 {
|
|
for bx in source.boxes(x, y, z) {
|
|
let o = DVec3::new(f64::from(x), f64::from(y), f64::from(z));
|
|
if let Some((t, axis)) = enter_box(a, d, o + DVec3::new(bx[0], bx[1], bx[2]), o + DVec3::new(bx[3], bx[4], bx[5]))
|
|
&& best.is_none_or(|b| t < b.0)
|
|
{
|
|
best = Some((t, axis, [x, y, z]));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
match best {
|
|
None => RayReply { id, clear: true, hit: false, fraction: 0.0, normal: [0; 3], block: [0; 3] },
|
|
Some((t, axis, block)) => {
|
|
let mut normal = [0; 3];
|
|
if axis < 3 {
|
|
normal[axis] = if d[axis] > 0.0 { -1 } else { 1 };
|
|
}
|
|
RayReply { id, clear: false, hit: true, fraction: t, normal, block }
|
|
}
|
|
}
|
|
})
|
|
.collect();
|
|
self.answer_rays(&replies);
|
|
}
|
|
|
|
/// Answers the questions of the last tick from a synthetic world, as the game does one tick later.
|
|
pub fn serve(&mut self, source: &dyn WorldSource, wanted: Vec<Sec>, now: u64) {
|
|
let answers = wanted.into_iter().map(|sec| SectionAnswer { sec, fill: section_of(source, sec) }).collect();
|
|
self.answer(answers, now);
|
|
let rays = self.take_rays(now);
|
|
self.serve_rays(source, rays);
|
|
}
|
|
}
|