urt-rs / crates /urt-gameplay /src /client_timer.rs
nmarshall's picture
gameplay: port caller timing preparation policy
74af72a
Raw History Blame Contribute Delete
17.3 kB
//! Recovered client timer contracts; native damage and frame scheduling stay explicit.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TimerTerminalState {
pub entity_flag: u32,
pub client_flags: u32,
pub elapsed_total: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TimerTerminalInput {
pub stamp: u32,
pub cooldown: u32,
pub tick: u32,
pub deadline: u32,
pub msec: u32,
}
/// Original150791 pre-loop fragment: signed comparison after word arithmetic.
pub fn timer_entity_deadline(state: &mut TimerTerminalState, input: TimerTerminalInput) {
let limit = input.stamp.wrapping_add(input.cooldown.wrapping_mul(1000));
if (limit as i32) < input.tick as i32 {
state.entity_flag = 0;
}
}
/// Original151085 terminal fragment: preserve unrelated flag bits and timestamps.
pub fn timer_terminal_tail(state: &mut TimerTerminalState, input: TimerTerminalInput) {
if state.client_flags & 2 != 0 && (input.tick as i32) > input.deadline as i32 {
state.client_flags &= !2;
}
state.elapsed_total = state.elapsed_total.wrapping_add(input.msec);
}
#[cfg(test)]
mod terminal_tests {
use super::*;
#[test]
fn strict_equalities_preserve_flags_and_accumulation_wraps() {
let mut s = TimerTerminalState {
entity_flag: 7,
client_flags: 7,
elapsed_total: u32::MAX,
};
let i = TimerTerminalInput {
stamp: 8000,
cooldown: 2,
tick: 10000,
deadline: 10000,
msec: 2,
};
timer_entity_deadline(&mut s, i);
timer_terminal_tail(&mut s, i);
assert_eq!(
s,
TimerTerminalState {
entity_flag: 7,
client_flags: 7,
elapsed_total: 1
}
);
let i = TimerTerminalInput { tick: 10001, ..i };
timer_entity_deadline(&mut s, i);
timer_terminal_tail(&mut s, i);
assert_eq!(s.entity_flag, 0);
assert_eq!(s.client_flags, 5);
}
#[test]
fn cooldown_wrap_precedes_signed_compare() {
let mut s = TimerTerminalState {
entity_flag: 7,
client_flags: 0,
elapsed_total: 0,
};
let i = TimerTerminalInput {
stamp: 7999,
cooldown: i32::MAX as u32,
tick: 10000,
deadline: 0,
msec: 0,
};
timer_entity_deadline(&mut s, i);
assert_eq!(s.entity_flag, 0);
s.entity_flag = 7;
timer_entity_deadline(
&mut s,
TimerTerminalInput {
tick: i32::MIN as u32,
..i
},
);
assert_eq!(s.entity_flag, 7);
}
}
use crate::native_policy::Error;
pub const TIMER_DIVISORS: [i32; 6] = [1120403456, 9, 4, 3, 2, 1];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TimerLoopState {
pub flags: u32,
pub status: u32,
pub rate: u32,
pub phase: u32,
pub timer: u32,
pub residual: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TimerLoopInput {
pub health: u32,
pub weapon: u32,
pub target: u32,
pub owner: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TimerDamage {
pub target: u32,
pub inflictor: u32,
pub attacker: u32,
pub direction: u32,
pub point: u32,
pub amount: u32,
pub flags: u32,
pub means: u32,
pub weapon: u32,
}
/// Original150810 complete finite-domain loop. Damage effects are requests;
/// supplied health is deliberately unchanged. Rate0 aliases the preceding word.
pub fn timer_tick_loop(
state: &mut TimerLoopState,
input: TimerLoopInput,
) -> Result<Vec<TimerDamage>, Error> {
if state.rate > 5 {
return Err(Error::InvalidIndex);
}
if state.residual as i32 > 10000 {
return Err(Error::InvalidInput);
}
if state.residual as i32 >= 100
&& state.flags & 512 != 0
&& !f32::from_bits(state.timer).is_finite()
{
return Err(Error::NonFinite);
}
let mut next = *state;
let mut damage = vec![];
while next.residual as i32 >= 100 {
next.phase =
((next.phase.wrapping_add(1) as i32) % TIMER_DIVISORS[next.rate as usize]) as u32;
next.residual = next.residual.wrapping_sub(100);
if next.flags & 256 != 0 && (input.health as i32) > 0 && next.phase == 0 {
damage.push(TimerDamage {
target: input.target,
inflictor: input.owner,
attacker: input.owner,
direction: 0,
point: 0,
amount: 1,
flags: 0,
means: 23,
weapon: input.weapon,
});
}
if next.flags & 512 != 0 {
let mut timer = f32::from_bits(next.timer);
if next.flags & 256 != 0 && timer <= 0.0 {
next.flags &= !256;
next.rate = 0;
timer = 0.0;
}
if timer > 0.0 {
timer -= 0.1f32;
if timer <= 0.0 {
next.flags &= !256;
next.rate = 0;
timer = 0.0;
}
} else if next.flags & 128 != 0 {
next.flags &= !128;
next.status &= !1;
}
next.timer = timer.to_bits();
}
}
*state = next;
Ok(damage)
}
#[cfg(test)]
mod tick_tests {
use super::*;
fn input() -> TimerLoopInput {
TimerLoopInput {
health: 1,
weapon: 7,
target: 2,
owner: 3,
}
}
#[test]
fn phase_wrap_signed_remainder_alias_and_request_arguments() {
let mut s = TimerLoopState {
flags: 256,
status: 3,
rate: 0,
phase: 0,
timer: 0,
residual: 250,
};
assert!(timer_tick_loop(&mut s, input()).unwrap().is_empty());
assert_eq!(s.phase, 2);
assert_eq!(s.residual, 50);
s.rate = 5;
s.residual = 100;
s.phase = i32::MAX as u32;
let p = timer_tick_loop(&mut s, input()).unwrap();
assert_eq!(s.phase, 0);
assert_eq!(
p,
[TimerDamage {
target: 2,
inflictor: 3,
attacker: 3,
direction: 0,
point: 0,
amount: 1,
flags: 0,
means: 23,
weapon: 7
}]
);
}
#[test]
fn negative_residual_skips_and_unsupported_domains_are_atomic() {
let mut s = TimerLoopState {
flags: 256,
status: 3,
rate: 2,
phase: 0,
timer: 0,
residual: u32::MAX,
};
let old = s;
assert!(timer_tick_loop(&mut s, input()).unwrap().is_empty());
assert_eq!(s, old);
s.rate = 6;
let old = s;
assert!(timer_tick_loop(&mut s, input()).is_err());
assert_eq!(s, old);
}
}
#[cfg(test)]
mod recovery_tests {
use super::*;
fn input() -> TimerLoopInput {
TimerLoopInput {
health: 1,
weapon: 7,
target: 2,
owner: 3,
}
}
#[test]
fn zero_crossing_delays_secondary_cleanup_and_switches_divisor() {
let mut s = TimerLoopState {
flags: 896,
status: 3,
rate: 2,
phase: 3,
timer: 0.05f32.to_bits(),
residual: 100,
};
let p = timer_tick_loop(&mut s, input()).unwrap();
assert_eq!(p.len(), 1);
assert_eq!(
[s.flags, s.status, s.rate, s.phase, s.timer],
[640, 3, 0, 0, 0]
);
s.residual = 100;
timer_tick_loop(&mut s, input()).unwrap();
assert_eq!([s.flags, s.status, s.rate, s.phase], [512, 2, 0, 1]);
}
#[test]
fn initial_nonpositive_timer_cleans_immediately_and_nonfinite_rejects_atomically() {
let mut s = TimerLoopState {
flags: 896,
status: 3,
rate: 2,
phase: 3,
timer: (-0.1f32).to_bits(),
residual: 100,
};
timer_tick_loop(&mut s, input()).unwrap();
assert_eq!([s.flags, s.status, s.rate, s.timer], [512, 2, 0, 0]);
s.timer = f32::NAN.to_bits();
s.residual = 100;
let old = s;
assert_eq!(timer_tick_loop(&mut s, input()), Err(Error::NonFinite));
assert_eq!(s, old);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CompleteTimerState {
pub activity: u32,
pub entity_bit: u32,
pub terminal: TimerTerminalState,
pub ticks: TimerLoopState,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CompleteTimerInput {
pub time: TimerTerminalInput,
pub last_tick: u32,
pub velocity: [u32; 3],
pub entity_bits: u32,
pub now: u32,
pub damage: TimerLoopInput,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TimerEvent {
Time,
Damage(TimerDamage),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CompleteTimerResult {
pub vector_squared: Option<u32>,
pub events: Vec<TimerEvent>,
}
/// Complete150692 persistent-state body through its void-return boundary. Time
/// is supplied and damage effects stay captured; this is not a live health VM.
pub fn complete_client_timer(
records: &mut [crate::native_policy::FilterRecord],
state: &mut CompleteTimerState,
input: CompleteTimerInput,
) -> Result<CompleteTimerResult, Error> {
use crate::filter_commands::{FilterFrameInput, FilterFrameState, filter_frame_prefix};
let mut staged = records.to_vec();
let mut next = *state;
let mut prefix = FilterFrameState {
activity: next.activity,
flag: next.entity_bit,
accumulator: next.ticks.residual,
};
let out = filter_frame_prefix(
&mut staged,
&mut prefix,
FilterFrameInput {
tick: input.time.tick,
last_tick: input.last_tick,
velocity: input.velocity,
entity_flags: input.entity_bits,
msec: input.time.msec,
now: input.now,
},
)?;
next.activity = prefix.activity;
next.entity_bit = prefix.flag;
next.ticks.residual = prefix.accumulator;
timer_entity_deadline(&mut next.terminal, input.time);
let damage = timer_tick_loop(&mut next.ticks, input.damage)?;
timer_terminal_tail(&mut next.terminal, input.time);
let mut events = vec![TimerEvent::Time];
events.extend(damage.into_iter().map(TimerEvent::Damage));
records.copy_from_slice(&staged);
*state = next;
Ok(CompleteTimerResult {
vector_squared: out.vector_squared,
events,
})
}
#[cfg(test)]
mod complete_tests {
use super::*;
fn state() -> CompleteTimerState {
CompleteTimerState {
activity: 0,
entity_bit: 17,
terminal: TimerTerminalState {
entity_flag: 7,
client_flags: 7,
elapsed_total: u32::MAX,
},
ticks: TimerLoopState {
flags: 896,
status: 3,
rate: 2,
phase: 3,
timer: 0.05f32.to_bits(),
residual: 250,
},
}
}
fn input() -> CompleteTimerInput {
CompleteTimerInput {
time: TimerTerminalInput {
stamp: 7999,
cooldown: 2,
tick: 10000,
deadline: 9999,
msec: 100,
},
last_tick: 6999,
velocity: [11f32.to_bits(), 0, 0],
entity_bits: 64,
now: 60,
damage: TimerLoopInput {
health: 1,
weapon: 7,
target: 2,
owner: 3,
},
}
}
#[test]
fn shared_residual_ordering_terminal_fields_and_full_expiry() {
let mut rows = vec![
crate::native_policy::FilterRecord {
mask: 7,
comparison: 8,
metadata: 0
};
4096
];
let mut s = state();
let out = complete_client_timer(&mut rows, &mut s, input()).unwrap();
assert_eq!(s.activity, 1);
assert_eq!(s.entity_bit, 1);
assert_eq!(
s.terminal,
TimerTerminalState {
entity_flag: 0,
client_flags: 5,
elapsed_total: 99
}
);
assert_eq!(
[
s.ticks.flags,
s.ticks.status,
s.ticks.rate,
s.ticks.phase,
s.ticks.timer,
s.ticks.residual
],
[512, 2, 0, 2, 0, 50]
);
assert_eq!(out.events.len(), 2);
assert_eq!(out.events[0], TimerEvent::Time);
assert!(matches!(out.events[1], TimerEvent::Damage(_)));
assert_eq!(rows[4095].comparison, u32::MAX);
}
#[test]
fn unsupported_loop_rolls_back_prefix_and_expiry_together() {
let mut rows = vec![
crate::native_policy::FilterRecord {
mask: 7,
comparison: 8,
metadata: 0
};
4096
];
let before = rows.clone();
let mut s = state();
s.ticks.rate = 6;
let old = s;
assert!(complete_client_timer(&mut rows, &mut s, input()).is_err());
assert_eq!(rows, before);
assert_eq!(s, old);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TimerPrepareState {
pub connection: u32,
pub exception: u32,
pub command_time: u32,
pub current_time: u32,
pub quantize: u32,
pub copied_tick: u32,
pub copied_command: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TimerPreparation {
pub proceed: bool,
pub msec: Option<u32>,
}
/// Original154089 prefix only. Sequential signed bounds use wrapping words;
/// optional8ms command rounding occurs after delta calculation, without recompute.
pub fn prepare_client_timing(state: &mut TimerPrepareState, tick: u32) -> TimerPreparation {
if state.connection != 2 {
return TimerPreparation {
proceed: false,
msec: None,
};
}
state.copied_tick = tick;
state.copied_command = state.command_time;
let upper = tick.wrapping_add(200);
let lower = tick.wrapping_sub(1000);
if state.command_time as i32 > upper as i32 {
state.command_time = upper;
}
if (state.command_time as i32) < lower as i32 {
state.command_time = lower;
}
let mut delta = state.command_time.wrapping_sub(state.current_time);
if (delta as i32) < 1 && state.exception != 2 {
return TimerPreparation {
proceed: false,
msec: Some(delta),
};
}
if delta as i32 > 200 {
delta = 200;
}
if state.quantize != 0 {
state.command_time =
((state.command_time.wrapping_add(7) as i32) / 8).wrapping_mul(8) as u32;
}
TimerPreparation {
proceed: true,
msec: Some(delta),
}
}
#[cfg(test)]
mod prepare_tests {
use super::*;
fn state() -> TimerPrepareState {
TimerPrepareState {
connection: 2,
exception: 0,
command_time: 9999,
current_time: 9990,
quantize: 1,
copied_tick: 17,
copied_command: 18,
}
}
#[test]
fn rounding_preserves_pre_round_delta_and_disconnected_state_is_untouched() {
let mut s = state();
let out = prepare_client_timing(&mut s, 10000);
assert_eq!(
out,
TimerPreparation {
proceed: true,
msec: Some(9)
}
);
assert_eq!(s.command_time, 10000);
assert_eq!(s.current_time, 9990);
assert_eq!(s.copied_command, 9999);
assert_eq!(s.copied_tick, 10000);
s.connection = 1;
let before = s;
assert_eq!(
prepare_client_timing(&mut s, 20000),
TimerPreparation {
proceed: false,
msec: None
}
);
assert_eq!(s, before);
}
#[test]
fn exception_allows_negative_delta_and_bounds_are_sequential_under_wrap() {
let mut s = state();
s.exception = 2;
s.command_time = 8000;
s.current_time = 10000;
let out = prepare_client_timing(&mut s, 10000);
assert_eq!(
out,
TimerPreparation {
proceed: true,
msec: Some((-1000i32) as u32)
}
);
assert_eq!(s.command_time, 9000);
s.exception = 0;
s.command_time = i32::MAX as u32;
s.current_time = 0;
s.quantize = 0;
let out = prepare_client_timing(&mut s, i32::MAX as u32);
assert_eq!(s.command_time, (i32::MAX - 1000) as u32);
assert_eq!(out.msec, Some(200));
}
}