Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions native/src/sql/sim/missile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,27 @@ pub fn spawn(
)
}

/// [`spawn`] over an ask list, folded rather than mapped, as an array of
/// [`thrown`] tuples.
///
/// A map runs every function in its body once even on an empty list, and
/// this body is `P_SpawnMissile` and the move test under it. A fold runs
/// its body only where the list has an element, so a tic that throws
/// nothing pays for the fold and nothing under it.
pub fn spawn_fold(
asks: &str,
world: &Throwing<'_>,
spawning: &mobj::Spawning<'_>,
map: &World<'_>,
) -> String {
let (values, body) = thrown(world, spawning, map);
format!(
"arrayFold((ms_held, ms_ask) -> arrayPushBack(ms_held, {}), {asks}, \
CAST([] AS Array({THROWN_TYPE})))",
bind::chain_in("msa", &values, &body)
)
}

/// What one missile works out, as the values a body reads and the
/// [`thrown`] tuple it answers with.
fn thrown(
Expand Down
79 changes: 69 additions & 10 deletions native/src/sql/sim/mobj.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! What a thing does with its momentum and its states, from `p_mobj.c`.

use super::map::{self, World, answer};
use super::{State, attacks, enemy, inter, maputl, sight};
use super::{State, attacks, enemy, inter, maputl, missile, sight};
use crate::sql::Statement;
use crate::sql::bind;
use crate::sql::fixed;
Expand Down Expand Up @@ -102,6 +102,7 @@ pub fn constants(db: &str) -> Vec<(String, String)> {
super::table_column(db, "mobjinfo", "flags"),
),
("mt_puff".to_owned(), thing_type(db, "MT_PUFF")),
("mt_troopshot".to_owned(), thing_type(db, "MT_TROOPSHOT")),
("mt_blood".to_owned(), thing_type(db, "MT_BLOOD")),
// The engine names the frames a puff and a blood spot are put into
// by hand. No table holds them, so they are read off the spawn
Expand Down Expand Up @@ -623,7 +624,7 @@ pub fn thinkers(state: &State) -> Vec<(String, String)> {
}
// The attack reads what the state cycle and the chase left, and what
// it leaves stands over them.
for (name, expr) in strikes(state) {
for (name, expr) in strikes(state, &world) {
bind(&name, expr);
}
bind(
Expand Down Expand Up @@ -678,7 +679,8 @@ pub fn thinkers(state: &State) -> Vec<(String, String)> {
/// pointer at the thing that was taken becomes 0, which is what the
/// contract says none means.
///
/// `next_seq` counts what the level has ever spawned and does not move.
/// A fireball the tic threw goes on the end of the list, behind what the
/// compaction left, and takes one of each counter.
fn removed(state: &State, player: &str) -> Vec<(String, String)> {
let s = |column: &str| state.get(column);
let mut bindings: Vec<(String, String)> = Vec::new();
Expand Down Expand Up @@ -726,9 +728,25 @@ fn removed(state: &State, player: &str) -> Vec<(String, String)> {
} else {
held
};
// `P_AddThinker` puts a new thing on the end of the list, so a
// fireball the tic threw goes behind what survived the compaction.
let born = match missile::born_column(column, "t") {
Some(value) => {
let value = if POINTERS.contains(&column) {
moved_slot(&value)
} else {
value
};
format!("arrayMap(t -> {value}, mt_thrown)")
}
None => format!(
"arrayMap((t, i) -> toUInt32({} + i - 1), mt_thrown, arrayEnumerate(mt_thrown))",
s("next_linkseq")
),
};
bind(
&format!("now_{column}"),
format!("arrayFilter((v, a) -> a = 1, {held}, mt_kept)"),
format!("arrayConcat(arrayFilter((v, a) -> a = 1, {held}, mt_kept), {born})"),
);
}
bind(
Expand All @@ -745,6 +763,13 @@ fn removed(state: &State, player: &str) -> Vec<(String, String)> {
);
bind("now_p_attacker", moved_slot(&s("p_attacker")));
bind("now_p_mo", format!("toUInt32(mt_slot[{player}])"));
// Every thing the tic threw took one of each counter.
for column in ["next_seq", "next_linkseq"] {
bind(
&format!("now_{column}"),
format!("toUInt32({} + length(mt_thrown))", s(column)),
);
}
bindings
}

Expand Down Expand Up @@ -809,7 +834,7 @@ const POINTERS: [&str; 2] = ["m_target", "m_tracer"];
/// reaching a routine, because the second would draw from an index the
/// first moves; the fireball, which wants a missile spawned; and a claw
/// that kills, which owes the kill count and whatever the corpse drops.
fn strikes(state: &State) -> Vec<(String, String)> {
fn strikes(state: &State, map: &World<'_>) -> Vec<(String, String)> {
let s = |column: &str| state.get(column);
let mut bindings: Vec<(String, String)> = Vec::new();
let mut bind = |name: &str, expr: String| bindings.push((name.to_owned(), expr));
Expand Down Expand Up @@ -891,6 +916,38 @@ fn strikes(state: &State) -> Vec<(String, String)> {
};
bind("mt_hurt", inter::damage_fold("mt_hurt_asks", &hurting));

// `P_SpawnMissile` for an imp whose claw did not reach. The fireball
// is the only missile a routine throws here, so its type is the one
// constant.
bind(
"mt_throw_asks",
format!(
"arraySlice([(at_one, toUInt32(mk_m_target[greatest(at_one, 1)]), \
mt_troopshot, at_base + toUInt32(at_struck.{}))], 1, at_struck.{})",
attacks::attacked::DRAWS,
attacks::attacked::THROWS,
),
);
let throwing = missile::Throwing {
m_x: "mk_m_x",
m_y: "mk_m_y",
m_z: "mk_m_z",
m_radius: &s("m_radius"),
m_height: &s("m_height"),
m_flags: "cq_m_flags",
prndindex: &s("prndindex"),
};
let spawning = Spawning {
floorheight: &s("sec_floorheight"),
ceilingheight: &s("sec_ceilingheight"),
prndindex: &s("prndindex"),
skill: "skill",
};
bind(
"mt_thrown",
missile::spawn_fold("mt_throw_asks", &throwing, &spawning, map),
);

bind(
"at_clawed",
format!("toUInt8(at_struck.{})", attacks::attacked::CLAWED),
Expand All @@ -902,21 +959,23 @@ fn strikes(state: &State) -> Vec<(String, String)> {
bind(
"at_draws",
format!(
"toUInt32(toUInt32(at_struck.{}) + toUInt32(mt_hurt.{}))",
"toUInt32(toUInt32(at_struck.{}) + toUInt32(mt_hurt.{}) \
+ arraySum(arrayMap(t -> toUInt32(t.{}), mt_thrown)))",
attacks::attacked::DRAWS,
inter::hurt::DRAWS,
missile::thrown::DRAWS,
),
);
bind(
"at_unrun",
format!(
"toUInt8(length(mt_attackers) > 1 \
OR at_struck.{throws} = 1 OR at_struck.{stuck} = 1 \
OR mt_hurt.{counted} = 1 OR mt_hurt.{drop} != -1)",
throws = attacks::attacked::THROWS,
"toUInt8(length(mt_attackers) > 1 OR at_struck.{stuck} = 1 \
OR mt_hurt.{counted} = 1 OR mt_hurt.{drop} != -1 \
OR arrayExists(t -> t.{thrown} = 1, mt_thrown))",
stuck = attacks::attacked::STUCK,
counted = inter::hurt::COUNTED,
drop = inter::hurt::DROP,
thrown = missile::thrown::STUCK,
),
);
bindings
Expand Down
2 changes: 1 addition & 1 deletion native/src/sql/sim/tick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ mod tests {
#[test]
fn each_caller_of_the_move_test_holds_one() {
let sql = resident_statement("nat");
assert_eq!(sql.matches("arrayMap(mv ->").count(), 4);
assert_eq!(sql.matches("arrayMap(mv ->").count(), 5);
assert_eq!(sql.matches("arrayMap(clip ->").count(), 1);
assert_eq!(sql.matches("arrayFold((move_at, move_step)").count(), 1);
assert_eq!(sql.matches("arrayFold((cw_at, cw_step)").count(), 1);
Expand Down
90 changes: 81 additions & 9 deletions native/tests/sim_parity_live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,36 @@ const ATTACK: [(u32, i32, i32, u32); 5] = [
/// past `MELEERANGE`, so the routine throws a fireball.
const FIREBALL: u32 = 169;

/// The last thing on the list at [`FIREBALL`], read out of the reference
/// emulator's demo3 trace: `m_type`, `m_x`, `m_y`, `m_z`, `m_momx`,
/// `m_momy`, `m_momz`, `m_angle`, `m_target`, `m_state`, `m_tics` and
/// `m_flags`. `MT_TROOPSHOT` is thing type 31 and the imp that threw it is
/// slot 119.
///
/// `P_AddThinker` puts a new thing on the end of the list and
/// `P_RunThinkers` walks that list to its end, so the engine runs the
/// fireball's own thinker on the tic it was thrown. The `x`, `y` and
/// `tics` here are what that thinker left. What `P_SpawnMissile` and
/// `P_CheckMissileSpawn` leave is one momentum step behind it, with one
/// tic more on the clock.
const THROWN: [i64; 12] = [
31,
21_353_709,
-8_314_002,
2_097_152,
-285_410,
589_940,
0,
1_381_806_976,
119,
97,
1,
67_088,
];

/// How many things stand on the list before the imp throws.
const ON_THE_LIST: u64 = 264;

const THRUST: [(u32, usize, i32, i32, i32, i32); 4] = [
(142, 118, 8272000, -4310912, 0, 0),
(142, 258, 13992912, 4297488, 0, 0),
Expand Down Expand Up @@ -345,6 +375,8 @@ struct Walked {
state119: i32,
flags119: i32,
angle119: u32,
things: u64,
shot: Vec<i64>,
}

async fn walked(fixture: &Fixture, db: &str) -> Vec<Walked> {
Expand Down Expand Up @@ -375,7 +407,12 @@ async fn walked(fixture: &Fixture, db: &str) -> Vec<Walked> {
arrayMap(k -> m_momx[k], [118, 258]) AS thrust_momx, \
arrayMap(k -> m_momy[k], [118, 258]) AS thrust_momy, \
p_extralight AS extralight, m_state[119] AS state119, \
m_flags[119] AS flags119, m_angle[119] AS angle119 \
m_flags[119] AS flags119, m_angle[119] AS angle119, \
toUInt64(length(m_x)) AS things, \
arrayMap(a -> toInt64(a[length(a)]), \
[m_type, m_x, m_y, m_z, m_momx, m_momy, m_momz, \
arrayMap(v -> toInt32(v), m_angle), arrayMap(v -> toInt32(v), m_target), \
m_state, m_tics, m_flags]) AS shot \
FROM {db}.native_state ORDER BY tic"
))
.await
Expand Down Expand Up @@ -579,17 +616,52 @@ async fn the_tic_matches_the_engine_where_the_fixture_reaches() {
"the imp winding up its attack at gametic {tic}"
);
}
// The routine turns the imp and then wants a missile, which is the
// branch this tic does not spawn.
// The fireball the routine throws, on the end of the list.
assert_eq!(
at(FIREBALL).unresolved,
1,
"the tic the imp throws its fireball says it could not be produced"
at(FIREBALL - 1).things,
ON_THE_LIST,
"the list before the throw is the level's own"
);
assert_eq!(
at(FIREBALL - 1).unresolved,
0,
"and the tic before it is one the run carried through"
at(FIREBALL).things,
ON_THE_LIST + 1,
"and the throw puts one more thing on it"
);
let shot = &at(FIREBALL).shot;
let (x, y, momx, momy) = (shot[1], shot[2], shot[4], shot[5]);
// Everything the spawn itself decides.
for (at, name) in [
(0, "type"),
(3, "z"),
(4, "momx"),
(5, "momy"),
(6, "momz"),
(7, "angle"),
(8, "target"),
(9, "state"),
(11, "flags"),
] {
assert_eq!(shot[at], THROWN[at], "the fireball's {name}");
}
// The engine runs the fireball's own thinker on the tic it was thrown
// and this does not, so the point is one momentum step short of the
// probe's and the wait is one tic longer.
assert_eq!(
(x + momx, y + momy),
(THROWN[1], THROWN[2]),
"the fireball stands one step behind where the engine left it"
);
assert_eq!(
shot[10],
THROWN[10] + 1,
"and one tic behind on its own clock"
);
// Every number the throw draws is counted, which is what moved the
// first divergence off `prndindex`.
assert_eq!(
at(FIREBALL).prndindex,
194,
"the tic draws what the engine draws"
);
for (tic, slot, x, y, momx, momy) in THRUST {
let row = at(tic);
Expand Down
33 changes: 27 additions & 6 deletions native/tests/sim_troop_live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ const ATTACK: i32 = 454;
/// `p_local.h`
const BASETHRESHOLD: i32 = 100;

/// `mobjtype.tsv`: the fireball an imp throws.
const TROOPSHOT: i32 = 31;

/// `p_mobj.h`
const MF_AMBUSH: i64 = 32;

Expand Down Expand Up @@ -88,6 +91,9 @@ struct Clawed {
threshold: i32,
prndindex: u8,
unresolved: u8,
things: u64,
last_type: i32,
last_target: u32,
}

/// A column of one slot replaced, leaving every other slot alone.
Expand Down Expand Up @@ -168,7 +174,9 @@ async fn a_tic_carries_the_imps_attack_through() {
"SELECT tic, m_state[{ATTACKER}] AS state, m_angle[{ATTACKER}] AS angle, \
m_flags[{ATTACKER}] AS attacker_flags, m_health[{TARGET}] AS health, \
m_target[{TARGET}] AS hunts, m_threshold[{TARGET}] AS threshold, \
prndindex, unresolved \
prndindex, unresolved, toUInt64(length(m_x)) AS things, \
m_type[length(m_type)] AS last_type, \
m_target[length(m_target)] AS last_target \
FROM {db}.native_state WHERE tic IN ({}) ORDER BY tic",
wanted.join(", ")
))
Expand Down Expand Up @@ -218,17 +226,30 @@ async fn a_tic_carries_the_imps_attack_through() {
"and holds that for the threshold's worth of tics"
);

// The imp out of reach throws a fireball, which this tic does not
// spawn.
// The imp out of reach throws a fireball, which goes on the end of the
// list.
let (before, after) = (at(300), at(301));
assert_eq!(after.state, ATTACK, "the cycle reaches the routine");
assert_eq!(
after.health, before.health,
"nothing reaches a target four hundred units away"
"no claw reaches a target four hundred units away"
);
assert_eq!(
after.things,
before.things + 1,
"the throw puts one more thing on the list"
);
assert_eq!(
after.last_type, TROOPSHOT,
"and the thing it puts there is a fireball"
);
assert_eq!(
after.unresolved, 1,
"and the fireball says the tic could not be produced"
after.last_target, ATTACKER as u32,
"carrying a pointer back at the imp that threw it"
);
assert_ne!(
after.prndindex, before.prndindex,
"and the spawn draws for itself"
);

// `A_FaceTarget` takes the thing off ambush, and the tic carries that
Expand Down