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
29 changes: 26 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -329,16 +329,39 @@ jobs:
# slice 4 its 400 units are KayKit character models (about 2.2 M
# triangles in the main pass), which lavapipe draws at about 1.4 s a
# frame, so tick 400 comes about 120 s in: it gets a 180 s deadline
# (the sixth argument; docs/BUILD_TIMES.md). winit
# loads libxkbcommon-x11 and the X libraries at run time; the runner
# image lacks the first. The screenshots are uploaded for review.
# (the sixth argument; docs/BUILD_TIMES.md). Since art slice 6 it
# also renders `art_bench`, the art pass's stress scene (two full
# Towns, 400 units fighting, all of P0's units selected at tick 40),
# in its own 60 m framing: on lavapipe it draws about one frame a
# second, so on the 4-vCPU VM tick 200 came 64 s in and the run's
# 120 s reached tick 493, past the whole fight (ticks 47 to 388);
# tick 300 came 78 to 87 s in, too close to 120 s for a runner of
# unknown speed.
# winit loads libxkbcommon-x11 and the X libraries at run time; the
# runner image lacks the first. The screenshots are uploaded for
# review.
- name: Render smoke test (Xvfb, lavapipe)
run: |
sudo apt-get install -y --no-install-recommends xvfb mesa-vulkan-drivers libxkbcommon-x11-0 libx11-xcb1 libxcursor1 libxi6 libxrandr2
cargo build -p game --locked --profile ci
scripts/render_smoke.sh target/ci/eonmark "$RUNNER_TEMP/render-smoke"
scripts/render_smoke.sh target/ci/eonmark "$RUNNER_TEMP/render-smoke/unit_looks" unit_looks 20,32,40 100
scripts/render_smoke.sh target/ci/eonmark "$RUNNER_TEMP/render-smoke/enemy_outpost" enemy_outpost '' 400 180
scripts/render_smoke.sh target/ci/eonmark "$RUNNER_TEMP/render-smoke/art_bench" art_bench '' 200 120

# Art pass AP4: every render smoke runs with --draw-stats and prints
# `frame_stats: draws ... max=<n> ...`; fail unless every run's maximum
# is under 500 draw calls a frame (scripts/check_draws.sh; `mode=none`
# fails, since the counter does not count it). For `art_bench` (and
# `enemy_outpost`) --fight also requires that the fight was measured:
# a non-zero `fight_max` and a `frame_stats: fight` line with fight
# frames and ticks, so a run whose armies never fought fails.
- name: Draw-call gate (under 500 a frame)
run: |
scripts/check_draws.sh "$RUNNER_TEMP/render-smoke/render_smoke.log"
scripts/check_draws.sh "$RUNNER_TEMP/render-smoke/unit_looks/render_smoke.log"
scripts/check_draws.sh --fight "$RUNNER_TEMP/render-smoke/enemy_outpost/render_smoke.log"
scripts/check_draws.sh --fight "$RUNNER_TEMP/render-smoke/art_bench/render_smoke.log"

- uses: actions/upload-artifact@v7
if: always()
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ png = "0.18"

# dev-dependencies
pathfinding = "4.16"
# The game's HUD fit tests measure Inter's advances (art pass); the version
# parley 0.9 (Bevy's text layout) already locks, without its defaults.
skrifa = { version = "=0.42.1", default-features = false, features = ["std"] }
proptest = "1.11"
criterion = "0.8"

Expand Down
5 changes: 5 additions & 0 deletions crates/game/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,10 @@ objc2-app-kit = { workspace = true, features = [
"NSWorkspace",
] }

[dev-dependencies]
# HUD fit tests: Inter's advance widths (`fonts::measure`), the skrifa that
# parley already locks.
skrifa.workspace = true

[lints]
workspace = true
19 changes: 6 additions & 13 deletions crates/game/src/age_hud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
use bevy::picking::events::{Click, Pointer};
use bevy::picking::pointer::PointerButton;
use bevy::prelude::*;
use bevy::text::FontSize;
use rules::Rules;
use rules::strings::fill;
use sim::{Command, RejectReason, SimView};
Expand Down Expand Up @@ -145,10 +144,7 @@ impl Plugin for AgeHudPlugin {
fn text(value: &str, size: f32) -> impl Bundle {
(
Text::new(value),
TextFont {
font_size: FontSize::Px(size),
..default()
},
crate::fonts::hud_font(size),
TextColor(palette::HUD_TEXT),
TextLayout::no_wrap(),
)
Expand Down Expand Up @@ -287,6 +283,7 @@ mod tests {
use super::*;
use crate::economy_hud::PopReadout;
use crate::hud::spawn_hud;
use bevy::text::FontSize;

fn data_dir() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data")
Expand Down Expand Up @@ -493,14 +490,10 @@ mod tests {
}));
}

/// The glyph advance of Bevy 0.19.1's default font, the only one the
/// game uses until M7 (`bevy_text-0.19.1/src/FiraMono-subset.ttf`, a
/// monospace font: every glyph advances 600 of 1000 units per em).
const GLYPH_ADVANCE_EM: f32 = 0.6;

/// The width `e` lays out to, logical px, with `text_of` the text of a
/// readout (else its current text): a text, `GLYPH_ADVANCE_EM` per
/// character of its font size; a node, its children side by side with
/// readout (else its current text): a text, the sum of the UI font's
/// advances at its size (Inter since the art pass,
/// `crate::fonts::measure`); a node, its children side by side with
/// its column gap, plus its horizontal padding; nothing when hidden.
fn laid_out_width(world: &World, e: Entity, text_of: &dyn Fn(Entity) -> Option<String>) -> f32 {
let px = |v: Val| if let Val::Px(x) = v { x } else { 0.0 };
Expand All @@ -514,7 +507,7 @@ mod tests {
panic!("HUD text is sized in px");
};
let shown = text_of(e).unwrap_or_else(|| text.0.clone());
return padding + shown.chars().count() as f32 * GLYPH_ADVANCE_EM * font;
return padding + crate::fonts::measure::text_width_px(&shown, font);
}
let children: Vec<Entity> = world
.get::<Children>(e)
Expand Down
18 changes: 17 additions & 1 deletion crates/game/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,11 @@ pub fn run(cli: &Cli) -> AppExit {
// tracing subscriber, and an event before it is dropped.
// `scripts/render_smoke.sh` requires this line.
info!("assets: {}", assets.display());
// Art pass: Inter replaces Bevy's built-in FiraMono before the first
// text layout (`crate::fonts`).
app.add_plugins(crate::fonts::FontsPlugin {
assets: assets.clone(),
});
// Display sleep or a screen lock must not end the session.
app.add_plugins(MonitorLossPlugin);
if background {
Expand Down Expand Up @@ -437,17 +442,28 @@ pub fn run(cli: &Cli) -> AppExit {
crate::terrain_props::TerrainPropsPlugin,
));
// M5a: the attrition indicator, supply ring, annexation timer and
// readout, and the outcome overlay over the frozen world.
// readout, and the outcome overlay over the frozen world. Art pass:
// arrows, stones and melee lunges for every hit (presentation only).
app.add_plugins((
crate::markers::MarkersPlugin,
crate::outcome::OutcomeOverlayPlugin,
crate::combat_fx::CombatFxPlugin,
));

// Art pass AP6 (owner decision 7): the minimap bottom-left over the
// bottom bar; it prints `frame_stats: minimap ...` on exit.
app.add_plugins(crate::minimap::MinimapPlugin);

// Art pass AP10: every unit selectable by a real click (a driver
// clicks the printed targets; `crate::click_check`).
if cli.click_check {
app.add_plugins(crate::click_check::ClickCheckPlugin);
}
// Art pass AP6: the minimap's input with real X input (a driver
// performs the printed steps; `crate::minimap_check`).
if cli.minimap_check {
app.add_plugins(crate::minimap_check::MinimapCheckPlugin);
}

// Presentation only: frames and selects the walkthrough's beats, or
// selects `art_bench`'s army.
Expand Down
35 changes: 33 additions & 2 deletions crates/game/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
//! The art pass adds `--assets-dir <dir>` ([`Cli::resolve_assets_dir`], also
//! `EONMARK_ASSETS`), `--window-size <W>x<H>`, `--grid` (the debug grid,
//! off by default; G toggles it), `--draw-stats` (the draw-call counter,
//! `crate::draw_stats`) and `--click-check` (every unit selectable by a
//! real click, `crate::click_check`).
//! `crate::draw_stats`), `--click-check` (every unit selectable by a
//! real click, `crate::click_check`) and `--minimap-check` (the minimap's
//! input with real clicks, `crate::minimap_check`).
//! Parsing is complete and tested here.

use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -87,6 +88,12 @@ Options:
each primary click against the next target, then print
`click_check: ok <n>/<n>` (exit 0) or `click_check: FAIL
...` (exit 1). A driver clicks with real X input.
--minimap-check With --scenario economy_walkthrough: from tick 40 print
the minimap's rect and four steps (`minimap_check: step
<n> ...`: press, drag, right click, A then left click),
judge each, then print `minimap_check: ok` (exit 0) or
`minimap_check: FAIL ...` (exit 1). A driver performs the
steps with real X input.
-h, --help Show this text.

Data directory resolution order:
Expand Down Expand Up @@ -225,6 +232,9 @@ pub struct Cli {
/// `--click-check` (art pass): print every unit's click target and
/// judge real clicks against them (`crate::click_check`).
pub click_check: bool,
/// `--minimap-check` (art pass): print the minimap's steps and judge
/// real input against them (`crate::minimap_check`).
pub minimap_check: bool,
}

impl Default for Cli {
Expand Down Expand Up @@ -254,6 +264,7 @@ impl Default for Cli {
grid: false,
draw_stats: false,
click_check: false,
minimap_check: false,
}
}
}
Expand Down Expand Up @@ -343,6 +354,7 @@ impl Cli {
"--grid" => cli.grid = true,
"--draw-stats" => cli.draw_stats = true,
"--click-check" => cli.click_check = true,
"--minimap-check" => cli.minimap_check = true,
"--scenario" => {
let name = args.next().ok_or_else(|| format!("{arg}: missing value"))?;
if name.is_empty() || name.starts_with("--") {
Expand Down Expand Up @@ -426,6 +438,7 @@ impl Cli {
(cli.grid, "--grid"),
(cli.draw_stats, "--draw-stats"),
(cli.click_check, "--click-check"),
(cli.minimap_check, "--minimap-check"),
(cli.fast_forward_to.is_some(), "--fast-forward-to"),
(
cli.new_game_after_seconds.is_some(),
Expand All @@ -443,6 +456,12 @@ impl Cli {
.into(),
);
}
if cli.minimap_check && cli.headless_run.is_none() && cli.scenario.is_none() {
return Err(
"--minimap-check drives a scenario's minimap; add --scenario economy_walkthrough"
.into(),
);
}
if cli.spectate && cli.scenario.is_some() {
return Err("--spectate builds its own match; drop --scenario".into());
}
Expand Down Expand Up @@ -696,6 +715,7 @@ mod tests {
grid: false,
draw_stats: false,
click_check: false,
minimap_check: false,
})
);
let Parsed::Run(cli) = parsed else {
Expand Down Expand Up @@ -983,11 +1003,22 @@ mod tests {
);
let err = parse(&["--click-check"]).unwrap_err();
assert!(err.contains("--scenario unit_looks"), "{err}");
assert_eq!(
parse(&["--scenario", "economy_walkthrough", "--minimap-check"]),
Ok(Parsed::Run(Cli {
scenario: Some("economy_walkthrough".into()),
minimap_check: true,
..Cli::default()
}))
);
let err = parse(&["--minimap-check"]).unwrap_err();
assert!(err.contains("--scenario economy_walkthrough"), "{err}");
for flag in [
vec!["--window-size", "1280x720"],
vec!["--grid"],
vec!["--draw-stats"],
vec!["--click-check"],
vec!["--minimap-check"],
] {
let mut args = vec!["--headless-run", "10"];
args.extend(flag.iter().copied());
Expand Down
Loading
Loading