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 e82f3c95b Add vocabulary
e82f3c95b is described below
commit e82f3c95b2e7603dc7042b4931a92699fd7ce541
Author: spetz <[email protected]>
AuthorDate: Tue Apr 14 16:42:52 2026 +0200
Add vocabulary
---
core/simulator_ui/src/components.rs | 10 +
core/simulator_ui/src/helpers.rs | 481 ++++++++++++++++++++++++------------
core/simulator_ui/src/input.rs | 62 +++--
core/simulator_ui/src/main.rs | 3 +
core/simulator_ui/src/setup.rs | 157 +++++++++---
core/simulator_ui/src/simulation.rs | 42 +++-
core/simulator_ui/src/visuals.rs | 59 ++++-
core/simulator_ui/src/vocabulary.rs | 392 +++++++++++++++++++++++++++++
8 files changed, 978 insertions(+), 228 deletions(-)
diff --git a/core/simulator_ui/src/components.rs
b/core/simulator_ui/src/components.rs
index 4b3669f6a..8225067e6 100644
--- a/core/simulator_ui/src/components.rs
+++ b/core/simulator_ui/src/components.rs
@@ -247,3 +247,13 @@ pub(crate) struct SelectionLabel {
#[derive(Component)]
pub(crate) struct SelectionTitle;
+
+#[derive(Component)]
+pub(crate) struct VocabCard {
+ pub(crate) is_dog: bool,
+}
+
+#[derive(Component)]
+pub(crate) struct VocabLabel {
+ pub(crate) is_dog: bool,
+}
diff --git a/core/simulator_ui/src/helpers.rs b/core/simulator_ui/src/helpers.rs
index 76e13f8e8..446d4207d 100644
--- a/core/simulator_ui/src/helpers.rs
+++ b/core/simulator_ui/src/helpers.rs
@@ -23,6 +23,7 @@ use crate::resources::*;
use crate::theme::*;
use crate::tracing_layer::CapturedSimEvent;
use crate::types::{MessageType, Role, Status};
+use crate::vocabulary::VocabMode;
pub(crate) fn circle_shape(radius: f32) -> shapes::Circle {
shapes::Circle {
@@ -103,30 +104,6 @@ pub(crate) fn status_color(status: Status, alive: bool) ->
Color {
}
}
-pub(crate) fn role_label(role: Role, status: Status) -> &'static str {
- match (role, status) {
- (Role::Primary, Status::Normal) => "LEAD DOG / running",
- (Role::Primary, Status::ViewChange) => "LEAD DOG / howling",
- (Role::Primary, Status::Recovering) => "LEAD DOG / limping",
- (Role::Backup, Status::Normal) => "PACK DOG / running",
- (Role::Backup, Status::ViewChange) => "PACK DOG / howling",
- (Role::Backup, Status::Recovering) => "PACK DOG / limping",
- }
-}
-
-pub(crate) fn dog_name(id: u8) -> &'static str {
- match id {
- 0 => "Iggy",
- 1 => "Zippy",
- 2 => "Dash",
- 3 => "Bolt",
- 4 => "Flash",
- 5 => "Storm",
- 6 => "Blaze",
- _ => "Pup",
- }
-}
-
pub(crate) fn trigger_replica_callout(
replica_fx: &mut ReplicaFxState,
id: u8,
@@ -232,19 +209,33 @@ pub(crate) fn narrate_event(
event: &CapturedSimEvent,
tick: u64,
replica_count: u8,
+ vocab: VocabMode,
) -> Option<EventLogEntry> {
match event.sim_event.as_str() {
"ClientRequestReceived" => {
let replica_id = event.replica_id.unwrap_or(0) as u8;
- let name = dog_name(replica_id);
+ let name = vocab.node_name(replica_id);
+ let (headline, detail) = match vocab {
+ VocabMode::Dog => (
+ format!("Ball thrown to {name}!"),
+ format!(
+ "A client tossed a new request to {name}
(R{replica_id}). \
+ As lead dog, {name} will replicate it to the pack
before confirming."
+ ),
+ ),
+ VocabMode::Technical => (
+ format!("Client request -> {name}"),
+ format!(
+ "Primary {name} (R{replica_id}) received client
request. \
+ Will broadcast Prepare to backups."
+ ),
+ ),
+ };
Some(EventLogEntry {
tick,
icon: "[REQ]",
- headline: format!("Ball thrown to {name}!"),
- detail: format!(
- "A client tossed a new request to {name} (R{replica_id}). \
- As lead dog, {name} will replicate it to the pack before
confirming."
- ),
+ headline,
+ detail,
color: IGGY_ORANGE,
})
}
@@ -252,15 +243,28 @@ pub(crate) fn narrate_event(
let replica_id = event.replica_id.unwrap_or(0) as u8;
let op = event.op.unwrap_or(0);
let pipeline_depth = event.pipeline_depth.unwrap_or(0);
- let name = dog_name(replica_id);
+ let name = vocab.node_name(replica_id);
+ let (headline, detail) = match vocab {
+ VocabMode::Dog => (
+ format!("{name} queued op #{op}"),
+ format!(
+ "{name} added operation #{op} to the pipeline
({pipeline_depth}/8 slots used). \
+ Now broadcasting Prepare to the pack -- every dog
must log this before it counts."
+ ),
+ ),
+ VocabMode::Technical => (
+ format!("{name} queued op #{op}"),
+ format!(
+ "Operation #{op} added to pipeline ({pipeline_depth}/8
used). \
+ Broadcasting Prepare to all backups."
+ ),
+ ),
+ };
Some(EventLogEntry {
tick,
icon: "[QUE]",
- headline: format!("{name} queued op #{op}"),
- detail: format!(
- "{name} added operation #{op} to the pipeline
({pipeline_depth}/8 slots used). \
- Now broadcasting Prepare to the pack -- every dog must
log this before it counts."
- ),
+ headline,
+ detail,
color: Color::srgb_u8(95, 135, 253),
})
}
@@ -271,26 +275,50 @@ pub(crate) fn narrate_event(
let ack_count = event.ack_count.unwrap_or(0);
let quorum = event.quorum.unwrap_or(0);
let quorum_reached = event.quorum_reached.unwrap_or(false);
- let lead = dog_name(replica_id);
- let acker = dog_name(ack_from);
- let status_detail = if quorum_reached {
- format!("That's {ack_count}/{quorum} -- quorum reached! {lead}
can now commit.")
- } else {
- format!(
- "That's {ack_count}/{quorum} acks. Need {} more for
quorum.",
- quorum.saturating_sub(ack_count)
- )
+ let lead = vocab.node_name(replica_id);
+ let acker = vocab.node_name(ack_from);
+ let quorum_suffix = if quorum_reached { " -- QUORUM!" } else { ""
};
+ let (headline, detail) = match vocab {
+ VocabMode::Dog => {
+ let status_detail = if quorum_reached {
+ format!(
+ "That's {ack_count}/{quorum} -- quorum reached!
{lead} can now commit."
+ )
+ } else {
+ format!(
+ "That's {ack_count}/{quorum} acks. Need {} more
for quorum.",
+ quorum.saturating_sub(ack_count)
+ )
+ };
+ (
+ format!("{acker} acked op #{op}{quorum_suffix}"),
+ format!(
+ "{acker} (R{ack_from}) confirmed it logged
operation #{op}. {status_detail}"
+ ),
+ )
+ }
+ VocabMode::Technical => {
+ let status_detail = if quorum_reached {
+ format!("{ack_count}/{quorum} acks -- quorum reached.
{lead} will commit.")
+ } else {
+ format!(
+ "{ack_count}/{quorum} acks. Need {} more for
quorum.",
+ quorum.saturating_sub(ack_count)
+ )
+ };
+ (
+ format!("{acker} acked op #{op}{quorum_suffix}"),
+ format!(
+ "{acker} (R{ack_from}) acknowledged operation
#{op}. {status_detail}"
+ ),
+ )
+ }
};
Some(EventLogEntry {
tick,
icon: if quorum_reached { "[OK!]" } else { "[ACK]" },
- headline: format!(
- "{acker} acked op #{op}{}",
- if quorum_reached { " -- QUORUM!" } else { "" }
- ),
- detail: format!(
- "{acker} (R{ack_from}) confirmed it logged operation
#{op}. {status_detail}"
- ),
+ headline,
+ detail,
color: if quorum_reached {
IGGY_ORANGE
} else {
@@ -301,30 +329,53 @@ pub(crate) fn narrate_event(
"OperationCommitted" => {
let replica_id = event.replica_id.unwrap_or(0) as u8;
let op = event.op.unwrap_or(0);
- let name = dog_name(replica_id);
+ let name = vocab.node_name(replica_id);
+ let (headline, detail) = match vocab {
+ VocabMode::Dog => (
+ format!("{name} committed op #{op}!"),
+ format!(
+ "Operation #{op} is officially committed on {name}
(R{replica_id}). \
+ The pack agreed -- this data is now durable and safe.
Good boy!"
+ ),
+ ),
+ VocabMode::Technical => (
+ format!("{name} committed op #{op}"),
+ format!(
+ "Op #{op} committed on {name} (R{replica_id}). \
+ Quorum achieved, data durable."
+ ),
+ ),
+ };
Some(EventLogEntry {
tick,
icon: "[WIN]",
- headline: format!("{name} committed op #{op}!"),
- detail: format!(
- "Operation #{op} is officially committed on {name}
(R{replica_id}). \
- The pack agreed -- this data is now durable and safe.
Good boy!"
- ),
+ headline,
+ detail,
color: IGGY_ORANGE,
})
}
"ClientReplyEmitted" => {
let replica_id = event.replica_id.unwrap_or(0) as u8;
let op = event.op.unwrap_or(0);
- let name = dog_name(replica_id);
+ let name = vocab.node_name(replica_id);
+ let (headline, detail) = match vocab {
+ VocabMode::Dog => (
+ format!("{name} returned the ball!"),
+ format!(
+ "{name} sent a reply to the client. The request for op
#{op} \
+ was committed and acknowledged. Fetch complete!"
+ ),
+ ),
+ VocabMode::Technical => (
+ format!("{name} replied to client"),
+ format!("Reply sent for op #{op}. Request complete."),
+ ),
+ };
Some(EventLogEntry {
tick,
icon: "[RPL]",
- headline: format!("{name} returned the ball!"),
- detail: format!(
- "{name} sent a reply to the client. The request for op
#{op} \
- was committed and acknowledged. Fetch complete!"
- ),
+ headline,
+ detail,
color: MessageType::ClientReply.color(),
})
}
@@ -332,51 +383,92 @@ pub(crate) fn narrate_event(
let replica_id = event.replica_id.unwrap_or(0) as u8;
let target = event.target_replica.unwrap_or(0) as u8;
let action = event.action.as_deref().unwrap_or("");
- let from_name = dog_name(replica_id);
- let to_name = dog_name(target);
+ let from_name = vocab.node_name(replica_id);
+ let to_name = vocab.node_name(target);
let msg_type = MessageType::from_action(action);
- let (icon, headline, detail) = match msg_type {
- Some(MessageType::PrepareOk) => (
- "[POK]",
- format!("{from_name} -> {to_name}: PrepareOk"),
- format!(
- "{from_name} confirmed to {to_name}: \"I logged op
#{}!\" \
- One step closer to quorum.",
- event.op.unwrap_or(0)
+ let (icon, headline, detail) = match vocab {
+ VocabMode::Dog => match msg_type {
+ Some(MessageType::PrepareOk) => (
+ "[POK]",
+ format!("{from_name} -> {to_name}: PrepareOk"),
+ format!(
+ "{from_name} confirmed to {to_name}: \"I logged op
#{}!\" \
+ One step closer to quorum.",
+ event.op.unwrap_or(0)
+ ),
),
- ),
- Some(MessageType::StartViewChange) => (
- "[SVC]",
- format!("{from_name} howls: StartViewChange!"),
- format!(
- "{from_name} suspects the lead dog is down!
Broadcasting view change \
- to view {} -- the pack needs a new leader.",
- event.view.unwrap_or(0)
+ Some(MessageType::StartViewChange) => (
+ "[SVC]",
+ format!("{from_name} howls: StartViewChange!"),
+ format!(
+ "{from_name} suspects the lead dog is down!
Broadcasting view change \
+ to view {} -- the pack needs a new leader.",
+ event.view.unwrap_or(0)
+ ),
),
- ),
- Some(MessageType::DoViewChange) => (
- "[DVC]",
- format!("{from_name} -> {to_name}: DoViewChange"),
- format!(
- "{from_name} is voting for {to_name} to become the new
lead dog in view {}. \
- Sending its log state so the new leader has all
committed data.",
- event.view.unwrap_or(0)
+ Some(MessageType::DoViewChange) => (
+ "[DVC]",
+ format!("{from_name} -> {to_name}: DoViewChange"),
+ format!(
+ "{from_name} is voting for {to_name} to become the
new lead dog in view {}. \
+ Sending its log state so the new leader has all
committed data.",
+ event.view.unwrap_or(0)
+ ),
),
- ),
- Some(MessageType::StartView) => (
- "[NEW]",
- format!("{from_name} announces: I'm lead dog!"),
- format!(
- "{from_name} won the election for view {}!
Broadcasting StartView to the pack -- \
- everyone sync up and follow the new leader.",
- event.view.unwrap_or(0)
+ Some(MessageType::StartView) => (
+ "[NEW]",
+ format!("{from_name} announces: I'm lead dog!"),
+ format!(
+ "{from_name} won the election for view {}!
Broadcasting StartView to the pack -- \
+ everyone sync up and follow the new leader.",
+ event.view.unwrap_or(0)
+ ),
),
- ),
- _ => (
- "[MSG]",
- format!("{from_name} -> {to_name}: {action}"),
- format!("Control message from {from_name} to {to_name}."),
- ),
+ _ => (
+ "[MSG]",
+ format!("{from_name} -> {to_name}: {action}"),
+ format!("Control message from {from_name} to
{to_name}."),
+ ),
+ },
+ VocabMode::Technical => match msg_type {
+ Some(MessageType::PrepareOk) => (
+ "[POK]",
+ format!("{from_name} -> {to_name}: PrepareOk"),
+ format!(
+ "{from_name} acknowledged op #{} to {to_name}.",
+ event.op.unwrap_or(0)
+ ),
+ ),
+ Some(MessageType::StartViewChange) => (
+ "[SVC]",
+ format!("{from_name} -> {to_name}: StartViewChange"),
+ format!(
+ "{from_name} initiated view change to view {}.",
+ event.view.unwrap_or(0)
+ ),
+ ),
+ Some(MessageType::DoViewChange) => (
+ "[DVC]",
+ format!("{from_name} -> {to_name}: DoViewChange"),
+ format!(
+ "{from_name} sending DoViewChange to {to_name} for
view {}.",
+ event.view.unwrap_or(0)
+ ),
+ ),
+ Some(MessageType::StartView) => (
+ "[NEW]",
+ format!("{from_name} -> {to_name}: StartView"),
+ format!(
+ "{from_name} broadcasting StartView for view {}.",
+ event.view.unwrap_or(0)
+ ),
+ ),
+ _ => (
+ "[MSG]",
+ format!("{from_name} -> {to_name}: {action}"),
+ format!("Control message from {from_name} to
{to_name}."),
+ ),
+ },
};
let color = msg_type.map(|mt| mt.color()).unwrap_or(DIM_GRAY);
Some(EventLogEntry {
@@ -391,61 +483,106 @@ pub(crate) fn narrate_event(
let replica_id = event.replica_id.unwrap_or(0) as u8;
let old_view = event.old_view.unwrap_or(0);
let new_view = event.new_view.unwrap_or(0);
- let name = dog_name(replica_id);
- let reason_text = event
- .reason
- .as_deref()
- .map(|reason| match reason {
- "heartbeat_timeout" => format!(
- "{name} hasn't heard from the lead dog in too long
(heartbeat timeout)."
- ),
- "view_change_timeout" => {
- "The view change itself timed out -- the pack couldn't
agree fast enough."
- .to_string()
- }
- "received_start_view_change" => {
- format!("{name} heard another dog howling for a view
change and joined in.")
- }
- "received_do_view_change" => {
+ let name = vocab.node_name(replica_id);
+ let (headline, detail) = match vocab {
+ VocabMode::Dog => {
+ let reason_text = event
+ .reason
+ .as_deref()
+ .map(|reason| match reason {
+ "heartbeat_timeout" => format!(
+ "{name} hasn't heard from the lead dog in too
long (heartbeat timeout)."
+ ),
+ "view_change_timeout" => {
+ "The view change itself timed out -- the pack
couldn't agree fast enough."
+ .to_string()
+ }
+ "received_start_view_change" => {
+ format!("{name} heard another dog howling for
a view change and joined in.")
+ }
+ "received_do_view_change" => {
+ format!(
+ "{name} received a DoViewChange vote,
triggering its own view change."
+ )
+ }
+ other => format!("Reason: {other}"),
+ })
+ .unwrap_or_default();
+ (
+ format!("{name} started view change! (v{old_view} ->
v{new_view})"),
format!(
- "{name} received a DoViewChange vote, triggering
its own view change."
- )
- }
- other => format!("Reason: {other}"),
- })
- .unwrap_or_default();
+ "{reason_text} The pack is reshuffling -- \
+ 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
+ ),
+ )
+ }
+ VocabMode::Technical => {
+ let reason_text = event
+ .reason
+ .as_deref()
+ .map(|reason| match reason {
+ "heartbeat_timeout" => "Heartbeat
timeout.".to_string(),
+ "view_change_timeout" => "View change
timeout.".to_string(),
+ "received_start_view_change" => {
+ "Received StartViewChange from
peer.".to_string()
+ }
+ "received_do_view_change" => {
+ "Received DoViewChange from peer.".to_string()
+ }
+ other => format!("Reason: {other}."),
+ })
+ .unwrap_or_default();
+ (
+ format!("{name} started view change (v{old_view} ->
v{new_view})"),
+ format!(
+ "{reason_text} New primary will be R{} (view
{new_view} mod {replica_count}).",
+ new_view % replica_count as u64
+ ),
+ )
+ }
+ };
Some(EventLogEntry {
tick,
icon: "[VCH]",
- 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{} \
- will become the new lead dog if quorum agrees.",
- new_view % replica_count as u64
- ),
+ headline,
+ detail,
color: NEON_MAGENTA,
})
}
"PrimaryElected" => {
let replica_id = event.replica_id.unwrap_or(0) as u8;
let view = event.view.unwrap_or(0);
- let name = dog_name(replica_id);
+ let name = vocab.node_name(replica_id);
+ let (headline, detail) = match vocab {
+ VocabMode::Dog => (
+ format!("{name} is the new lead dog! (view {view})"),
+ format!(
+ "{name} (R{replica_id}) collected enough DoViewChange
votes and is now \
+ the lead dog for view {view}. The pack will follow
{name}'s lead \
+ for all new operations. Broadcasting StartView to
synchronize everyone."
+ ),
+ ),
+ VocabMode::Technical => (
+ format!("{name} elected primary (view {view})"),
+ format!(
+ "{name} (R{replica_id}) elected primary for view
{view}. \
+ Received sufficient DoViewChange votes. Broadcasting
StartView."
+ ),
+ ),
+ };
Some(EventLogEntry {
tick,
icon: "[LDR]",
- headline: format!("{name} is the new lead dog! (view {view})"),
- detail: format!(
- "{name} (R{replica_id}) collected enough DoViewChange
votes and is now \
- the lead dog for view {view}. The pack will follow
{name}'s lead \
- for all new operations. Broadcasting StartView to
synchronize everyone."
- ),
+ headline,
+ detail,
color: NEON_YELLOW,
})
}
"ReplicaStateChanged" => {
let replica_id = event.replica_id.unwrap_or(0) as u8;
- let name = dog_name(replica_id);
+ let name = vocab.node_name(replica_id);
let new_status = event
.status
.as_deref()
@@ -456,9 +593,51 @@ pub(crate) fn narrate_event(
.as_deref()
.map(Role::from_str)
.unwrap_or(Role::Backup);
- let role_str = match role {
- Role::Primary => "lead dog",
- Role::Backup => "pack dog",
+ let (headline, detail) = match vocab {
+ VocabMode::Dog => {
+ let role_str = match role {
+ Role::Primary => "lead dog",
+ Role::Backup => "pack dog",
+ };
+ (
+ format!("{name} ({role_str}): -> {new_status:?}"),
+ match new_status {
+ Status::Normal => {
+ format!("{name} is back to running! The pack
is stable again.")
+ }
+ Status::ViewChange => {
+ format!("{name} senses trouble -- howling for
a new leader!")
+ }
+ Status::Recovering => format!(
+ "{name} stumbled and is now recovering --
needs to catch up with the pack."
+ ),
+ },
+ )
+ }
+ VocabMode::Technical => {
+ let role_str = match role {
+ Role::Primary => "primary",
+ Role::Backup => "backup",
+ };
+ (
+ format!("{name} ({role_str}): -> {new_status:?}"),
+ match new_status {
+ Status::Normal => {
+ format!(
+ "{name} (R{replica_id}) transitioned to
Normal. Cluster stable."
+ )
+ }
+ Status::ViewChange => {
+ format!("{name} (R{replica_id}) entered
ViewChange state.")
+ }
+ Status::Recovering => {
+ format!(
+ "{name} (R{replica_id}) entered Recovering
state. Awaiting state transfer."
+ )
+ }
+ },
+ )
+ }
};
Some(EventLogEntry {
tick,
@@ -467,18 +646,8 @@ pub(crate) fn narrate_event(
Status::ViewChange => "[HWL]",
Status::Recovering => "[LMP]",
},
- headline: format!("{name} ({role_str}): -> {new_status:?}"),
- detail: match new_status {
- Status::Normal => {
- format!("{name} is back to running! The pack is stable
again.")
- }
- Status::ViewChange => {
- format!("{name} senses trouble -- howling for a new
leader!")
- }
- Status::Recovering => format!(
- "{name} stumbled and is now recovering -- needs to
catch up with the pack."
- ),
- },
+ headline,
+ detail,
color: match new_status {
Status::Normal => NEON_CYAN,
Status::ViewChange => NEON_MAGENTA,
diff --git a/core/simulator_ui/src/input.rs b/core/simulator_ui/src/input.rs
index e786f60a3..b5e5d62f1 100644
--- a/core/simulator_ui/src/input.rs
+++ b/core/simulator_ui/src/input.rs
@@ -23,11 +23,13 @@ use crate::helpers::*;
use crate::queries::FxParams;
use crate::resources::*;
use crate::theme::*;
+use crate::vocabulary::Vocab;
pub(crate) fn handle_selection_input(
keys: Res<ButtonInput<KeyCode>>,
mut state: ResMut<SelectionState>,
mut config: ResMut<ReplicaConfig>,
+ mut vocab: ResMut<Vocab>,
mut next_state: ResMut<NextState<AppPhase>>,
) {
if keys.just_pressed(KeyCode::ArrowLeft) && state.index > 0 {
@@ -47,6 +49,10 @@ pub(crate) fn handle_selection_input(
state.index = 2;
}
+ if keys.just_pressed(KeyCode::Tab) {
+ vocab.mode = vocab.mode.toggle();
+ }
+
if keys.just_pressed(KeyCode::Space) || keys.just_pressed(KeyCode::Enter) {
config.count = PACK_OPTIONS[state.index];
next_state.set(AppPhase::Simulating);
@@ -62,6 +68,7 @@ pub(crate) fn handle_keyboard_input(
mut console: ResMut<GameConsole>,
config: Res<ReplicaConfig>,
positions: Res<ReplicaPositions>,
+ vocab: Res<Vocab>,
mut fx: FxParams,
) {
let replica_count = config.count;
@@ -96,7 +103,7 @@ pub(crate) fn handle_keyboard_input(
if !state.alive {
continue;
}
- let name = dog_name(state.id);
+ let name = vocab.mode.node_name(state.id);
sim.simulator.kill_replica(state.id);
fx.screen_flash.timer = SCREEN_FLASH_DURATION;
fx.screen_flash.color = NEON_MAGENTA;
@@ -105,18 +112,15 @@ pub(crate) fn handle_keyboard_input(
trigger_replica_callout(
&mut fx.replica_fx,
state.id,
- "GREYHOUND DOWN",
+ vocab.mode.callout_node_down(),
NEON_MAGENTA,
1.4,
);
event_log.push(EventLogEntry {
tick: sim.simulator.tick,
icon: "[KIL]",
- headline: format!("{name} (R{}) was tripped!", state.id),
- detail: format!(
- "{name} crashed! Network links disabled. The pack loses a
member -- \
- if quorum is lost, no new operations can commit."
- ),
+ headline: vocab.mode.kill_headline(name, state.id),
+ detail: vocab.mode.kill_detail(name),
color: NEON_MAGENTA,
});
let center = positions.0[state.id as usize];
@@ -143,7 +147,13 @@ pub(crate) fn handle_keyboard_input(
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);
+ trigger_replica_callout(
+ &mut fx.replica_fx,
+ replica_id,
+ vocab.mode.callout_healed(),
+ NEON_CYAN,
+ 1.2,
+ );
let center = positions.0[replica_id as usize];
commands.spawn((
CommitRing {
@@ -160,10 +170,8 @@ pub(crate) fn handle_keyboard_input(
event_log.push(EventLogEntry {
tick: sim.simulator.tick,
icon: "[HEL]",
- headline: "Pack healed! All dogs back, all fences
down.".to_string(),
- detail: "All crashed replicas re-enabled, all network partitions
cleared. \
- The pack is whole again."
- .to_string(),
+ headline: vocab.mode.heal_headline().to_string(),
+ detail: vocab.mode.heal_detail().to_string(),
color: NEON_CYAN,
});
}
@@ -177,13 +185,25 @@ pub(crate) fn handle_keyboard_input(
let first = alive[0];
let second = alive[1];
sim.simulator.partition_link(first, second);
- let name_first = dog_name(first);
- let name_second = dog_name(second);
+ let name_first = vocab.mode.node_name(first);
+ let name_second = vocab.mode.node_name(second);
fx.screen_flash.timer = SCREEN_FLASH_DURATION * 0.5;
fx.screen_flash.color = NEON_MAGENTA;
fx.screen_flash.intensity = 0.04;
- trigger_replica_callout(&mut fx.replica_fx, first, "FENCED!",
NEON_MAGENTA, 1.3);
- trigger_replica_callout(&mut fx.replica_fx, second, "FENCED!",
NEON_MAGENTA, 1.3);
+ trigger_replica_callout(
+ &mut fx.replica_fx,
+ first,
+ vocab.mode.callout_fenced(),
+ NEON_MAGENTA,
+ 1.3,
+ );
+ trigger_replica_callout(
+ &mut fx.replica_fx,
+ second,
+ vocab.mode.callout_fenced(),
+ NEON_MAGENTA,
+ 1.3,
+ );
for &replica_id in &[first, second] {
let center = positions.0[replica_id as usize];
commands.spawn((
@@ -201,12 +221,10 @@ pub(crate) fn handle_keyboard_input(
event_log.push(EventLogEntry {
tick: sim.simulator.tick,
icon: "[FNC]",
- headline: format!("Fence between {name_first} and
{name_second}!"),
- detail: format!(
- "Network partition between {name_first} (R{first}) and
{name_second} (R{second}). \
- Messages between them will be dropped. If this splits
quorum, \
- expect a view change."
- ),
+ headline: vocab.mode.partition_headline(name_first,
name_second),
+ detail: vocab
+ .mode
+ .partition_detail(name_first, first, name_second, second),
color: NEON_MAGENTA,
});
}
diff --git a/core/simulator_ui/src/main.rs b/core/simulator_ui/src/main.rs
index 4104cdbd7..039bf109b 100644
--- a/core/simulator_ui/src/main.rs
+++ b/core/simulator_ui/src/main.rs
@@ -29,6 +29,7 @@ mod tracing_layer;
mod types;
mod util;
mod visuals;
+mod vocabulary;
use bevy::asset::AssetPlugin;
use bevy::prelude::*;
@@ -38,6 +39,7 @@ use bridge::UiSimulator;
use resources::*;
use theme::*;
use tracing_layer::EventBuffer;
+use vocabulary::Vocab;
#[derive(States, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub(crate) enum AppPhase {
@@ -123,6 +125,7 @@ fn main() {
count: DEFAULT_REPLICA_COUNT,
})
.insert_resource(SelectionState::default())
+ .insert_resource(Vocab::default())
.insert_resource(SharedBuffers { event_buffer })
.insert_resource(ReplicaPositions::default())
.insert_resource(ReplicaFxState::default())
diff --git a/core/simulator_ui/src/setup.rs b/core/simulator_ui/src/setup.rs
index fbcf96290..acc663a5f 100644
--- a/core/simulator_ui/src/setup.rs
+++ b/core/simulator_ui/src/setup.rs
@@ -25,6 +25,7 @@ use crate::components::*;
use crate::helpers::*;
use crate::resources::*;
use crate::theme::*;
+use crate::vocabulary::{Vocab, VocabMode};
pub(crate) fn setup_camera(mut commands: Commands) {
commands.spawn(Camera2d);
@@ -251,13 +252,56 @@ pub(crate) fn setup_selection_screen(mut commands:
Commands, asset_server: Res<A
flex_direction: FlexDirection::Row,
column_gap: Val::Px(20.0),
align_items: AlignItems::Center,
- margin: UiRect::top(Val::Px(12.0)),
+ margin: UiRect::top(Val::Px(8.0)),
+ ..default()
+ })
+ .with_children(|row| {
+ row.spawn((
+ Text::new("STYLE"),
+ TextFont {
+ font_size: 11.0,
+ ..default()
+ },
+ TextColor(DIM_GRAY),
+ ));
+
+ for (is_dog, label) in [(true, "FUN"), (false, "TECHNICAL")] {
+ row.spawn((
+ VocabCard { is_dog },
+ Node {
+ padding: UiRect::axes(Val::Px(14.0), Val::Px(6.0)),
+ border: UiRect::all(Val::Px(1.0)),
+ ..default()
+ },
+ BackgroundColor(Color::srgba_u8(0x0c, 0x14, 0x24,
0xcc)),
+ BorderColor::all(DIM_GRAY.with_alpha(0.2)),
+ ))
+ .with_children(|card| {
+ card.spawn((
+ VocabLabel { is_dog },
+ Text::new(label),
+ TextFont {
+ font_size: 14.0,
+ ..default()
+ },
+ TextColor(DIM_GRAY.with_alpha(0.4)),
+ ));
+ });
+ }
+ });
+
+ root.spawn(Node {
+ flex_direction: FlexDirection::Row,
+ column_gap: Val::Px(20.0),
+ align_items: AlignItems::Center,
+ margin: UiRect::top(Val::Px(10.0)),
..default()
})
.with_children(|hint| {
for (key, action, color) in [
- ("< >", "select", NEON_CYAN),
- ("SPACE", "unleash the pack!", IGGY_ORANGE),
+ ("< >", "select pack", NEON_CYAN),
+ ("TAB", "switch style", NEON_YELLOW),
+ ("SPACE", "start!", IGGY_ORANGE),
] {
hint.spawn(Node {
flex_direction: FlexDirection::Row,
@@ -279,7 +323,7 @@ pub(crate) fn setup_selection_screen(mut commands:
Commands, asset_server: Res<A
badge.spawn((
Text::new(key),
TextFont {
- font_size: 15.0,
+ font_size: 14.0,
..default()
},
TextColor(color),
@@ -299,29 +343,56 @@ pub(crate) fn setup_selection_screen(mut commands:
Commands, asset_server: Res<A
});
}
-#[allow(clippy::type_complexity)]
+#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub(crate) fn update_selection_screen(
time: Res<Time>,
state: Res<SelectionState>,
+ vocab: Res<Vocab>,
mut cards: Query<
(&SelectionCard, &mut BorderColor, &mut BackgroundColor),
- Without<SelectionNumber>,
+ (Without<SelectionNumber>, Without<VocabCard>),
>,
mut numbers: Query<
(&SelectionNumber, &mut TextColor),
- (Without<SelectionCard>, Without<SelectionLabel>),
+ (
+ Without<SelectionCard>,
+ Without<SelectionLabel>,
+ Without<SelectionTitle>,
+ Without<VocabLabel>,
+ ),
>,
mut labels: Query<
- (&SelectionLabel, &mut TextColor),
- (Without<SelectionCard>, Without<SelectionNumber>),
+ (&SelectionLabel, &mut Text, &mut TextColor),
+ (
+ Without<SelectionCard>,
+ Without<SelectionNumber>,
+ Without<SelectionTitle>,
+ Without<VocabLabel>,
+ ),
>,
mut title: Query<
- &mut TextColor,
+ (&mut Text, &mut TextColor),
(
With<SelectionTitle>,
Without<SelectionCard>,
Without<SelectionNumber>,
Without<SelectionLabel>,
+ Without<VocabCard>,
+ Without<VocabLabel>,
+ ),
+ >,
+ mut vocab_cards: Query<
+ (&VocabCard, &mut BorderColor, &mut BackgroundColor),
+ (Without<SelectionCard>, Without<VocabLabel>),
+ >,
+ mut vocab_labels: Query<
+ (&VocabLabel, &mut TextColor),
+ (
+ Without<SelectionCard>,
+ Without<SelectionLabel>,
+ Without<SelectionNumber>,
+ Without<SelectionTitle>,
+ Without<VocabCard>,
),
>,
) {
@@ -329,8 +400,10 @@ pub(crate) fn update_selection_screen(
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;
+ let is_dog = vocab.mode == VocabMode::Dog;
- for mut color in title.iter_mut() {
+ for (mut text, mut color) in title.iter_mut() {
+ **text = vocab.mode.choose_label().to_string();
*color = TextColor(NEON_CYAN.with_alpha(0.7 + slow_pulse * 0.3));
}
@@ -354,13 +427,34 @@ pub(crate) fn update_selection_screen(
}
}
- for (label, mut color) in labels.iter_mut() {
+ for (label, mut text, mut color) in labels.iter_mut() {
+ **text = vocab.mode.pack_option_name(label.index).to_string();
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));
}
}
+
+ for (vc, mut border, mut bg) in vocab_cards.iter_mut() {
+ let selected = vc.is_dog == is_dog;
+ if selected {
+ *border = BorderColor::all(NEON_YELLOW.with_alpha(0.6 + fast_pulse
* 0.4));
+ *bg = BackgroundColor(NEON_YELLOW.with_alpha(0.06 + pulse * 0.04));
+ } else {
+ *border = BorderColor::all(Color::srgba_u8(0x22, 0x2a, 0x3a,
0x44));
+ *bg = BackgroundColor(Color::srgba_u8(0x08, 0x0e, 0x1a, 0x88));
+ }
+ }
+
+ for (vl, mut color) in vocab_labels.iter_mut() {
+ let selected = vl.is_dog == is_dog;
+ if selected {
+ *color = TextColor(NEON_YELLOW.with_alpha(0.85 + fast_pulse *
0.15));
+ } else {
+ *color = TextColor(DIM_GRAY.with_alpha(0.25));
+ }
+ }
}
pub(crate) fn despawn_selection_screen(
@@ -399,6 +493,7 @@ pub(crate) fn setup_simulation_world(
config: Res<ReplicaConfig>,
positions: Res<ReplicaPositions>,
asset_server: Res<AssetServer>,
+ vocab: Res<Vocab>,
) {
let replica_count = config.count;
let mascot_handle: Handle<Image> = asset_server.load("iggy.png");
@@ -561,7 +656,7 @@ pub(crate) fn setup_simulation_world(
let apps = [
(
0usize,
- "BALL THROWER",
+ vocab.mode.app_name(0),
AppKind::Producer,
Vec2::new(-560.0, 330.0),
0u8,
@@ -570,7 +665,7 @@ pub(crate) fn setup_simulation_world(
),
(
1usize,
- "BALL FETCHER",
+ vocab.mode.app_name(1),
AppKind::Consumer,
Vec2::new(560.0, 330.0),
1u8.min(replica_count - 1),
@@ -579,7 +674,7 @@ pub(crate) fn setup_simulation_world(
),
(
2usize,
- "TREAT DISPENSER",
+ vocab.mode.app_name(2),
AppKind::Producer,
Vec2::new(-560.0, 70.0),
2u8.min(replica_count - 1),
@@ -588,7 +683,7 @@ pub(crate) fn setup_simulation_world(
),
(
3usize,
- "TREAT GOBBLER",
+ vocab.mode.app_name(3),
AppKind::Consumer,
Vec2::new(560.0, 70.0),
0u8,
@@ -646,13 +741,15 @@ pub(crate) fn setup_hud(
mut commands: Commands,
asset_server: Res<AssetServer>,
config: Res<ReplicaConfig>,
+ vocab: Res<Vocab>,
) {
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);
+ let v = vocab.mode;
+ let node_unit_label = format!("{} {}", config.count, v.node_unit());
commands
.spawn(Node {
@@ -684,7 +781,7 @@ pub(crate) fn setup_hud(
})
.with_children(|left| {
left.spawn((
- Text::new("iggy::pack"),
+ Text::new(v.hud_title()),
TextFont {
font_size: 16.0,
..default()
@@ -699,7 +796,7 @@ pub(crate) fn setup_hud(
})
.with_children(|stat| {
stat.spawn((
- Text::new("TREATS"),
+ Text::new(v.hud_commits_label()),
TextFont {
font_size: 9.0,
..default()
@@ -724,7 +821,7 @@ pub(crate) fn setup_hud(
})
.with_children(|stat| {
stat.spawn((
- Text::new("LAPS/S"),
+ Text::new(v.hud_ops_label()),
TextFont {
font_size: 9.0,
..default()
@@ -793,7 +890,7 @@ pub(crate) fn setup_hud(
.with_children(|col| {
col.spawn((
HudStateText,
- Text::new("RACING"),
+ Text::new(v.hud_playing()),
TextFont {
font_size: 16.0,
..default()
@@ -836,7 +933,7 @@ pub(crate) fn setup_hud(
})
.with_children(|col| {
col.spawn((
- Text::new("APACHE IGGY SIMULATOR"),
+ Text::new("APACHE IGGY VSR SIMULATOR"),
TextFont {
font_size: 16.0,
..default()
@@ -844,7 +941,7 @@ pub(crate) fn setup_hud(
TextColor(COOL_WHITE),
));
col.spawn((
- Text::new("Italian greyhounds racing to consensus"),
+ Text::new(v.subtitle()),
TextFont {
font_size: 12.0,
..default()
@@ -861,9 +958,9 @@ pub(crate) fn setup_hud(
})
.with_children(|keys| {
for (key, action, color) in [
- ("SPACE", "race/rest", NEON_CYAN),
- ("R", "throw ball", IGGY_ORANGE),
- ("K", "trip dog", HUD_ALERT),
+ ("SPACE", v.action_toggle(), NEON_CYAN),
+ ("R", v.action_inject(), IGGY_ORANGE),
+ ("K", v.action_kill(), HUD_ALERT),
("P", "partition", NEON_MAGENTA),
("H", "heal all", NEON_CYAN),
("UP/DN", "speed", NEON_YELLOW),
@@ -916,7 +1013,7 @@ pub(crate) fn setup_hud(
})
.with_children(|chips| {
for (label, color) in [
- (greyhound_label.as_str(), IGGY_ORANGE),
+ (node_unit_label.as_str(), IGGY_ORANGE),
("FAULT READY", HUD_ALERT),
] {
chips
@@ -960,7 +1057,7 @@ pub(crate) fn setup_hud(
.with_children(|col| {
col.spawn((
PauseMainText,
- Text::new("RESTING"),
+ Text::new(v.pause_title()),
TextFont {
font_size: 52.0,
..default()
@@ -969,7 +1066,7 @@ pub(crate) fn setup_hud(
));
col.spawn((
PauseSubtext,
- Text::new("press SPACE to unleash"),
+ Text::new(v.pause_hint()),
TextFont {
font_size: 16.0,
..default()
@@ -997,7 +1094,7 @@ pub(crate) fn setup_hud(
))
.with_children(|panel| {
panel.spawn((
- Text::new("PACK ACTIVITY LOG"),
+ Text::new(v.event_log_title()),
TextFont {
font_size: 16.0,
..default()
diff --git a/core/simulator_ui/src/simulation.rs
b/core/simulator_ui/src/simulation.rs
index d8f102d5c..87bc4a986 100644
--- a/core/simulator_ui/src/simulation.rs
+++ b/core/simulator_ui/src/simulation.rs
@@ -27,7 +27,9 @@ use crate::resources::*;
use crate::theme::*;
use crate::tracing_layer::CapturedSimEvent;
use crate::types::{MessageType, Status};
+use crate::vocabulary::{Vocab, VocabMode};
+#[allow(clippy::too_many_arguments)]
pub(crate) fn tick_simulation(
mut commands: Commands,
time: Res<Time>,
@@ -36,6 +38,7 @@ pub(crate) fn tick_simulation(
positions: Res<ReplicaPositions>,
mut fx: FxParams,
mut event_log: ResMut<EventLog>,
+ vocab: Res<Vocab>,
) {
let elapsed_secs = time.elapsed_secs_f64();
if elapsed_secs - sim.ops_window_start >= 1.0 {
@@ -60,7 +63,7 @@ pub(crate) fn tick_simulation(
let events = sim.simulator.step();
for event in &events {
- if let Some(entry) = narrate_event(event, tick, replica_count) {
+ if let Some(entry) = narrate_event(event, tick, replica_count,
vocab.mode) {
event_log.push(entry);
}
match event.sim_event.as_str() {
@@ -84,6 +87,7 @@ pub(crate) fn tick_simulation(
&mut fx.replica_fx,
&mut fx.app_fx,
&mut fx.track_pulse,
+ vocab.mode,
);
}
"ViewChangeStarted" => {
@@ -96,6 +100,7 @@ pub(crate) fn tick_simulation(
&mut fx.app_fx,
&mut fx.screen_flash,
replica_count,
+ vocab.mode,
);
}
"ClientRequestReceived" => {
@@ -109,6 +114,7 @@ pub(crate) fn tick_simulation(
&positions,
&mut fx.replica_fx,
&mut fx.screen_flash,
+ vocab.mode,
);
}
"ReplicaStateChanged" => {
@@ -119,9 +125,9 @@ pub(crate) fn tick_simulation(
.map(Status::from_str)
.unwrap_or(Status::Normal);
let (label, color, duration) = match new_status {
- Status::Normal => ("RUNNING!", NEON_CYAN, 0.85),
- Status::ViewChange => ("HOWLING!", NEON_MAGENTA, 1.1),
- Status::Recovering => ("LIMPING...", NEON_YELLOW, 1.1),
+ Status::Normal => (vocab.mode.callout_normal(),
NEON_CYAN, 0.85),
+ Status::ViewChange =>
(vocab.mode.callout_view_change(), NEON_MAGENTA, 1.1),
+ Status::Recovering =>
(vocab.mode.callout_recovering(), NEON_YELLOW, 1.1),
};
trigger_replica_callout(&mut fx.replica_fx, replica_id,
label, color, duration);
}
@@ -239,6 +245,7 @@ pub(crate) fn spawn_prepare_particles(
}
}
+#[allow(clippy::too_many_arguments)]
pub(crate) fn handle_commit_event(
commands: &mut Commands,
event: &CapturedSimEvent,
@@ -247,12 +254,19 @@ pub(crate) fn handle_commit_event(
replica_fx: &mut ResMut<ReplicaFxState>,
app_fx: &mut ResMut<AppFxState>,
track_pulse: &mut ResMut<TrackPulse>,
+ vocab: VocabMode,
) {
let replica_id = event.replica_id.unwrap_or(0) as u8;
sim.total_commits += 1;
sim.ops_window_count += 1;
replica_fx.healthy[replica_id as usize] = 0.45;
- trigger_replica_callout(replica_fx, replica_id, "GOOD BOY!", IGGY_ORANGE,
0.9);
+ trigger_replica_callout(
+ replica_fx,
+ replica_id,
+ vocab.callout_committed(),
+ IGGY_ORANGE,
+ 0.9,
+ );
app_fx.pulse[2] = 0.55;
track_pulse.energy = (track_pulse.energy + 0.4).min(1.0);
commands.spawn(GlowFlash {
@@ -364,9 +378,16 @@ pub(crate) fn handle_view_change_event(
app_fx: &mut ResMut<AppFxState>,
screen_flash: &mut ResMut<ScreenFlash>,
replica_count: u8,
+ vocab: VocabMode,
) {
let replica_id = event.replica_id.unwrap_or(0) as u8;
- trigger_replica_callout(replica_fx, replica_id, "PACK SHUFFLE!",
NEON_MAGENTA, 1.15);
+ trigger_replica_callout(
+ replica_fx,
+ replica_id,
+ vocab.callout_view_change_started(),
+ NEON_MAGENTA,
+ 1.15,
+ );
app_fx.pulse[3] = 0.8;
screen_flash.timer = SCREEN_FLASH_DURATION;
screen_flash.color = NEON_MAGENTA;
@@ -435,9 +456,16 @@ pub(crate) fn handle_primary_elected_event(
positions: &ReplicaPositions,
replica_fx: &mut ResMut<ReplicaFxState>,
screen_flash: &mut ResMut<ScreenFlash>,
+ vocab: VocabMode,
) {
let replica_id = event.replica_id.unwrap_or(0) as u8;
- trigger_replica_callout(replica_fx, replica_id, "NEW LEAD DOG!",
NEON_YELLOW, 1.2);
+ trigger_replica_callout(
+ replica_fx,
+ replica_id,
+ vocab.callout_primary_elected(),
+ NEON_YELLOW,
+ 1.2,
+ );
screen_flash.timer = SCREEN_FLASH_DURATION * 0.7;
screen_flash.color = NEON_YELLOW;
screen_flash.intensity = 0.08;
diff --git a/core/simulator_ui/src/visuals.rs b/core/simulator_ui/src/visuals.rs
index 87129fdfe..1091a1e18 100644
--- a/core/simulator_ui/src/visuals.rs
+++ b/core/simulator_ui/src/visuals.rs
@@ -26,16 +26,18 @@ use crate::queries::*;
use crate::resources::*;
use crate::theme::*;
use crate::types::{Role, Status};
+use crate::vocabulary::Vocab;
#[allow(clippy::type_complexity)]
pub(crate) fn update_replica_visuals(
- (time, sim, config, positions, mut replica_fx, mut commands): (
+ (time, sim, config, positions, mut replica_fx, mut commands, vocab): (
Res<Time>,
NonSend<SimulationState>,
Res<ReplicaConfig>,
Res<ReplicaPositions>,
ResMut<ReplicaFxState>,
Commands,
+ Res<Vocab>,
),
mut flash_query: Query<(Entity, &mut GlowFlash)>,
mut shape_visuals: ParamSet<(
@@ -91,10 +93,22 @@ pub(crate) fn update_replica_visuals(
if state.alive {
replica_fx.revive[idx] = 1.0;
replica_fx.healthy[idx] = 0.6;
- trigger_replica_callout(&mut replica_fx, id, "BACK ON TRACK!",
IGGY_ORANGE, 1.3);
+ trigger_replica_callout(
+ &mut replica_fx,
+ id,
+ vocab.mode.callout_recovered(),
+ IGGY_ORANGE,
+ 1.3,
+ );
} else {
replica_fx.kill[idx] = 1.0;
- trigger_replica_callout(&mut replica_fx, id, "GREYHOUND DOWN",
NEON_MAGENTA, 1.4);
+ trigger_replica_callout(
+ &mut replica_fx,
+ id,
+ vocab.mode.callout_node_down(),
+ NEON_MAGENTA,
+ 1.4,
+ );
}
}
@@ -103,14 +117,32 @@ pub(crate) fn update_replica_visuals(
match state.status {
Status::Normal => {
replica_fx.healthy[idx] = 0.5;
- trigger_replica_callout(&mut replica_fx, id, "RUNNING!",
NEON_CYAN, 0.8);
+ trigger_replica_callout(
+ &mut replica_fx,
+ id,
+ vocab.mode.callout_normal(),
+ NEON_CYAN,
+ 0.8,
+ );
}
Status::Recovering => {
replica_fx.revive[idx] = 0.8;
- trigger_replica_callout(&mut replica_fx, id, "LIMPING...",
NEON_YELLOW, 1.1);
+ trigger_replica_callout(
+ &mut replica_fx,
+ id,
+ vocab.mode.callout_recovering(),
+ NEON_YELLOW,
+ 1.1,
+ );
}
Status::ViewChange => {
- trigger_replica_callout(&mut replica_fx, id, "HOWLING!",
NEON_MAGENTA, 1.1);
+ trigger_replica_callout(
+ &mut replica_fx,
+ id,
+ vocab.mode.callout_view_change(),
+ NEON_MAGENTA,
+ 1.1,
+ );
}
}
}
@@ -275,9 +307,9 @@ pub(crate) fn update_replica_visuals(
continue;
}
let label = if !state.alive {
- "NAPPING"
+ vocab.mode.callout_crashed()
} else {
- role_label(state.role, state.status)
+ vocab.mode.role_label(state.role, state.status)
};
**text = label.to_string();
let status_pulse = ((elapsed * 4.2 + id as f32 * 0.7).sin() * 0.5)
+ 0.5;
@@ -303,12 +335,12 @@ pub(crate) fn update_replica_visuals(
let timer = replica_fx.callout_timer[idx];
let persistent = if !state.alive {
- Some(("NAPPING", NEON_MAGENTA))
+ Some((vocab.mode.callout_crashed(), NEON_MAGENTA))
} else {
match state.status {
Status::Normal => None,
- Status::ViewChange => Some(("HOWLING!", NEON_MAGENTA)),
- Status::Recovering => Some(("LIMPING...", NEON_YELLOW)),
+ Status::ViewChange =>
Some((vocab.mode.callout_view_change(), NEON_MAGENTA)),
+ Status::Recovering =>
Some((vocab.mode.callout_recovering(), NEON_YELLOW)),
}
};
@@ -588,6 +620,7 @@ pub(crate) fn update_app_flow_particles(
pub(crate) fn update_hud_text(
sim: NonSend<SimulationState>,
+ vocab: Res<Vocab>,
mut hud: ParamSet<(
HudTickQuery,
HudOpsQuery,
@@ -610,10 +643,10 @@ pub(crate) fn update_hud_text(
for (mut text, mut color) in hud.p3().iter_mut() {
if sim.playing {
- **text = "RACING".to_string();
+ **text = vocab.mode.hud_playing().to_string();
*color = TextColor(NEON_CYAN);
} else {
- **text = "RESTING".to_string();
+ **text = vocab.mode.hud_paused().to_string();
*color = TextColor(NEON_YELLOW);
}
}
diff --git a/core/simulator_ui/src/vocabulary.rs
b/core/simulator_ui/src/vocabulary.rs
new file mode 100644
index 000000000..f6b17e0eb
--- /dev/null
+++ b/core/simulator_ui/src/vocabulary.rs
@@ -0,0 +1,392 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use bevy::prelude::*;
+
+use crate::types::{Role, Status};
+
+#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
+pub(crate) enum VocabMode {
+ #[default]
+ Dog,
+ Technical,
+}
+
+#[derive(Resource)]
+pub(crate) struct Vocab {
+ pub(crate) mode: VocabMode,
+}
+
+impl Default for Vocab {
+ fn default() -> Self {
+ Self {
+ mode: VocabMode::Dog,
+ }
+ }
+}
+
+#[allow(dead_code)]
+impl VocabMode {
+ pub(crate) fn node_name(self, id: u8) -> &'static str {
+ match self {
+ Self::Dog => match id {
+ 0 => "Iggy",
+ 1 => "Zippy",
+ 2 => "Dash",
+ 3 => "Bolt",
+ 4 => "Flash",
+ 5 => "Storm",
+ 6 => "Blaze",
+ _ => "Pup",
+ },
+ Self::Technical => match id {
+ 0 => "R0",
+ 1 => "R1",
+ 2 => "R2",
+ 3 => "R3",
+ 4 => "R4",
+ 5 => "R5",
+ 6 => "R6",
+ _ => "RN",
+ },
+ }
+ }
+
+ pub(crate) fn primary(self) -> &'static str {
+ match self {
+ Self::Dog => "LEAD DOG",
+ Self::Technical => "PRIMARY",
+ }
+ }
+
+ pub(crate) fn backup(self) -> &'static str {
+ match self {
+ Self::Dog => "PACK DOG",
+ Self::Technical => "BACKUP",
+ }
+ }
+
+ pub(crate) fn role_label(self, role: Role, status: Status) -> &'static str
{
+ match self {
+ Self::Dog => match (role, status) {
+ (Role::Primary, Status::Normal) => "LEAD DOG / running",
+ (Role::Primary, Status::ViewChange) => "LEAD DOG / howling",
+ (Role::Primary, Status::Recovering) => "LEAD DOG / limping",
+ (Role::Backup, Status::Normal) => "PACK DOG / running",
+ (Role::Backup, Status::ViewChange) => "PACK DOG / howling",
+ (Role::Backup, Status::Recovering) => "PACK DOG / limping",
+ },
+ Self::Technical => match (role, status) {
+ (Role::Primary, Status::Normal) => "PRIMARY / normal",
+ (Role::Primary, Status::ViewChange) => "PRIMARY / view-change",
+ (Role::Primary, Status::Recovering) => "PRIMARY / recovering",
+ (Role::Backup, Status::Normal) => "BACKUP / normal",
+ (Role::Backup, Status::ViewChange) => "BACKUP / view-change",
+ (Role::Backup, Status::Recovering) => "BACKUP / recovering",
+ },
+ }
+ }
+
+ pub(crate) fn callout_normal(self) -> &'static str {
+ match self {
+ Self::Dog => "RUNNING!",
+ Self::Technical => "NORMAL",
+ }
+ }
+
+ pub(crate) fn callout_view_change(self) -> &'static str {
+ match self {
+ Self::Dog => "HOWLING!",
+ Self::Technical => "VIEW-CHANGE",
+ }
+ }
+
+ pub(crate) fn callout_recovering(self) -> &'static str {
+ match self {
+ Self::Dog => "LIMPING...",
+ Self::Technical => "RECOVERING",
+ }
+ }
+
+ pub(crate) fn callout_crashed(self) -> &'static str {
+ match self {
+ Self::Dog => "NAPPING",
+ Self::Technical => "CRASHED",
+ }
+ }
+
+ pub(crate) fn callout_committed(self) -> &'static str {
+ match self {
+ Self::Dog => "GOOD BOY!",
+ Self::Technical => "COMMITTED",
+ }
+ }
+
+ pub(crate) fn callout_node_down(self) -> &'static str {
+ match self {
+ Self::Dog => "GREYHOUND DOWN",
+ Self::Technical => "NODE CRASHED",
+ }
+ }
+
+ pub(crate) fn callout_view_change_started(self) -> &'static str {
+ match self {
+ Self::Dog => "PACK SHUFFLE!",
+ Self::Technical => "VIEW CHANGE",
+ }
+ }
+
+ pub(crate) fn callout_primary_elected(self) -> &'static str {
+ match self {
+ Self::Dog => "NEW LEAD DOG!",
+ Self::Technical => "PRIMARY ELECTED",
+ }
+ }
+
+ pub(crate) fn callout_recovered(self) -> &'static str {
+ match self {
+ Self::Dog => "BACK ON TRACK!",
+ Self::Technical => "NODE RECOVERED",
+ }
+ }
+
+ pub(crate) fn callout_healed(self) -> &'static str {
+ match self {
+ Self::Dog => "HEALED!",
+ Self::Technical => "RECOVERED",
+ }
+ }
+
+ pub(crate) fn callout_fenced(self) -> &'static str {
+ match self {
+ Self::Dog => "FENCED!",
+ Self::Technical => "PARTITIONED",
+ }
+ }
+
+ pub(crate) fn hud_title(self) -> &'static str {
+ match self {
+ Self::Dog => "iggy::pack",
+ Self::Technical => "vsr::cluster",
+ }
+ }
+
+ pub(crate) fn hud_commits_label(self) -> &'static str {
+ match self {
+ Self::Dog => "TREATS",
+ Self::Technical => "COMMITS",
+ }
+ }
+
+ pub(crate) fn hud_ops_label(self) -> &'static str {
+ match self {
+ Self::Dog => "LAPS/S",
+ Self::Technical => "OPS/S",
+ }
+ }
+
+ pub(crate) fn hud_playing(self) -> &'static str {
+ match self {
+ Self::Dog => "RACING",
+ Self::Technical => "RUNNING",
+ }
+ }
+
+ pub(crate) fn hud_paused(self) -> &'static str {
+ match self {
+ Self::Dog => "RESTING",
+ Self::Technical => "PAUSED",
+ }
+ }
+
+ pub(crate) fn node_unit(self) -> &'static str {
+ match self {
+ Self::Dog => "GREYHOUNDS",
+ Self::Technical => "REPLICAS",
+ }
+ }
+
+ pub(crate) fn action_toggle(self) -> &'static str {
+ match self {
+ Self::Dog => "race/rest",
+ Self::Technical => "start/pause",
+ }
+ }
+
+ pub(crate) fn action_inject(self) -> &'static str {
+ match self {
+ Self::Dog => "throw ball",
+ Self::Technical => "inject request",
+ }
+ }
+
+ pub(crate) fn action_kill(self) -> &'static str {
+ match self {
+ Self::Dog => "trip dog",
+ Self::Technical => "crash node",
+ }
+ }
+
+ pub(crate) fn app_name(self, id: usize) -> &'static str {
+ match self {
+ Self::Dog => match id {
+ 0 => "BALL THROWER",
+ 1 => "BALL FETCHER",
+ 2 => "TREAT DISPENSER",
+ _ => "TREAT GOBBLER",
+ },
+ Self::Technical => match id {
+ 0 => "PRODUCER A",
+ 1 => "CONSUMER A",
+ 2 => "PRODUCER B",
+ _ => "CONSUMER B",
+ },
+ }
+ }
+
+ pub(crate) fn event_log_title(self) -> &'static str {
+ match self {
+ Self::Dog => "PACK ACTIVITY LOG",
+ Self::Technical => "EVENT LOG",
+ }
+ }
+
+ pub(crate) fn pause_title(self) -> &'static str {
+ match self {
+ Self::Dog => "RESTING",
+ Self::Technical => "PAUSED",
+ }
+ }
+
+ pub(crate) fn pause_hint(self) -> &'static str {
+ match self {
+ Self::Dog => "press SPACE to unleash",
+ Self::Technical => "press SPACE to start",
+ }
+ }
+
+ pub(crate) fn subtitle(self) -> &'static str {
+ match self {
+ Self::Dog => "Italian greyhounds racing to consensus",
+ Self::Technical => "Viewstamped Replication consensus protocol",
+ }
+ }
+
+ pub(crate) fn choose_label(self) -> &'static str {
+ match self {
+ Self::Dog => "CHOOSE YOUR PACK",
+ Self::Technical => "SELECT REPLICA COUNT",
+ }
+ }
+
+ pub(crate) fn pack_option_name(self, idx: usize) -> &'static str {
+ match self {
+ Self::Dog => match idx {
+ 0 => "TIGHT PACK",
+ 1 => "BALANCED",
+ _ => "FULL PACK",
+ },
+ Self::Technical => match idx {
+ 0 => "MINIMAL",
+ 1 => "BALANCED",
+ _ => "MAXIMUM",
+ },
+ }
+ }
+
+ pub(crate) fn confirm_action(self) -> &'static str {
+ match self {
+ Self::Dog => "unleash the pack!",
+ Self::Technical => "start simulation",
+ }
+ }
+
+ pub(crate) fn kill_headline(self, name: &str, id: u8) -> String {
+ match self {
+ Self::Dog => format!("{name} (R{id}) was tripped!"),
+ Self::Technical => format!("Node R{id} crashed"),
+ }
+ }
+
+ pub(crate) fn kill_detail(self, name: &str) -> String {
+ match self {
+ Self::Dog => format!(
+ "{name} crashed! Network links disabled. The pack loses a
member -- \
+ if quorum is lost, no new operations can commit."
+ ),
+ Self::Technical => format!(
+ "Node {name} crashed. Network links disabled. \
+ If quorum is lost, commits will stall."
+ ),
+ }
+ }
+
+ pub(crate) fn heal_headline(self) -> &'static str {
+ match self {
+ Self::Dog => "Pack healed! All dogs back, all fences down.",
+ Self::Technical => "All nodes recovered. Partitions cleared.",
+ }
+ }
+
+ pub(crate) fn heal_detail(self) -> &'static str {
+ match self {
+ Self::Dog => {
+ "All crashed replicas re-enabled, all network partitions
cleared. \
+ The pack is whole again."
+ }
+ Self::Technical => {
+ "All crashed replicas re-enabled. All network partitions
cleared. \
+ Cluster is fully connected."
+ }
+ }
+ }
+
+ pub(crate) fn partition_headline(self, a: &str, b: &str) -> String {
+ match self {
+ Self::Dog => format!("Fence between {a} and {b}!"),
+ Self::Technical => format!("Network partition: {a} <-> {b}"),
+ }
+ }
+
+ pub(crate) fn partition_detail(self, a: &str, a_id: u8, b: &str, b_id: u8)
-> String {
+ match self {
+ Self::Dog => format!(
+ "Network partition between {a} (R{a_id}) and {b} (R{b_id}). \
+ Messages between them will be dropped. If this splits quorum,
\
+ expect a view change."
+ ),
+ Self::Technical => format!(
+ "Bidirectional partition between {a} (R{a_id}) and {b}
(R{b_id}). \
+ Messages dropped. May trigger view change if quorum is split."
+ ),
+ }
+ }
+
+ pub(crate) fn mode_label(self) -> &'static str {
+ match self {
+ Self::Dog => "FUN",
+ Self::Technical => "TECHNICAL",
+ }
+ }
+
+ pub(crate) fn toggle(self) -> Self {
+ match self {
+ Self::Dog => Self::Technical,
+ Self::Technical => Self::Dog,
+ }
+ }
+}