fix(lint): resolve clippy warnings and add cross-platform lint recipe

- Add `just lint` recipe for desktop + wasm clippy checks
- Fix clippy warnings: redundant field names, formatting, dead code
- Gate Emscripten-specific audio methods behind target_os cfg
This commit is contained in:
2025-12-29 01:15:03 -06:00
parent 6db061cc41
commit 791a0e48e3
7 changed files with 46 additions and 15 deletions
+8
View File
@@ -40,6 +40,14 @@ web *args:
bun run --cwd web build bun run --cwd web build
caddy file-server --root web/dist/client --listen :8547 caddy file-server --root web/dist/client --listen :8547
# Run strict multi-platform lints (desktop + wasm)
lint:
@echo "Running clippy for desktop target..."
@cargo clippy --all-targets --all-features --quiet -- -D warnings
@echo "Running clippy for wasm target..."
@cargo clippy -p pacman --target wasm32-unknown-emscripten --all-features --quiet -- -D warnings
@echo "All lints passed!"
# Fix linting errors & formatting # Fix linting errors & formatting
fix: fix:
cargo fix --workspace --lib --allow-dirty cargo fix --workspace --lib --allow-dirty
+1 -1
View File
@@ -75,7 +75,7 @@ impl AppState {
sessions: Arc::new(DashMap::new()), sessions: Arc::new(DashMap::new()),
jwt_encoding_key: Arc::new(EncodingKey::from_secret(jwt_secret.as_bytes())), jwt_encoding_key: Arc::new(EncodingKey::from_secret(jwt_secret.as_bytes())),
jwt_decoding_key: Arc::new(DecodingKey::from_secret(jwt_secret.as_bytes())), jwt_decoding_key: Arc::new(DecodingKey::from_secret(jwt_secret.as_bytes())),
db: db, db,
health: Arc::new(RwLock::new(Health::default())), health: Arc::new(RwLock::new(Health::default())),
image_storage, image_storage,
healthchecker_task: Arc::new(RwLock::new(None)), healthchecker_task: Arc::new(RwLock::new(None)),
+15 -3
View File
@@ -56,11 +56,18 @@ pub struct Audio {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AudioState { enum AudioState {
Enabled { volume: u8 }, Enabled {
Muted { previous_volume: u8 }, volume: u8,
},
Muted {
previous_volume: u8,
},
/// Audio is suspended until user interaction unlocks it (browser autoplay policy). /// Audio is suspended until user interaction unlocks it (browser autoplay policy).
/// On Emscripten, audio starts in this state and transitions to Enabled when unlock() is called. /// On Emscripten, audio starts in this state and transitions to Enabled when unlock() is called.
Suspended { volume: u8 }, #[cfg(target_os = "emscripten")]
Suspended {
volume: u8,
},
Disabled, Disabled,
} }
@@ -264,6 +271,7 @@ impl Audio {
/// ///
/// Transitions from Suspended to Enabled state, allowing audio to play. /// Transitions from Suspended to Enabled state, allowing audio to play.
/// Called when the user clicks or presses a key to satisfy browser autoplay policy. /// Called when the user clicks or presses a key to satisfy browser autoplay policy.
#[cfg(target_os = "emscripten")]
pub fn unlock(&mut self) { pub fn unlock(&mut self) {
if let AudioState::Suspended { volume } = self.state { if let AudioState::Suspended { volume } = self.state {
tracing::info!("Audio unlocked after user interaction"); tracing::info!("Audio unlocked after user interaction");
@@ -275,11 +283,15 @@ impl Audio {
/// ///
/// Returns `true` if audio is in the Enabled state, `false` if suspended, /// Returns `true` if audio is in the Enabled state, `false` if suspended,
/// muted, or disabled. /// muted, or disabled.
#[cfg(target_os = "emscripten")]
#[allow(dead_code)]
pub fn is_ready(&self) -> bool { pub fn is_ready(&self) -> bool {
matches!(self.state, AudioState::Enabled { .. }) matches!(self.state, AudioState::Enabled { .. })
} }
/// Returns whether audio is suspended waiting for user interaction. /// Returns whether audio is suspended waiting for user interaction.
#[cfg(target_os = "emscripten")]
#[allow(dead_code)]
pub fn is_suspended(&self) -> bool { pub fn is_suspended(&self) -> bool {
matches!(self.state, AudioState::Suspended { .. }) matches!(self.state, AudioState::Suspended { .. })
} }
+5 -2
View File
@@ -22,9 +22,12 @@ use crate::systems::{
EntityType, Frozen, FruitSprites, GameStage, Ghost, GhostAnimation, GhostAnimations, GhostBundle, GhostCollider, GhostState, EntityType, Frozen, FruitSprites, GameStage, Ghost, GhostAnimation, GhostAnimations, GhostBundle, GhostCollider, GhostState,
GlobalState, ItemBundle, ItemCollider, LastAnimationState, LinearAnimation, MapTextureResource, MovementModifiers, NodeId, GlobalState, ItemBundle, ItemCollider, LastAnimationState, LinearAnimation, MapTextureResource, MovementModifiers, NodeId,
PacmanCollider, PlayerAnimation, PlayerBundle, PlayerControlled, PlayerDeathAnimation, PlayerLives, Position, RenderDirty, PacmanCollider, PlayerAnimation, PlayerBundle, PlayerControlled, PlayerDeathAnimation, PlayerLives, Position, RenderDirty,
Renderable, ScoreResource, StartupSequence, SystemId, SystemTimings, Timing, TouchState, Velocity, Visibility, Renderable, ScoreResource, SystemId, SystemTimings, Timing, TouchState, Velocity, Visibility,
}; };
#[cfg(not(target_os = "emscripten"))]
use crate::systems::StartupSequence;
use crate::texture::animated::{DirectionalTiles, TileSequence}; use crate::texture::animated::{DirectionalTiles, TileSequence};
use crate::texture::sprite::AtlasTile; use crate::texture::sprite::AtlasTile;
use crate::texture::sprites::{FrightenedColor, GameSprite, GhostSprite, MazeSprite, PacmanSprite}; use crate::texture::sprites::{FrightenedColor, GameSprite, GhostSprite, MazeSprite, PacmanSprite};
@@ -787,7 +790,7 @@ impl Game {
// Use dt to determine expected frame time, with 80% as threshold to account for normal variance // Use dt to determine expected frame time, with 80% as threshold to account for normal variance
// Desktop uses LOOP_TIME (~16.67ms), WebAssembly adapts to requestAnimationFrame timing // Desktop uses LOOP_TIME (~16.67ms), WebAssembly adapts to requestAnimationFrame timing
let frame_budget_ms = (dt * 1000.0 * 1.2) as u128; let frame_budget_ms = (dt * 1000.0 * 1.2) as u128;
// Log performance warnings for slow frames // Log performance warnings for slow frames
if total_duration.as_millis() > frame_budget_ms { if total_duration.as_millis() > frame_budget_ms {
let slowest_systems = timings.get_slowest_systems(); let slowest_systems = timings.get_slowest_systems();
+1 -6
View File
@@ -23,12 +23,7 @@ extern "C" {
/// - `arg`: user data pointer passed to callback /// - `arg`: user data pointer passed to callback
/// - `fps`: target FPS (0 = use requestAnimationFrame) /// - `fps`: target FPS (0 = use requestAnimationFrame)
/// - `simulate_infinite_loop`: if 1, never returns (standard for games) /// - `simulate_infinite_loop`: if 1, never returns (standard for games)
pub fn emscripten_set_main_loop_arg( pub fn emscripten_set_main_loop_arg(func: EmMainLoopCallback, arg: *mut c_void, fps: c_int, simulate_infinite_loop: c_int);
func: EmMainLoopCallback,
arg: *mut c_void,
fps: c_int,
simulate_infinite_loop: c_int,
);
/// Execute JavaScript code from Rust /// Execute JavaScript code from Rust
fn emscripten_run_script(script: *const i8); fn emscripten_run_script(script: *const i8);
+2 -2
View File
@@ -89,7 +89,7 @@ pub fn hud_render_system(
if pause_state.active() { if pause_state.active() {
// Enable blending for transparency // Enable blending for transparency
canvas.set_blend_mode(sdl2::render::BlendMode::Blend); canvas.set_blend_mode(sdl2::render::BlendMode::Blend);
// Draw semi-transparent black overlay // Draw semi-transparent black overlay
canvas.set_draw_color(Color::RGBA(0, 0, 0, 160)); canvas.set_draw_color(Color::RGBA(0, 0, 0, 160));
let _ = canvas.fill_rect(Rect::new(0, 0, constants::CANVAS_SIZE.x, constants::CANVAS_SIZE.y)); let _ = canvas.fill_rect(Rect::new(0, 0, constants::CANVAS_SIZE.x, constants::CANVAS_SIZE.y));
@@ -101,7 +101,7 @@ pub fn hud_render_system(
let paused_height = paused_renderer.text_height(); let paused_height = paused_renderer.text_height();
let paused_position = glam::UVec2::new( let paused_position = glam::UVec2::new(
(constants::CANVAS_SIZE.x - paused_width) / 2, (constants::CANVAS_SIZE.x - paused_width) / 2,
(constants::CANVAS_SIZE.y - paused_height) / 2 (constants::CANVAS_SIZE.y - paused_height) / 2,
); );
if let Err(e) = paused_renderer.render_with_color(canvas, &mut atlas, paused_text, paused_position, Color::YELLOW) { if let Err(e) = paused_renderer.render_with_color(canvas, &mut atlas, paused_text, paused_position, Color::YELLOW) {
errors.write(TextureError::RenderFailed(format!("Failed to render PAUSED text: {}", e)).into()); errors.write(TextureError::RenderFailed(format!("Failed to render PAUSED text: {}", e)).into());
+14 -1
View File
@@ -43,6 +43,7 @@ pub struct IntroPlayed(pub bool);
pub enum GameStage { pub enum GameStage {
/// Waiting for user interaction before starting (Emscripten only). /// Waiting for user interaction before starting (Emscripten only).
/// Game is rendered but audio/gameplay are paused until the user clicks or presses a key. /// Game is rendered but audio/gameplay are paused until the user clicks or presses a key.
#[cfg(target_os = "emscripten")]
WaitingForInteraction, WaitingForInteraction,
Starting(StartupSequence), Starting(StartupSequence),
/// The main gameplay loop is active. /// The main gameplay loop is active.
@@ -186,7 +187,15 @@ impl TooSimilar for GameStage {
fn too_similar(&self, other: &Self) -> bool { fn too_similar(&self, other: &Self) -> bool {
discriminant(self) == discriminant(other) && { discriminant(self) == discriminant(other) && {
// These states are very simple, so they're 'too similar' automatically // These states are very simple, so they're 'too similar' automatically
if matches!(self, GameStage::Playing | GameStage::GameOver | GameStage::WaitingForInteraction) { #[cfg(target_os = "emscripten")]
if matches!(
self,
GameStage::Playing | GameStage::GameOver | GameStage::WaitingForInteraction
) {
return true;
}
#[cfg(not(target_os = "emscripten"))]
if matches!(self, GameStage::Playing | GameStage::GameOver) {
return true; return true;
} }
@@ -210,7 +219,10 @@ impl TooSimilar for GameStage {
}, },
) => ghost_entity == other_ghost_entity && ghost_type == other_ghost_type && node == other_node, ) => ghost_entity == other_ghost_entity && ghost_type == other_ghost_type && node == other_node,
// Already handled, but kept to properly exhaust the match // Already handled, but kept to properly exhaust the match
#[cfg(target_os = "emscripten")]
(GameStage::Playing, _) | (GameStage::GameOver, _) | (GameStage::WaitingForInteraction, _) => unreachable!(), (GameStage::Playing, _) | (GameStage::GameOver, _) | (GameStage::WaitingForInteraction, _) => unreachable!(),
#[cfg(not(target_os = "emscripten"))]
(GameStage::Playing, _) | (GameStage::GameOver, _) => unreachable!(),
_ => unreachable!(), _ => unreachable!(),
} }
} }
@@ -315,6 +327,7 @@ pub fn stage_system(
} }
let new_state: GameStage = new_state_opt.unwrap_or_else(|| match *game_state { let new_state: GameStage = new_state_opt.unwrap_or_else(|| match *game_state {
#[cfg(target_os = "emscripten")]
GameStage::WaitingForInteraction => { GameStage::WaitingForInteraction => {
// Stay in this state until JS calls start_game() // Stay in this state until JS calls start_game()
*game_state *game_state