874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
312 lines
10 KiB
Rust
312 lines
10 KiB
Rust
#![forbid(unsafe_code)]
|
|
//! Runs the JSON-lines scenarios of tools/test_game_loop_parity.py through the
|
|
//! Rust timer, tick-rate manager, window, mouse and key-mapping code.
|
|
use horizon_client::{
|
|
Result,
|
|
clock::{ServerTickRateManager, Timer},
|
|
keymapping::KeyState,
|
|
mouse::{LookOptions, MouseLook, Rotation},
|
|
options_file::{OptionsFile, V},
|
|
window::{self, FrameContext, InactivityFpsLimit},
|
|
};
|
|
use serde_json::Value;
|
|
use std::{
|
|
fs::File,
|
|
io::{BufRead, BufReader, BufWriter, Write},
|
|
};
|
|
|
|
fn fl(v: &Value) -> f32 {
|
|
f32::from_bits(u32::from_str_radix(v.as_str().unwrap(), 16).unwrap())
|
|
}
|
|
fn db(v: &Value) -> f64 {
|
|
f64::from_bits(u64::from_str_radix(v.as_str().unwrap(), 16).unwrap())
|
|
}
|
|
fn fb(v: f32) -> String {
|
|
format!("{:08x}", if v.is_nan() { 0x7fc0_0000 } else { v.to_bits() })
|
|
}
|
|
fn dbs(v: f64) -> String {
|
|
format!(
|
|
"{:016x}",
|
|
if v.is_nan() {
|
|
0x7ff8_0000_0000_0000
|
|
} else {
|
|
v.to_bits()
|
|
}
|
|
)
|
|
}
|
|
fn b(v: &Value) -> bool {
|
|
v.as_bool().unwrap()
|
|
}
|
|
|
|
fn timer(c: &Value, out: &mut impl Write) -> Result<()> {
|
|
let mut timer = Timer::new(fl(&c["tps"]), c["start"].as_i64().unwrap());
|
|
writeln!(out, "timer")?;
|
|
for op in c["ops"].as_array().unwrap() {
|
|
let name = op[0].as_str().unwrap();
|
|
let mut result = String::new();
|
|
match name {
|
|
"adv" => {
|
|
let mspt = if op[2].is_null() { 0.0 } else { fl(&op[2]) };
|
|
let ticks = timer.advance_game_time(op[1].as_i64().unwrap(), |d| {
|
|
if mspt == 0.0 { d } else { java_max(d, mspt) }
|
|
});
|
|
result = format!(" {ticks}");
|
|
}
|
|
"real" => timer.advance_real_time(op[1].as_i64().unwrap()),
|
|
"pause" => timer.update_pause_state(b(&op[1])),
|
|
"frozen" => timer.update_frozen_state(b(&op[1])),
|
|
_ => panic!("{name}"),
|
|
}
|
|
writeln!(
|
|
out,
|
|
"{name}{result} {} {} {} {}",
|
|
fb(timer.game_time_delta_ticks()),
|
|
fb(timer.game_time_delta_partial_tick(false)),
|
|
fb(timer.game_time_delta_partial_tick(true)),
|
|
fb(timer.realtime_delta_ticks())
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Math.max(float, float).
|
|
fn java_max(a: f32, b: f32) -> f32 {
|
|
if a.is_nan() || b.is_nan() {
|
|
f32::NAN
|
|
} else if a == 0.0 && b == 0.0 {
|
|
f32::from_bits(a.to_bits() & b.to_bits())
|
|
} else if a >= b {
|
|
a
|
|
} else {
|
|
b
|
|
}
|
|
}
|
|
|
|
fn rate(c: &Value, out: &mut impl Write) -> Result<()> {
|
|
let mut m = ServerTickRateManager::default();
|
|
writeln!(out, "rate")?;
|
|
for op in c["ops"].as_array().unwrap() {
|
|
let name = op[0].as_str().unwrap();
|
|
let mut result = String::new();
|
|
let mut report = None;
|
|
match name {
|
|
"rate" => m.set_tick_rate(fl(&op[1])),
|
|
"freeze" => m.base.set_frozen(b(&op[1])),
|
|
"step" => {
|
|
result = format!(" {}", m.step_game_if_paused(op[1].as_i64().unwrap() as i32))
|
|
}
|
|
"stopstep" => result = format!(" {}", m.stop_stepping()),
|
|
"sprint" => {
|
|
result = format!(
|
|
" {}",
|
|
m.request_game_to_sprint(op[1].as_i64().unwrap() as i32)
|
|
)
|
|
}
|
|
"check" => {
|
|
let (run, r) = m.check_should_sprint_this_tick(op[1].as_i64().unwrap());
|
|
result = format!(" {run}");
|
|
report = r;
|
|
}
|
|
"end" => m.end_tick_work(op[1].as_i64().unwrap()),
|
|
"stopsprint" => {
|
|
report = m.stop_sprinting();
|
|
result = format!(" {}", report.is_some());
|
|
}
|
|
"tick" => m.base.tick(),
|
|
_ => panic!("{name}"),
|
|
}
|
|
let tail = report.map_or(String::new(), |r| {
|
|
format!(" report {} {}", r.ticks_per_second, r.milliseconds_per_tick)
|
|
});
|
|
writeln!(
|
|
out,
|
|
"{name}{result} | {} {} {} {} {} {} {} {}{tail}",
|
|
fb(m.base.tickrate()),
|
|
m.base.nanoseconds_per_tick(),
|
|
fb(m.base.milliseconds_per_tick()),
|
|
m.base.runs_normally(),
|
|
m.base.is_stepping_forward(),
|
|
m.base.frozen_ticks_to_run(),
|
|
m.base.is_frozen(),
|
|
m.is_sprinting()
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn scale(c: &Value, out: &mut impl Write) -> Result<()> {
|
|
let i = |k: &str| c[k].as_i64().unwrap() as i32;
|
|
let fbsize = [i("w"), i("h")];
|
|
let scale = window::calculate_scale(fbsize, i("max"), b(&c["uni"]));
|
|
let [gw, gh] = window::gui_scaled_size(fbsize, scale);
|
|
writeln!(
|
|
out,
|
|
"scale {scale} {gw} {gh} {} {}",
|
|
dbs(window::scaled_pos(db(&c["x"]), gw, i("sw"))),
|
|
dbs(window::scaled_pos(db(&c["y"]), gh, i("sh")))
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn limit(c: &Value, out: &mut impl Write) -> Result<()> {
|
|
let ctx = FrameContext {
|
|
inactivity: if b(&c["afk"]) {
|
|
InactivityFpsLimit::Afk
|
|
} else {
|
|
InactivityFpsLimit::Minimized
|
|
},
|
|
iconified: b(&c["iconified"]),
|
|
focused: b(&c["focused"]),
|
|
exclusive_fullscreen: false,
|
|
now_ms: c["ago"].as_i64().unwrap(),
|
|
latest_input_ms: 0,
|
|
has_level: b(&c["level"]),
|
|
has_screen: b(&c["screen"]),
|
|
has_overlay: b(&c["overlay"]),
|
|
};
|
|
let reason = window::throttle_reason(ctx);
|
|
let name = match reason {
|
|
window::ThrottleReason::None => "NONE",
|
|
window::ThrottleReason::WindowIconified => "WINDOW_ICONIFIED",
|
|
window::ThrottleReason::LongAfk => "LONG_AFK",
|
|
window::ThrottleReason::ShortAfk => "SHORT_AFK",
|
|
window::ThrottleReason::OutOfLevelMenu => "OUT_OF_LEVEL_MENU",
|
|
};
|
|
writeln!(
|
|
out,
|
|
"limit {name} {} {}",
|
|
window::framerate_limit(c["opt"].as_i64().unwrap() as i32, ctx),
|
|
window::heavily_throttled(reason)
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn mouse(c: &Value, out: &mut impl Write) -> Result<()> {
|
|
let look = LookOptions {
|
|
sensitivity: db(&c["sens"]),
|
|
smooth_camera: b(&c["smooth"]),
|
|
scoping: b(&c["scoping"]),
|
|
invert_x: b(&c["ix"]),
|
|
invert_y: b(&c["iy"]),
|
|
};
|
|
let (active, grabbed) = (b(&c["active"]), b(&c["grabbed"]));
|
|
let mut handler = MouseLook::default();
|
|
let mut rotation = Rotation {
|
|
x_rot: 0.0,
|
|
y_rot: 0.0,
|
|
x_rot_o: 0.0,
|
|
y_rot_o: 0.0,
|
|
};
|
|
writeln!(out, "mouse")?;
|
|
for op in c["ops"].as_array().unwrap() {
|
|
match op[0].as_str().unwrap() {
|
|
"move" => handler.on_move(
|
|
db(&op[1]),
|
|
db(&op[2]),
|
|
db(&op[3]),
|
|
db(&op[4]),
|
|
active,
|
|
grabbed,
|
|
),
|
|
"ignore" => handler.set_ignore_first_move(),
|
|
"frame" => {
|
|
if let Some((xo, yo)) =
|
|
handler.handle_accumulated_movement(db(&op[1]), active, grabbed, look)
|
|
{
|
|
rotation.turn(xo, yo);
|
|
}
|
|
writeln!(
|
|
out,
|
|
"frame {} {} {} {}",
|
|
fb(rotation.x_rot),
|
|
fb(rotation.y_rot),
|
|
fb(rotation.x_rot_o),
|
|
fb(rotation.y_rot_o)
|
|
)?;
|
|
}
|
|
other => panic!("{other}"),
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn scroll(c: &Value, out: &mut impl Write) -> Result<()> {
|
|
let mut handler = MouseLook::default();
|
|
let mut slot = c["slot"].as_i64().unwrap() as i32;
|
|
let spectator = c["spectator"].as_bool().unwrap_or(false);
|
|
let mut speed = 0.05;
|
|
// OptionInstance.set quantises the wheel sensitivity.
|
|
let mut file = OptionsFile::default();
|
|
file.set("mouseWheelSensitivity", V::Double(db(&c["sens"])));
|
|
let sensitivity = file.double("mouseWheelSensitivity");
|
|
writeln!(out, "scroll")?;
|
|
for op in c["ops"].as_array().unwrap() {
|
|
if let Some((wheel, (_, y))) =
|
|
handler.on_scroll(db(&op[0]), db(&op[1]), b(&c["discrete"]), sensitivity)
|
|
{
|
|
if spectator {
|
|
speed = horizon_client::mouse::spectator_flying_speed(speed, y);
|
|
} else {
|
|
slot = horizon_client::mouse::ScrollWheelHandler::next_selection(
|
|
f64::from(wheel),
|
|
slot,
|
|
9,
|
|
);
|
|
}
|
|
}
|
|
if spectator {
|
|
writeln!(out, "{slot} {}", fb(speed))?;
|
|
} else {
|
|
writeln!(out, "{slot}")?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn key(c: &Value, out: &mut impl Write) -> Result<()> {
|
|
let mut state = if b(&c["toggle"]) {
|
|
KeyState::toggle(b(&c["restore"]), b(&c["keyboard"]))
|
|
} else {
|
|
KeyState::plain()
|
|
};
|
|
let mut option = false;
|
|
writeln!(out, "key")?;
|
|
for op in c["ops"].as_array().unwrap() {
|
|
let name = op[0].as_str().unwrap();
|
|
let mut result = String::new();
|
|
match name {
|
|
"option" => option = b(&op[1]),
|
|
"down" => state.set_down(b(&op[1]), option),
|
|
"release" => state.release(option),
|
|
"restore" => {
|
|
result = format!(" {}", state.should_restore_state_on_screen_closed(option))
|
|
}
|
|
"click" => state.click(),
|
|
"consume" => result = format!(" {}", state.consume_click()),
|
|
"ingame" => result = format!(" {}", state.should_set_on_ingame_focus(option)),
|
|
_ => panic!("{name}"),
|
|
}
|
|
writeln!(out, "{name}{result} {}", state.is_down)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn main() -> Result<()> {
|
|
let args: Vec<_> = crate::args().skip(1).collect();
|
|
let mut out = BufWriter::new(File::create(&args[1])?);
|
|
for line in BufReader::new(File::open(&args[0])?).lines() {
|
|
let c: Value = serde_json::from_str(&line?)?;
|
|
match c["t"].as_str().unwrap() {
|
|
"timer" => timer(&c, &mut out)?,
|
|
"rate" => rate(&c, &mut out)?,
|
|
"scale" => scale(&c, &mut out)?,
|
|
"limit" => limit(&c, &mut out)?,
|
|
"mouse" => mouse(&c, &mut out)?,
|
|
"scroll" => scroll(&c, &mut out)?,
|
|
"key" => key(&c, &mut out)?,
|
|
other => panic!("{other}"),
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|