Skip to content
Merged
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
41 changes: 30 additions & 11 deletions pomme-client/src/app/phases/in_game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ pub struct GameState {
pub light_environment: crate::world::environment::LightEnvironment,
/// Vanilla `LightmapRenderStateExtractor.blockLightFlicker`.
pub block_light_flicker: f32,
/// Set by the test harness's `deterministic` step.
pub deterministic: bool,
/// F3+F4 game-mode switcher overlay, while open.
pub game_mode_switcher: Option<crate::ui::game_mode_switcher::GameModeSwitcherState>,
/// Spectator hotbar menu (vanilla `SpectatorGui`). Not a GUI screen: the
Expand Down Expand Up @@ -543,6 +545,7 @@ impl GameState {
default_clock: None,
light_environment: Default::default(),
block_light_flicker: 0.0,
deterministic: false,
game_mode_switcher: None,
spectator: Default::default(),
switcher_was_open: false,
Expand Down Expand Up @@ -2324,11 +2327,17 @@ pub fn update_game(
// `/tick freeze` (`isLevelRunningNormally`); pomme tracks neither yet.
let ticked = core.tick_accumulator >= TICK_RATE;
if ticked {
gfx.renderer.tick_texture_animations();
if game.deterministic {
gfx.renderer.hold_texture_animations();
} else {
gfx.renderer.tick_texture_animations();
}
}
while core.tick_accumulator >= TICK_RATE {
game.tick_count = game.tick_count.wrapping_add(1);
game.block_light_flicker = tick_block_light_flicker(game.block_light_flicker);
if !game.deterministic {
game.block_light_flicker = tick_block_light_flicker(game.block_light_flicker);
}
// Vanilla `Minecraft.tick` order: `gameMode.tick` drives the connection
// tick (and so the level load tracker) before the level's entities,
// i.e. before the local player moves or sends anything.
Expand Down Expand Up @@ -2361,11 +2370,13 @@ pub fn update_game(
}
game.item_entity_store
.tick(&game.chunk_store, game.block_registry.block_state_table());
game.level_runtime.animate_tick(
&game.block_registry,
&game.chunk_store,
*game.player.position,
);
if !game.deterministic {
game.level_runtime.animate_tick(
&game.block_registry,
&game.chunk_store,
*game.player.position,
);
}
game.particle_store
.tick(&game.chunk_store, game.block_registry.block_state_table());
game.block_entity_anim.tick();
Expand Down Expand Up @@ -2604,8 +2615,11 @@ pub fn update_game(
.prev_walk_dist
.lerp(game.player.walk_dist, partial_tick);
let bob_amount = game.player.prev_bob.lerp(game.player.bob, partial_tick);
gfx.renderer
.set_view_bob(bob_walk, bob_amount, core.menu.view_bobbing);
gfx.renderer.set_view_bob(
bob_walk,
bob_amount,
core.menu.view_bobbing && !game.deterministic,
);
gfx.renderer.set_post_effect(game.drawn_spectator_effect());
gfx.renderer.update_third_person_distance(
game.block_registry.render_view(),
Expand Down Expand Up @@ -4170,7 +4184,12 @@ pub fn update_game(
)
};

let particle_quads = if benchmark_running {
if game.deterministic {
game.particle_store.clear();
}
// Benchmarks and deterministic tests draw neither particles nor clouds.
let no_ambience = benchmark_running || game.deterministic;
let particle_quads = if no_ambience {
Vec::new()
} else {
game.particle_store
Expand Down Expand Up @@ -4245,7 +4264,7 @@ pub fn update_game(
&block_entity_renders,
&particle_quads,
&weather_columns,
if benchmark_running {
if no_ambience {
crate::renderer::CloudMode::Off
} else {
core.menu.cloud_mode
Expand Down
5 changes: 5 additions & 0 deletions pomme-client/src/harness/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ pub fn run(command: &Command, core: &mut AppCore, phase: &mut AppPhase) -> Outco
Command::Press(press) => press_key(core, press),
Command::Bind(bind) => bind_keys(core, bind),
Command::Option(options) => set_options(core, options),
Command::Deterministic => {
game.deterministic = true;
game.block_light_flicker = 0.0;
Ok(Value::Null)
}
Command::Hover(target) => hover(core, gfx, game, target),
Command::Cursor { at } => {
let gs = gui_scale(core, gfx);
Expand Down
4 changes: 4 additions & 0 deletions pomme-client/src/harness/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ pub enum Command {
Screenshot {
path: PathBuf,
},
/// Holds what varies frame to frame still for the rest of the session:
/// block light flicker, particles, animated textures, clouds and view
/// bobbing, so two runs of a script draw the same frames.
Deterministic,
}

#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
Expand Down
48 changes: 46 additions & 2 deletions pomme-client/src/harness/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ struct StepFile {
press: Option<protocol::Press>,
bind: Option<protocol::Bind>,
option: Option<protocol::OptionSet>,
deterministic: Option<bool>,
hover: Option<protocol::Hover>,
cursor: Option<[f32; 2]>,
scroll: Option<f32>,
text: Option<String>,
wait_ms: Option<u64>,
screenshot: Option<String>,
match_screenshots: Option<[String; 2]>,
expect: Option<Expect>,
timeout_ms: Option<u64>,
}
Expand Down Expand Up @@ -233,6 +235,8 @@ enum Step {
Run(Command),
Wait(Duration),
Screenshot(String),
/// Two of the script's screenshots, which must be pixel for pixel equal.
MatchScreenshots([String; 2]),
Expect(Expect, Duration),
}

Expand Down Expand Up @@ -273,6 +277,9 @@ impl StepFile {
if let Some(options) = self.option {
steps.push(Step::Run(Command::Option(options)));
}
if self.deterministic == Some(true) {
steps.push(Step::Run(Command::Deterministic));
}
if let Some(hover) = self.hover {
steps.push(Step::Run(Command::Hover(hover)));
}
Expand All @@ -291,6 +298,9 @@ impl StepFile {
if let Some(name) = self.screenshot {
steps.push(Step::Screenshot(name));
}
if let Some(names) = self.match_screenshots {
steps.push(Step::MatchScreenshots(names));
}
if let Some(expect) = self.expect {
steps.push(Step::Expect(expect, timeout.unwrap_or(DEFAULT_TIMEOUT)));
}
Expand Down Expand Up @@ -385,14 +395,15 @@ impl Script {
let path = match &self.pending_shot {
Some(path) => path.clone(),
None => {
let path = self.out_dir.join(format!("{name}.png"));
let path = self.shot_path(name);
exec::run(&Command::Screenshot { path: path.clone() }, core, phase);
self.pending_shot = Some(path.clone());
path
}
};
exec::screenshot_progress(phase, &path, started)
}
Step::MatchScreenshots(names) => Some(self.match_screenshots(names)),
Step::Expect(expect, timeout) => match check(expect, phase) {
Ok(()) => Some(Ok(())),
Err(e) if started.elapsed() > *timeout => Some(Err(e)),
Expand All @@ -416,6 +427,30 @@ impl Script {
}
}

fn shot_path(&self, name: &str) -> PathBuf {
self.out_dir.join(format!("{name}.png"))
}

fn match_screenshots(&self, [a, b]: &[String; 2]) -> Result<(), String> {
let load = |name: &String| {
image::open(self.shot_path(name))
.map(|image| image.into_rgba8())
.map_err(|e| format!("{name}: {e}"))
};
let (first, second) = (load(a)?, load(b)?);
if first.dimensions() != second.dimensions() {
return Err(format!("{a} and {b} differ in size"));
}
let differing = first
.pixels()
.zip(second.pixels())
.filter(|(p, q)| p != q)
.count();
ensure(differing == 0, || {
format!("{a} and {b} differ in {differing} pixels")
})
}

fn fail(&mut self, error: String) -> i32 {
self.reports.push(StepReport {
step: self.next + 1,
Expand Down Expand Up @@ -725,10 +760,19 @@ mod tests {
press = { key = "key.keyboard.j" }
[[step]]
option = { main_hand = "left" }
[[step]]
deterministic = true
[[step]]
match_screenshots = ["first", "second"]
"#,
)
.unwrap();
assert_eq!(steps.len(), 20);
assert_eq!(steps.len(), 22);
assert_eq!(steps[20], Step::Run(Command::Deterministic));
assert_eq!(
steps[21],
Step::MatchScreenshots(["first".into(), "second".into()])
);
assert_eq!(
steps[19],
Step::Run(Command::Option(OptionSet {
Expand Down
5 changes: 5 additions & 0 deletions pomme-client/src/particle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,11 @@ impl ParticleStore {
self.pending.push(particle);
}

pub fn clear(&mut self) {
self.particles.clear();
self.pending.clear();
}

pub fn tick(&mut self, chunks: &ChunkStore, block_state_table: BlockStateTable) {
let frames = self.end_rod_frames;
self.particles
Expand Down
10 changes: 10 additions & 0 deletions pomme-client/src/renderer/chunk/atlas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,16 @@ impl TextureAtlas {
self.animation_ticked = true;
}

/// Holds every animated sprite on its first frame.
pub fn hold_animations(&mut self) {
for sprite in &mut self.animated {
sprite.dirty |= sprite.frame != 0;
sprite.frame = 0;
sprite.sub_frame = 0;
}
self.animation_ticked = true;
}

/// Vanilla `uploadAnimationFrames`: writes every sprite that needs it
/// into its rectangle at each of its levels.
pub fn record_animation_upload(
Expand Down
4 changes: 4 additions & 0 deletions pomme-client/src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,10 @@ impl Renderer {
self.atlas.tick_animations();
}

pub fn hold_texture_animations(&mut self) {
self.atlas.hold_animations();
}

/// Redraws the lightmap with `info` on the next frame, as vanilla does
/// once per tick.
pub fn tick_lightmap(&mut self, info: LightmapInfo) {
Expand Down
9 changes: 9 additions & 0 deletions tests/ingame/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,22 @@ bind = { action = "key.drop", keys = ["key.keyboard.j"] } # this session only;
[[step]]
option = { main_hand = "left" } # sets client options, as the
# options screen does
[[step]]
deterministic = true # holds flicker, particles,
# animated textures, clouds and
# view bobbing still for the rest
# of the script

[[step]]
wait_ms = 1000

[[step]]
screenshot = "equipped" # target/ingame/<script>/equipped.png

[[step]]
match_screenshots = ["first", "second"] # fails unless two shots match
# pixel for pixel

[[step]]
expect = { slot = 6, item = "iron_chestplate" }
timeout_ms = 5000 # how long to retry (default 5 s)
Expand Down
76 changes: 76 additions & 0 deletions tests/ingame/world/deterministic.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# The deterministic step holds a scene's frame-to-frame noise still: a torch's
# flicker and smoke, lava's pops, animated textures, server particles and the
# clouds all stay put, so two shots a second apart match.

[[step]]
wait_ms = 2000

[[step]]
chat = "/gamerule advance_time false"

[[step]]
chat = "/time set 6000"

[[step]]
chat = "/gamemode creative"

# Sunk into the grass layer, so it can't flow.
[[step]]
chat = "/fill 79 -61 84 81 -61 85 minecraft:lava"

[[step]]
chat = "/setblock 82 -60 84 minecraft:torch"

[[step]]
chat = "/setblock 78 -60 84 minecraft:magma_block"

[[step]]
chat = "/tp @s 80.5 -60 81.5 0 30"

[[step]]
expect = { player = { position = [80.5, -60.0, 81.5] } }

[[step]]
press = { action = "key.toggleGui" }

[[step]]
deterministic = true

[[step]]
chat = "/particle minecraft:flame 80 -59.5 84 0.5 0.3 0.5 0 50 force"

[[step]]
wait_ms = 3000

[[step]]
screenshot = "first"

[[step]]
chat = "/particle minecraft:flame 80 -59.5 84 0.5 0.3 0.5 0 50 force"

[[step]]
wait_ms = 1000

[[step]]
screenshot = "second"

[[step]]
match_screenshots = ["first", "second"]

[[step]]
chat = "/tp @s 80.5 -60 81.5 0 -60"

[[step]]
expect = { player = { y_rot = 0.0 } }

[[step]]
wait_ms = 1000

[[step]]
screenshot = "sky"

[[step]]
chat = "/fill 79 -61 84 81 -61 85 minecraft:grass_block"

[[step]]
chat = "/fill 78 -60 84 82 -60 84 minecraft:air"
3 changes: 3 additions & 0 deletions tests/ingame/world/lightmap.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ expect = { player = { position = [60.5, -60.0, 50.5] } }
[[step]]
press = { action = "key.toggleGui" }

[[step]]
deterministic = true

[[step]]
wait_ms = 3000

Expand Down
Loading