This is an automated email from the ASF dual-hosted git repository.
piotr pushed a commit to branch simulator_ui
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/simulator_ui by this push:
new 8d313ed66 Add nodes selector
8d313ed66 is described below
commit 8d313ed665330044ae6ac8ed42c16b04b0de36d5
Author: spetz <[email protected]>
AuthorDate: Tue Apr 14 15:36:08 2026 +0200
Add nodes selector
---
core/simulator_ui/src/components.rs | 21 ++
core/simulator_ui/src/helpers.rs | 12 +-
core/simulator_ui/src/input.rs | 39 +++-
core/simulator_ui/src/main.rs | 94 ++++++--
core/simulator_ui/src/panels.rs | 19 +-
core/simulator_ui/src/resources.rs | 91 +++++---
core/simulator_ui/src/setup.rs | 409 +++++++++++++++++++++++++++++----
core/simulator_ui/src/simulation.rs | 30 ++-
core/simulator_ui/src/theme.rs | 2 +-
core/simulator_ui/src/tracing_layer.rs | 17 +-
core/simulator_ui/src/visuals.rs | 12 +-
11 files changed, 621 insertions(+), 125 deletions(-)
diff --git a/core/simulator_ui/src/components.rs
b/core/simulator_ui/src/components.rs
index f1c536644..4b3669f6a 100644
--- a/core/simulator_ui/src/components.rs
+++ b/core/simulator_ui/src/components.rs
@@ -226,3 +226,24 @@ pub(crate) struct Lightning {
pub(crate) lifetime: f32,
pub(crate) max_lifetime: f32,
}
+
+#[derive(Component)]
+pub(crate) struct SelectionScreen;
+
+#[derive(Component)]
+pub(crate) struct SelectionCard {
+ pub(crate) index: usize,
+}
+
+#[derive(Component)]
+pub(crate) struct SelectionNumber {
+ pub(crate) index: usize,
+}
+
+#[derive(Component)]
+pub(crate) struct SelectionLabel {
+ pub(crate) index: usize,
+}
+
+#[derive(Component)]
+pub(crate) struct SelectionTitle;
diff --git a/core/simulator_ui/src/helpers.rs b/core/simulator_ui/src/helpers.rs
index 01e7ad287..76e13f8e8 100644
--- a/core/simulator_ui/src/helpers.rs
+++ b/core/simulator_ui/src/helpers.rs
@@ -121,6 +121,8 @@ pub(crate) fn dog_name(id: u8) -> &'static str {
2 => "Dash",
3 => "Bolt",
4 => "Flash",
+ 5 => "Storm",
+ 6 => "Blaze",
_ => "Pup",
}
}
@@ -226,7 +228,11 @@ pub(crate) fn spawn_trail_dot(commands: &mut Commands,
position: Vec2, color: Co
));
}
-pub(crate) fn narrate_event(event: &CapturedSimEvent, tick: u64) ->
Option<EventLogEntry> {
+pub(crate) fn narrate_event(
+ event: &CapturedSimEvent,
+ tick: u64,
+ replica_count: u8,
+) -> Option<EventLogEntry> {
match event.sim_event.as_str() {
"ClientRequestReceived" => {
let replica_id = event.replica_id.unwrap_or(0) as u8;
@@ -414,9 +420,9 @@ pub(crate) fn narrate_event(event: &CapturedSimEvent, tick:
u64) -> Option<Event
headline: format!("{name} started view change! (v{old_view} ->
v{new_view})"),
detail: format!(
"{reason_text} The pack is reshuffling -- \
- the dog at position (view {new_view} mod {REPLICA_COUNT})
= R{} \
+ the dog at position (view {new_view} mod {replica_count})
= R{} \
will become the new lead dog if quorum agrees.",
- new_view % REPLICA_COUNT as u64
+ new_view % replica_count as u64
),
color: NEON_MAGENTA,
})
diff --git a/core/simulator_ui/src/input.rs b/core/simulator_ui/src/input.rs
index e9e50356f..e786f60a3 100644
--- a/core/simulator_ui/src/input.rs
+++ b/core/simulator_ui/src/input.rs
@@ -17,21 +17,55 @@
use bevy::prelude::*;
+use crate::AppPhase;
use crate::components::*;
use crate::helpers::*;
use crate::queries::FxParams;
use crate::resources::*;
use crate::theme::*;
+pub(crate) fn handle_selection_input(
+ keys: Res<ButtonInput<KeyCode>>,
+ mut state: ResMut<SelectionState>,
+ mut config: ResMut<ReplicaConfig>,
+ mut next_state: ResMut<NextState<AppPhase>>,
+) {
+ if keys.just_pressed(KeyCode::ArrowLeft) && state.index > 0 {
+ state.index -= 1;
+ }
+ if keys.just_pressed(KeyCode::ArrowRight) && state.index <
PACK_OPTIONS.len() - 1 {
+ state.index += 1;
+ }
+
+ if keys.just_pressed(KeyCode::Digit3) ||
keys.just_pressed(KeyCode::Numpad3) {
+ state.index = 0;
+ }
+ if keys.just_pressed(KeyCode::Digit5) ||
keys.just_pressed(KeyCode::Numpad5) {
+ state.index = 1;
+ }
+ if keys.just_pressed(KeyCode::Digit7) ||
keys.just_pressed(KeyCode::Numpad7) {
+ state.index = 2;
+ }
+
+ if keys.just_pressed(KeyCode::Space) || keys.just_pressed(KeyCode::Enter) {
+ config.count = PACK_OPTIONS[state.index];
+ next_state.set(AppPhase::Simulating);
+ }
+}
+
+#[allow(clippy::too_many_arguments)]
pub(crate) fn handle_keyboard_input(
mut commands: Commands,
keys: Res<ButtonInput<KeyCode>>,
mut sim: NonSendMut<SimulationState>,
mut event_log: ResMut<EventLog>,
mut console: ResMut<GameConsole>,
+ config: Res<ReplicaConfig>,
positions: Res<ReplicaPositions>,
mut fx: FxParams,
) {
+ let replica_count = config.count;
+
if keys.just_pressed(KeyCode::Space) {
sim.playing = !sim.playing;
}
@@ -50,7 +84,6 @@ pub(crate) fn handle_keyboard_input(
if keys.just_pressed(KeyCode::KeyR) {
sim.simulator.inject_client_request();
if !sim.playing {
- // Auto-step so the request gets processed even when paused
for _ in 0..50 {
sim.simulator.step();
}
@@ -107,7 +140,7 @@ pub(crate) fn handle_keyboard_input(
fx.screen_flash.timer = SCREEN_FLASH_DURATION * 0.5;
fx.screen_flash.color = NEON_CYAN;
fx.screen_flash.intensity = 0.04;
- for replica_id in 0..REPLICA_COUNT {
+ for replica_id in 0..replica_count {
fx.replica_fx.revive[replica_id as usize] = 1.0;
fx.replica_fx.healthy[replica_id as usize] = 0.6;
trigger_replica_callout(&mut fx.replica_fx, replica_id, "HEALED!",
NEON_CYAN, 1.2);
@@ -136,7 +169,7 @@ pub(crate) fn handle_keyboard_input(
}
if keys.just_pressed(KeyCode::KeyP) {
- let alive: Vec<u8> = (0..REPLICA_COUNT)
+ let alive: Vec<u8> = (0..replica_count)
.filter(|&replica_id| !sim.simulator.is_crashed(replica_id))
.collect();
diff --git a/core/simulator_ui/src/main.rs b/core/simulator_ui/src/main.rs
index a5878a225..4104cdbd7 100644
--- a/core/simulator_ui/src/main.rs
+++ b/core/simulator_ui/src/main.rs
@@ -39,6 +39,13 @@ use resources::*;
use theme::*;
use tracing_layer::EventBuffer;
+#[derive(States, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
+pub(crate) enum AppPhase {
+ #[default]
+ Selecting,
+ Simulating,
+}
+
fn main() {
use tracing_layer::SimEventLayer;
use tracing_subscriber::prelude::*;
@@ -46,18 +53,27 @@ fn main() {
let event_buffer = EventBuffer::default();
let raw_lines = tracing_layer::RawLineBuffer::default();
+ let raw_generation = tracing_layer::RawLineGeneration::default();
- let filter =
+ let fmt_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_|
EnvFilter::new("warn,iggy.sim=debug"));
+ let sim_filter = EnvFilter::new("off,iggy.sim=debug");
let fmt_layer = fmt::layer()
.with_target(true)
.with_ansi(true)
.compact()
- .with_filter(filter);
+ .with_filter(fmt_filter);
tracing_subscriber::registry()
- .with(SimEventLayer::new(event_buffer.clone(), raw_lines.clone()))
+ .with(
+ SimEventLayer::new(
+ event_buffer.clone(),
+ raw_lines.clone(),
+ raw_generation.clone(),
+ )
+ .with_filter(sim_filter),
+ )
.with(fmt_layer)
.init();
@@ -67,8 +83,9 @@ fn main() {
bucket_capacity: 1,
});
- let simulation = SimulationState {
- simulator: UiSimulator::new(REPLICA_COUNT, 42, event_buffer),
+ // Placeholder simulation — overwritten in OnEnter(Simulating) by
reinit_simulation
+ let placeholder_sim = SimulationState {
+ simulator: UiSimulator::new(DEFAULT_REPLICA_COUNT, 42,
event_buffer.clone()),
playing: false,
speed: 1.0,
tick_accumulator: 0.0,
@@ -99,8 +116,14 @@ fn main() {
}),
)
.add_plugins(ShapePlugin)
+ .init_state::<AppPhase>()
.insert_resource(ClearColor(BG_DARK))
- .insert_non_send_resource(simulation)
+ .insert_non_send_resource(placeholder_sim)
+ .insert_resource(ReplicaConfig {
+ count: DEFAULT_REPLICA_COUNT,
+ })
+ .insert_resource(SelectionState::default())
+ .insert_resource(SharedBuffers { event_buffer })
.insert_resource(ReplicaPositions::default())
.insert_resource(ReplicaFxState::default())
.insert_resource(AppFxState::default())
@@ -109,31 +132,68 @@ fn main() {
.insert_resource(EventLog::default())
.insert_resource(GameConsole {
raw_lines,
+ raw_generation,
open: false,
slide: 0.0,
- last_line_count: 0,
+ last_seen_generation: 0,
})
.add_systems(Startup, setup::setup_camera)
+ .add_systems(Startup,
setup::setup_background.after(setup::setup_camera))
+ .add_systems(OnEnter(AppPhase::Selecting),
setup::setup_selection_screen)
+ .add_systems(
+ Update,
+ (
+ input::handle_selection_input,
+ setup::update_selection_screen,
+ )
+ .run_if(in_state(AppPhase::Selecting)),
+ )
+ .add_systems(OnExit(AppPhase::Selecting),
setup::despawn_selection_screen)
+ .add_systems(OnEnter(AppPhase::Simulating), setup::reinit_simulation)
.add_systems(
- Startup,
- (setup::setup_world, setup::setup_hud).after(setup::setup_camera),
+ OnEnter(AppPhase::Simulating),
+ (setup::setup_simulation_world,
setup::setup_hud).after(setup::reinit_simulation),
+ )
+ .add_systems(
+ Update,
+
input::handle_keyboard_input.run_if(in_state(AppPhase::Simulating)),
)
- .add_systems(Update, input::handle_keyboard_input)
.add_systems(Update, simulation::tick_simulation)
- .add_systems(Update, visuals::update_replica_visuals)
- .add_systems(Update, visuals::update_hud_text)
- .add_systems(Update, visuals::update_pause_overlay)
- .add_systems(Update, visuals::update_app_visuals)
- .add_systems(Update, visuals::update_app_flow_particles)
+ .add_systems(
+ Update,
+
visuals::update_replica_visuals.run_if(in_state(AppPhase::Simulating)),
+ )
+ .add_systems(
+ Update,
+ visuals::update_hud_text.run_if(in_state(AppPhase::Simulating)),
+ )
+ .add_systems(
+ Update,
+
visuals::update_pause_overlay.run_if(in_state(AppPhase::Simulating)),
+ )
+ .add_systems(
+ Update,
+ visuals::update_app_visuals.run_if(in_state(AppPhase::Simulating)),
+ )
+ .add_systems(
+ Update,
+
visuals::update_app_flow_particles.run_if(in_state(AppPhase::Simulating)),
+ )
.add_systems(Update, visuals::update_message_particles)
.add_systems(Update, visuals::update_trail_dots)
- .add_systems(Update, visuals::update_link_lines)
+ .add_systems(
+ Update,
+ visuals::update_link_lines.run_if(in_state(AppPhase::Simulating)),
+ )
.add_systems(Update, visuals::update_scan_lines)
.add_systems(Update, visuals::update_commit_rings)
.add_systems(Update, visuals::update_ambient_particles)
.add_systems(Update, visuals::update_screen_flash)
.add_systems(Update, visuals::update_speed_lines)
- .add_systems(Update, visuals::update_track_ring)
+ .add_systems(
+ Update,
+ visuals::update_track_ring.run_if(in_state(AppPhase::Simulating)),
+ )
.add_systems(Update, visuals::update_paw_bursts)
.add_systems(Update, visuals::update_lightning)
.add_systems(Update, panels::update_event_log_panel)
diff --git a/core/simulator_ui/src/panels.rs b/core/simulator_ui/src/panels.rs
index 02cf56218..8734dcc4d 100644
--- a/core/simulator_ui/src/panels.rs
+++ b/core/simulator_ui/src/panels.rs
@@ -151,17 +151,18 @@ pub(crate) fn update_game_console(
return;
}
- let (line_count, lines) = {
- let raw = console.raw_lines.lock().unwrap_or_else(|e| e.into_inner());
- (
- raw.len(),
- raw.iter().rev().take(60).cloned().collect::<Vec<_>>(),
- )
- };
- if line_count == console.last_line_count {
+ let current_generation = console
+ .raw_generation
+ .load(std::sync::atomic::Ordering::Relaxed);
+ if current_generation == console.last_seen_generation {
return;
}
- console.last_line_count = line_count;
+ console.last_seen_generation = current_generation;
+
+ let lines = {
+ let raw = console.raw_lines.lock().unwrap_or_else(|e| e.into_inner());
+ raw.iter().rev().take(60).cloned().collect::<Vec<_>>()
+ };
for (content_entity, children) in content_query.iter() {
if let Some(children) = children {
diff --git a/core/simulator_ui/src/resources.rs
b/core/simulator_ui/src/resources.rs
index 34c969ed5..36ae18632 100644
--- a/core/simulator_ui/src/resources.rs
+++ b/core/simulator_ui/src/resources.rs
@@ -15,14 +15,13 @@
// specific language governing permissions and limitations
// under the License.
+use bevy::prelude::*;
use std::collections::VecDeque;
use std::f32::consts::PI;
-use bevy::prelude::*;
-
use crate::bridge::UiSimulator;
use crate::theme::*;
-use crate::tracing_layer::RawLineBuffer;
+use crate::tracing_layer::{EventBuffer, RawLineBuffer, RawLineGeneration};
use crate::types::Status;
pub(crate) struct SimulationState {
@@ -37,14 +36,24 @@ pub(crate) struct SimulationState {
pub(crate) frame_count: u64,
}
+#[derive(Resource)]
+pub(crate) struct ReplicaConfig {
+ pub(crate) count: u8,
+}
+
+#[derive(Resource, Clone)]
+pub(crate) struct SharedBuffers {
+ pub(crate) event_buffer: EventBuffer,
+}
+
#[derive(Resource)]
pub(crate) struct ReplicaPositions(pub(crate) Vec<Vec2>);
-impl Default for ReplicaPositions {
- fn default() -> Self {
- let mut positions = Vec::with_capacity(REPLICA_COUNT as usize);
- for idx in 0..REPLICA_COUNT {
- let angle = 2.0 * PI * (idx as f32) / (REPLICA_COUNT as f32) - PI
/ 2.0;
+impl ReplicaPositions {
+ pub(crate) fn new(count: u8) -> Self {
+ let mut positions = Vec::with_capacity(count as usize);
+ for idx in 0..count {
+ let angle = 2.0 * PI * (idx as f32) / (count as f32) - PI / 2.0;
positions.push(Vec2::new(
CIRCLE_RADIUS * angle.cos(),
CIRCLE_RADIUS * angle.sin() + WORLD_CENTER_Y,
@@ -54,33 +63,46 @@ impl Default for ReplicaPositions {
}
}
+impl Default for ReplicaPositions {
+ fn default() -> Self {
+ Self::new(DEFAULT_REPLICA_COUNT)
+ }
+}
+
#[derive(Resource)]
pub(crate) struct ReplicaFxState {
- pub(crate) kill: [f32; REPLICA_COUNT as usize],
- pub(crate) revive: [f32; REPLICA_COUNT as usize],
- pub(crate) healthy: [f32; REPLICA_COUNT as usize],
- pub(crate) last_alive: [bool; REPLICA_COUNT as usize],
- pub(crate) last_status: [Status; REPLICA_COUNT as usize],
- pub(crate) callout_timer: [f32; REPLICA_COUNT as usize],
- pub(crate) callout_text: [String; REPLICA_COUNT as usize],
- pub(crate) callout_color: [Color; REPLICA_COUNT as usize],
+ pub(crate) kill: Vec<f32>,
+ pub(crate) revive: Vec<f32>,
+ pub(crate) healthy: Vec<f32>,
+ pub(crate) last_alive: Vec<bool>,
+ pub(crate) last_status: Vec<Status>,
+ pub(crate) callout_timer: Vec<f32>,
+ pub(crate) callout_text: Vec<String>,
+ pub(crate) callout_color: Vec<Color>,
}
-impl Default for ReplicaFxState {
- fn default() -> Self {
+impl ReplicaFxState {
+ pub(crate) fn new(count: u8) -> Self {
+ let n = count as usize;
Self {
- kill: [0.0; REPLICA_COUNT as usize],
- revive: [0.0; REPLICA_COUNT as usize],
- healthy: [0.0; REPLICA_COUNT as usize],
- last_alive: [true; REPLICA_COUNT as usize],
- last_status: [Status::Normal; REPLICA_COUNT as usize],
- callout_timer: [0.0; REPLICA_COUNT as usize],
- callout_text: std::array::from_fn(|_| String::new()),
- callout_color: [COOL_WHITE; REPLICA_COUNT as usize],
+ kill: vec![0.0; n],
+ revive: vec![0.0; n],
+ healthy: vec![0.0; n],
+ last_alive: vec![true; n],
+ last_status: vec![Status::Normal; n],
+ callout_timer: vec![0.0; n],
+ callout_text: (0..n).map(|_| String::new()).collect(),
+ callout_color: vec![COOL_WHITE; n],
}
}
}
+impl Default for ReplicaFxState {
+ fn default() -> Self {
+ Self::new(DEFAULT_REPLICA_COUNT)
+ }
+}
+
#[derive(Resource, Default)]
pub(crate) struct AppFxState {
pub(crate) pulse: [f32; 4],
@@ -131,9 +153,24 @@ impl EventLog {
#[derive(Resource)]
pub(crate) struct GameConsole {
pub(crate) raw_lines: RawLineBuffer,
+ pub(crate) raw_generation: RawLineGeneration,
pub(crate) open: bool,
pub(crate) slide: f32,
- pub(crate) last_line_count: usize,
+ pub(crate) last_seen_generation: u64,
+}
+
+pub(crate) const PACK_OPTIONS: [u8; 3] = [3, 5, 7];
+
+#[derive(Resource)]
+pub(crate) struct SelectionState {
+ pub(crate) index: usize,
+}
+
+#[allow(clippy::derivable_impls)]
+impl Default for SelectionState {
+ fn default() -> Self {
+ Self { index: 0 }
+ }
}
#[derive(Resource, Default)]
diff --git a/core/simulator_ui/src/setup.rs b/core/simulator_ui/src/setup.rs
index 0c11a076b..fbcf96290 100644
--- a/core/simulator_ui/src/setup.rs
+++ b/core/simulator_ui/src/setup.rs
@@ -20,6 +20,7 @@ use std::f32::consts::PI;
use bevy::prelude::*;
use bevy_prototype_lyon::prelude::*;
+use crate::bridge::UiSimulator;
use crate::components::*;
use crate::helpers::*;
use crate::resources::*;
@@ -29,13 +30,7 @@ pub(crate) fn setup_camera(mut commands: Commands) {
commands.spawn(Camera2d);
}
-pub(crate) fn setup_world(
- mut commands: Commands,
- positions: Res<ReplicaPositions>,
- asset_server: Res<AssetServer>,
-) {
- let mascot_handle: Handle<Image> = asset_server.load("iggy.png");
-
+pub(crate) fn setup_background(mut commands: Commands) {
for offset in [-420.0, -280.0, -140.0, 0.0, 140.0, 280.0, 420.0] {
let rising = shapes::Line(
Vec2::new(-700.0, -520.0 + offset),
@@ -70,6 +65,344 @@ pub(crate) fn setup_world(
));
}
+ for particle_idx in 0..AMBIENT_PARTICLE_COUNT {
+ let phase = particle_idx as f32 / AMBIENT_PARTICLE_COUNT as f32;
+ let x_pos = (phase * 17.3 + 0.5).fract() * 1600.0 - 800.0;
+ let y_pos = (phase * 23.7 + 0.2).fract() * 1100.0 - 550.0;
+ let size = 0.8 + (phase * 7.1).fract() * 1.8;
+ let alpha = 0.04 + (phase * 13.3).fract() * 0.10;
+ let color = match particle_idx % 5 {
+ 0 => NEON_CYAN,
+ 1 => IGGY_ORANGE,
+ _ => COOL_WHITE,
+ };
+
+ commands.spawn((
+ AmbientParticle {
+ velocity: Vec2::new(
+ ((phase * 31.1).fract() - 0.5) * 12.0,
+ 8.0 + (phase * 11.7).fract() * 18.0,
+ ),
+ drift_phase: phase * PI * 2.0,
+ drift_speed: 0.4 + (phase * 5.3).fract() * 0.8,
+ base_alpha: alpha,
+ },
+ build_fill(&circle_shape(size), color.with_alpha(alpha)),
+ Transform::from_xyz(x_pos, y_pos, -0.08),
+ ));
+ }
+
+ commands.spawn((
+ ScreenFlashOverlay,
+ Sprite::from_color(NEON_CYAN.with_alpha(0.0), Vec2::new(1600.0,
1100.0)),
+ Transform::from_xyz(0.0, 0.0, 90.0),
+ ));
+}
+
+pub(crate) fn setup_selection_screen(mut commands: Commands, asset_server:
Res<AssetServer>) {
+ // sygnet is the properly cropped mascot (6910x5211, ~1.33:1 ratio)
+ let sygnet: Handle<Image> =
asset_server.load("logo/4x/[email protected]");
+ let full_logo: Handle<Image> =
asset_server.load("logo/4x/[email protected]");
+
+ commands
+ .spawn((
+ SelectionScreen,
+ Node {
+ width: Val::Percent(100.0),
+ height: Val::Percent(100.0),
+ justify_content: JustifyContent::Center,
+ align_items: AlignItems::Center,
+ flex_direction: FlexDirection::Column,
+ row_gap: Val::Px(10.0),
+ ..default()
+ },
+ ))
+ .with_children(|root| {
+ root.spawn((
+ ImageNode::new(full_logo),
+ Node {
+ width: Val::Px(300.0),
+ height: Val::Px(100.0),
+ margin: UiRect::bottom(Val::Px(8.0)),
+ ..default()
+ },
+ ));
+
+ root.spawn((
+ Text::new("VSR SIMULATOR"),
+ TextFont {
+ font_size: 46.0,
+ ..default()
+ },
+ TextColor(IGGY_ORANGE),
+ ));
+
+ root.spawn((
+ Text::new("Viewstamped Replication visualized with racing
greyhounds"),
+ TextFont {
+ font_size: 13.0,
+ ..default()
+ },
+ TextColor(DIM_GRAY),
+ ));
+
+ root.spawn((
+ SelectionTitle,
+ Text::new("CHOOSE YOUR PACK"),
+ TextFont {
+ font_size: 22.0,
+ ..default()
+ },
+ TextColor(NEON_CYAN),
+ Node {
+ margin: UiRect::top(Val::Px(20.0)),
+ ..default()
+ },
+ ));
+
+ root.spawn(Node {
+ flex_direction: FlexDirection::Row,
+ column_gap: Val::Px(28.0),
+ margin: UiRect::axes(Val::Px(0.0), Val::Px(10.0)),
+ ..default()
+ })
+ .with_children(|row| {
+ let options: [(u8, &str, &str); 3] = [
+ (3, "TIGHT PACK", "tolerates 1 fault"),
+ (5, "BALANCED", "tolerates 2 faults"),
+ (7, "FULL PACK", "tolerates 3 faults"),
+ ];
+
+ for (idx, (count, title, detail)) in
options.iter().enumerate() {
+ row.spawn((
+ SelectionCard { index: idx },
+ Node {
+ width: Val::Px(220.0),
+ flex_direction: FlexDirection::Column,
+ align_items: AlignItems::Center,
+ justify_content: JustifyContent::Center,
+ padding: UiRect::new(
+ Val::Px(20.0),
+ Val::Px(20.0),
+ Val::Px(22.0),
+ Val::Px(18.0),
+ ),
+ border: UiRect::all(Val::Px(2.0)),
+ row_gap: Val::Px(6.0),
+ ..default()
+ },
+ BackgroundColor(Color::srgba_u8(0x0c, 0x14, 0x24,
0xcc)),
+ BorderColor::all(DIM_GRAY.with_alpha(0.15)),
+ ))
+ .with_children(|card| {
+ card.spawn(Node {
+ flex_direction: FlexDirection::Row,
+ column_gap: Val::Px(2.0),
+ margin: UiRect::bottom(Val::Px(4.0)),
+ justify_content: JustifyContent::Center,
+ ..default()
+ })
+ .with_children(|dogs_row| {
+ for _ in 0..*count {
+ dogs_row.spawn((
+ ImageNode::new(sygnet.clone()),
+ Node {
+ width: Val::Px(28.0),
+ height: Val::Px(21.0),
+ ..default()
+ },
+ ));
+ }
+ });
+
+ card.spawn((
+ SelectionNumber { index: idx },
+ Text::new(format!("{count}")),
+ TextFont {
+ font_size: 56.0,
+ ..default()
+ },
+ TextColor(DIM_GRAY.with_alpha(0.4)),
+ ));
+
+ card.spawn((
+ SelectionLabel { index: idx },
+ Text::new(*title),
+ TextFont {
+ font_size: 15.0,
+ ..default()
+ },
+ TextColor(DIM_GRAY.with_alpha(0.35)),
+ ));
+
+ card.spawn((
+ Text::new(*detail),
+ TextFont {
+ font_size: 11.0,
+ ..default()
+ },
+ TextColor(DIM_GRAY.with_alpha(0.3)),
+ ));
+ });
+ }
+ });
+
+ root.spawn(Node {
+ flex_direction: FlexDirection::Row,
+ column_gap: Val::Px(20.0),
+ align_items: AlignItems::Center,
+ margin: UiRect::top(Val::Px(12.0)),
+ ..default()
+ })
+ .with_children(|hint| {
+ for (key, action, color) in [
+ ("< >", "select", NEON_CYAN),
+ ("SPACE", "unleash the pack!", IGGY_ORANGE),
+ ] {
+ hint.spawn(Node {
+ flex_direction: FlexDirection::Row,
+ align_items: AlignItems::Center,
+ column_gap: Val::Px(6.0),
+ ..default()
+ })
+ .with_children(|pair| {
+ pair.spawn((
+ Node {
+ padding: UiRect::axes(Val::Px(12.0),
Val::Px(6.0)),
+ border: UiRect::all(Val::Px(1.0)),
+ ..default()
+ },
+ BackgroundColor(Color::srgba_u8(0x0c, 0x14, 0x24,
0xdd)),
+ BorderColor::all(color.with_alpha(0.5)),
+ ))
+ .with_children(|badge| {
+ badge.spawn((
+ Text::new(key),
+ TextFont {
+ font_size: 15.0,
+ ..default()
+ },
+ TextColor(color),
+ ));
+ });
+ pair.spawn((
+ Text::new(action),
+ TextFont {
+ font_size: 13.0,
+ ..default()
+ },
+ TextColor(COOL_WHITE.with_alpha(0.7)),
+ ));
+ });
+ }
+ });
+ });
+}
+
+#[allow(clippy::type_complexity)]
+pub(crate) fn update_selection_screen(
+ time: Res<Time>,
+ state: Res<SelectionState>,
+ mut cards: Query<
+ (&SelectionCard, &mut BorderColor, &mut BackgroundColor),
+ Without<SelectionNumber>,
+ >,
+ mut numbers: Query<
+ (&SelectionNumber, &mut TextColor),
+ (Without<SelectionCard>, Without<SelectionLabel>),
+ >,
+ mut labels: Query<
+ (&SelectionLabel, &mut TextColor),
+ (Without<SelectionCard>, Without<SelectionNumber>),
+ >,
+ mut title: Query<
+ &mut TextColor,
+ (
+ With<SelectionTitle>,
+ Without<SelectionCard>,
+ Without<SelectionNumber>,
+ Without<SelectionLabel>,
+ ),
+ >,
+) {
+ let elapsed = time.elapsed_secs();
+ let pulse = (elapsed * 3.5).sin() * 0.5 + 0.5;
+ let fast_pulse = (elapsed * 5.0).sin() * 0.5 + 0.5;
+ let slow_pulse = (elapsed * 1.8).sin() * 0.5 + 0.5;
+
+ for mut color in title.iter_mut() {
+ *color = TextColor(NEON_CYAN.with_alpha(0.7 + slow_pulse * 0.3));
+ }
+
+ for (card, mut border, mut bg) in cards.iter_mut() {
+ if card.index == state.index {
+ let glow = 0.6 + fast_pulse * 0.4;
+ *border = BorderColor::all(IGGY_ORANGE.with_alpha(glow));
+ *bg = BackgroundColor(IGGY_ORANGE.with_alpha(0.08 + pulse * 0.06));
+ } else {
+ *border = BorderColor::all(Color::srgba_u8(0x22, 0x2a, 0x3a,
0x44));
+ *bg = BackgroundColor(Color::srgba_u8(0x08, 0x0e, 0x1a, 0x88));
+ }
+ }
+
+ for (num, mut color) in numbers.iter_mut() {
+ if num.index == state.index {
+ let bright = 0.9 + fast_pulse * 0.1;
+ *color = TextColor(IGGY_ORANGE.with_alpha(bright));
+ } else {
+ *color = TextColor(DIM_GRAY.with_alpha(0.22));
+ }
+ }
+
+ for (label, mut color) in labels.iter_mut() {
+ if label.index == state.index {
+ *color = TextColor(NEON_CYAN.with_alpha(0.8 + pulse * 0.2));
+ } else {
+ *color = TextColor(DIM_GRAY.with_alpha(0.18));
+ }
+ }
+}
+
+pub(crate) fn despawn_selection_screen(
+ mut commands: Commands,
+ query: Query<Entity, With<SelectionScreen>>,
+) {
+ for entity in query.iter() {
+ commands.entity(entity).despawn();
+ }
+}
+
+pub(crate) fn reinit_simulation(
+ mut sim: NonSendMut<SimulationState>,
+ config: Res<ReplicaConfig>,
+ buffers: Res<SharedBuffers>,
+ mut positions: ResMut<ReplicaPositions>,
+ mut fx: ResMut<ReplicaFxState>,
+) {
+ *sim = SimulationState {
+ simulator: UiSimulator::new(config.count, 42,
buffers.event_buffer.clone()),
+ playing: false,
+ speed: 1.0,
+ tick_accumulator: 0.0,
+ total_commits: 0,
+ ops_per_second: 0.0,
+ ops_window_start: 0.0,
+ ops_window_count: 0,
+ frame_count: 0,
+ };
+ *positions = ReplicaPositions::new(config.count);
+ *fx = ReplicaFxState::new(config.count);
+}
+
+pub(crate) fn setup_simulation_world(
+ mut commands: Commands,
+ config: Res<ReplicaConfig>,
+ positions: Res<ReplicaPositions>,
+ asset_server: Res<AssetServer>,
+) {
+ let replica_count = config.count;
+ let mascot_handle: Handle<Image> = asset_server.load("iggy.png");
+
for scan_idx in 0..SCAN_LINE_COUNT {
let y_pos = -500.0 + (scan_idx as f32) * (1000.0 / SCAN_LINE_COUNT as
f32);
let line = shapes::Line(Vec2::new(-700.0, 0.0), Vec2::new(700.0, 0.0));
@@ -82,6 +415,7 @@ pub(crate) fn setup_world(
));
}
+ // Track rings
commands.spawn((
TrackRing,
build_stroke(
@@ -108,40 +442,8 @@ pub(crate) fn setup_world(
Transform::from_xyz(0.0, WORLD_CENTER_Y, -0.01),
));
- for particle_idx in 0..AMBIENT_PARTICLE_COUNT {
- let phase = particle_idx as f32 / AMBIENT_PARTICLE_COUNT as f32;
- let x_pos = (phase * 17.3 + 0.5).fract() * 1600.0 - 800.0;
- let y_pos = (phase * 23.7 + 0.2).fract() * 1100.0 - 550.0;
- let size = 0.8 + (phase * 7.1).fract() * 1.8;
- let alpha = 0.04 + (phase * 13.3).fract() * 0.10;
- let color = match particle_idx % 5 {
- 0 => NEON_CYAN,
- 1 => IGGY_ORANGE,
- _ => COOL_WHITE,
- };
-
- commands.spawn((
- AmbientParticle {
- velocity: Vec2::new(
- ((phase * 31.1).fract() - 0.5) * 12.0,
- 8.0 + (phase * 11.7).fract() * 18.0,
- ),
- drift_phase: phase * PI * 2.0,
- drift_speed: 0.4 + (phase * 5.3).fract() * 0.8,
- base_alpha: alpha,
- },
- build_fill(&circle_shape(size), color.with_alpha(alpha)),
- Transform::from_xyz(x_pos, y_pos, -0.08),
- ));
- }
-
- commands.spawn((
- ScreenFlashOverlay,
- Sprite::from_color(NEON_CYAN.with_alpha(0.0), Vec2::new(1600.0,
1100.0)),
- Transform::from_xyz(0.0, 0.0, 90.0),
- ));
-
- for replica_id in 0..REPLICA_COUNT {
+ // Replicas
+ for replica_id in 0..replica_count {
let position = positions.0[replica_id as usize];
let is_primary = replica_id == 0;
let accent = if is_primary { IGGY_ORANGE } else { NEON_CYAN };
@@ -235,8 +537,9 @@ pub(crate) fn setup_world(
));
}
- for from_id in 0..REPLICA_COUNT {
- for to_id in (from_id + 1)..REPLICA_COUNT {
+ // Network links between replicas
+ for from_id in 0..replica_count {
+ for to_id in (from_id + 1)..replica_count {
let from_pos = positions.0[from_id as usize];
let to_pos = positions.0[to_id as usize];
let bend = if (from_id + to_id) % 2 == 0 {
@@ -254,6 +557,7 @@ pub(crate) fn setup_world(
}
}
+ // App cards (producers / consumers)
let apps = [
(
0usize,
@@ -269,7 +573,7 @@ pub(crate) fn setup_world(
"BALL FETCHER",
AppKind::Consumer,
Vec2::new(560.0, 330.0),
- 1u8,
+ 1u8.min(replica_count - 1),
NEON_CYAN,
false,
),
@@ -278,7 +582,7 @@ pub(crate) fn setup_world(
"TREAT DISPENSER",
AppKind::Producer,
Vec2::new(-560.0, 70.0),
- 2u8,
+ 2u8.min(replica_count - 1),
NEON_YELLOW,
true,
),
@@ -338,12 +642,18 @@ pub(crate) fn setup_world(
}
}
-pub(crate) fn setup_hud(mut commands: Commands, asset_server:
Res<AssetServer>) {
+pub(crate) fn setup_hud(
+ mut commands: Commands,
+ asset_server: Res<AssetServer>,
+ config: Res<ReplicaConfig>,
+) {
let sygnet_handle: Handle<Image> =
asset_server.load("logo/4x/[email protected]");
let wordmark_handle: Handle<Image> =
asset_server.load("logo/4x/[email protected]");
+ let greyhound_label = format!("{} GREYHOUNDS", config.count);
+
commands
.spawn(Node {
width: Val::Percent(100.0),
@@ -605,9 +915,10 @@ pub(crate) fn setup_hud(mut commands: Commands,
asset_server: Res<AssetServer>)
..default()
})
.with_children(|chips| {
- for (label, color) in
- [("3 GREYHOUNDS", IGGY_ORANGE), ("FAULT READY",
HUD_ALERT)]
- {
+ for (label, color) in [
+ (greyhound_label.as_str(), IGGY_ORANGE),
+ ("FAULT READY", HUD_ALERT),
+ ] {
chips
.spawn((
Node {
diff --git a/core/simulator_ui/src/simulation.rs
b/core/simulator_ui/src/simulation.rs
index 021a578c3..d8f102d5c 100644
--- a/core/simulator_ui/src/simulation.rs
+++ b/core/simulator_ui/src/simulation.rs
@@ -32,6 +32,7 @@ pub(crate) fn tick_simulation(
mut commands: Commands,
time: Res<Time>,
mut sim: NonSendMut<SimulationState>,
+ config: Res<ReplicaConfig>,
positions: Res<ReplicaPositions>,
mut fx: FxParams,
mut event_log: ResMut<EventLog>,
@@ -47,6 +48,7 @@ pub(crate) fn tick_simulation(
return;
}
+ let replica_count = config.count;
let delta = time.delta_secs();
sim.tick_accumulator += delta * sim.speed * 60.0;
@@ -58,15 +60,20 @@ pub(crate) fn tick_simulation(
let events = sim.simulator.step();
for event in &events {
- if let Some(entry) = narrate_event(event, tick) {
+ if let Some(entry) = narrate_event(event, tick, replica_count) {
event_log.push(entry);
}
match event.sim_event.as_str() {
"ControlMessageScheduled" => {
- spawn_control_message_particles(&mut commands, event,
&positions);
+ spawn_control_message_particles(
+ &mut commands,
+ event,
+ &positions,
+ replica_count,
+ );
}
"PrepareQueued" => {
- spawn_prepare_particles(&mut commands, event, &positions);
+ spawn_prepare_particles(&mut commands, event, &positions,
replica_count);
}
"OperationCommitted" => {
handle_commit_event(
@@ -88,6 +95,7 @@ pub(crate) fn tick_simulation(
&mut fx.replica_fx,
&mut fx.app_fx,
&mut fx.screen_flash,
+ replica_count,
);
}
"ClientRequestReceived" => {
@@ -127,13 +135,14 @@ pub(crate) fn spawn_control_message_particles(
commands: &mut Commands,
event: &CapturedSimEvent,
positions: &ReplicaPositions,
+ replica_count: u8,
) {
let action = event.action.as_deref().unwrap_or("");
let msg_type =
MessageType::from_action(action).unwrap_or(MessageType::Prepare);
let from_idx = event.replica_id.unwrap_or(0) as usize;
let to_idx = event.target_replica.unwrap_or(0) as usize;
- if from_idx >= REPLICA_COUNT as usize || to_idx >= REPLICA_COUNT as usize {
+ if from_idx >= replica_count as usize || to_idx >= replica_count as usize {
return;
}
@@ -179,16 +188,17 @@ pub(crate) fn spawn_prepare_particles(
commands: &mut Commands,
event: &CapturedSimEvent,
positions: &ReplicaPositions,
+ replica_count: u8,
) {
let from_idx = event.replica_id.unwrap_or(0) as usize;
- if from_idx >= REPLICA_COUNT as usize {
+ if from_idx >= replica_count as usize {
return;
}
let from_pos = positions.0[from_idx];
let msg_color = MessageType::Prepare.color();
- for to_idx in 0..REPLICA_COUNT as usize {
+ for to_idx in 0..replica_count as usize {
if to_idx == from_idx {
continue;
}
@@ -344,6 +354,7 @@ pub(crate) fn handle_commit_event(
}
}
+#[allow(clippy::too_many_arguments)]
pub(crate) fn handle_view_change_event(
commands: &mut Commands,
event: &CapturedSimEvent,
@@ -352,6 +363,7 @@ pub(crate) fn handle_view_change_event(
replica_fx: &mut ResMut<ReplicaFxState>,
app_fx: &mut ResMut<AppFxState>,
screen_flash: &mut ResMut<ScreenFlash>,
+ replica_count: u8,
) {
let replica_id = event.replica_id.unwrap_or(0) as u8;
trigger_replica_callout(replica_fx, replica_id, "PACK SHUFFLE!",
NEON_MAGENTA, 1.15);
@@ -359,7 +371,7 @@ pub(crate) fn handle_view_change_event(
screen_flash.timer = SCREEN_FLASH_DURATION;
screen_flash.color = NEON_MAGENTA;
screen_flash.intensity = 0.12;
- for rid in 0..REPLICA_COUNT {
+ for rid in 0..replica_count {
commands.spawn(GlowFlash {
id: rid,
timer: 0.3,
@@ -386,8 +398,8 @@ pub(crate) fn handle_view_change_event(
let bolt_count = 3 + (sim.frame_count % 2) as usize;
for bolt_idx in 0..bolt_count {
- let from_idx = bolt_idx % REPLICA_COUNT as usize;
- let to_idx = (bolt_idx + 1) % REPLICA_COUNT as usize;
+ let from_idx = bolt_idx % replica_count as usize;
+ let to_idx = (bolt_idx + 1) % replica_count as usize;
let from_pos = positions.0[from_idx];
let to_pos = positions.0[to_idx];
let segment_count = 3 + (bolt_idx % 2);
diff --git a/core/simulator_ui/src/theme.rs b/core/simulator_ui/src/theme.rs
index 0b7bab246..a2de83a5f 100644
--- a/core/simulator_ui/src/theme.rs
+++ b/core/simulator_ui/src/theme.rs
@@ -29,7 +29,7 @@ pub(crate) const GRID_LINE: Color = Color::srgb_u8(0x11,
0x1d, 0x35);
pub(crate) const HUD_EDGE: Color = Color::srgb_u8(0x3d, 0x44, 0x50);
pub(crate) const HUD_ALERT: Color = Color::srgb_u8(0xff, 0x5f, 0x57);
-pub(crate) const REPLICA_COUNT: u8 = 3;
+pub(crate) const DEFAULT_REPLICA_COUNT: u8 = 3;
pub(crate) const CIRCLE_RADIUS: f32 = 300.0;
pub(crate) const WORLD_CENTER_Y: f32 = 90.0;
pub(crate) const IGGY_SPRITE_SIZE: Vec2 = Vec2::new(210.0, 176.0);
diff --git a/core/simulator_ui/src/tracing_layer.rs
b/core/simulator_ui/src/tracing_layer.rs
index b6c421909..d0c7e6a9d 100644
--- a/core/simulator_ui/src/tracing_layer.rs
+++ b/core/simulator_ui/src/tracing_layer.rs
@@ -17,6 +17,7 @@
use std::collections::VecDeque;
use std::fmt::Write;
+use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use crate::util::recover_lock;
@@ -54,6 +55,7 @@ pub struct CapturedSimEvent {
pub type EventBuffer = Arc<Mutex<Vec<CapturedSimEvent>>>;
pub type RawLineBuffer = Arc<Mutex<VecDeque<String>>>;
+pub type RawLineGeneration = Arc<AtomicU64>;
struct SimEventVisitor(CapturedSimEvent);
@@ -124,11 +126,20 @@ impl Visit for SimEventVisitor {
pub struct SimEventLayer {
buffer: EventBuffer,
raw_lines: RawLineBuffer,
+ raw_generation: RawLineGeneration,
}
impl SimEventLayer {
- pub fn new(buffer: EventBuffer, raw_lines: RawLineBuffer) -> Self {
- Self { buffer, raw_lines }
+ pub fn new(
+ buffer: EventBuffer,
+ raw_lines: RawLineBuffer,
+ raw_generation: RawLineGeneration,
+ ) -> Self {
+ Self {
+ buffer,
+ raw_lines,
+ raw_generation,
+ }
}
}
@@ -154,6 +165,8 @@ impl<S: Subscriber> Layer<S> for SimEventLayer {
raw.push_back(line);
drop(raw);
+ self.raw_generation.fetch_add(1, Ordering::Relaxed);
+
recover_lock(&self.buffer).push(visitor.0);
}
}
diff --git a/core/simulator_ui/src/visuals.rs b/core/simulator_ui/src/visuals.rs
index 8a18e8918..87129fdfe 100644
--- a/core/simulator_ui/src/visuals.rs
+++ b/core/simulator_ui/src/visuals.rs
@@ -27,10 +27,12 @@ use crate::resources::*;
use crate::theme::*;
use crate::types::{Role, Status};
+#[allow(clippy::type_complexity)]
pub(crate) fn update_replica_visuals(
- (time, sim, positions, mut replica_fx, mut commands): (
+ (time, sim, config, positions, mut replica_fx, mut commands): (
Res<Time>,
NonSend<SimulationState>,
+ Res<ReplicaConfig>,
Res<ReplicaPositions>,
ResMut<ReplicaFxState>,
Commands,
@@ -53,15 +55,15 @@ pub(crate) fn update_replica_visuals(
let elapsed = time.elapsed_secs();
let delta = time.delta_secs();
- for idx in 0..REPLICA_COUNT as usize {
+ let replica_count = config.count;
+ for idx in 0..replica_count as usize {
replica_fx.kill[idx] = (replica_fx.kill[idx] - delta * 1.4).max(0.0);
replica_fx.revive[idx] = (replica_fx.revive[idx] - delta *
1.1).max(0.0);
replica_fx.healthy[idx] = (replica_fx.healthy[idx] - delta *
0.9).max(0.0);
replica_fx.callout_timer[idx] = (replica_fx.callout_timer[idx] -
delta).max(0.0);
}
- let mut flash_overrides: [Option<Color>; REPLICA_COUNT as usize] =
- [None; REPLICA_COUNT as usize];
+ let mut flash_overrides: Vec<Option<Color>> = vec![None; replica_count as
usize];
for (entity, mut flash) in flash_query.iter_mut() {
flash.timer -= delta;
if flash.timer <= 0.0 {
@@ -70,7 +72,7 @@ pub(crate) fn update_replica_visuals(
}
let idx = flash.id as usize;
- if idx >= REPLICA_COUNT as usize {
+ if idx >= replica_count as usize {
continue;
}
flash_overrides[idx] = Some(flash.flash_color);