ceb9c950a1
Add [workspace.lints.rust] and wire each member crate up with [lints] workspace = true: unsafe_code = "deny" (forbid would break the Android JNI build) single_use_lifetimes = "warn" trivial_casts = "warn" unused_lifetimes = "warn" unused_qualifications = "warn" variant_size_differences = "warn" unexpected_cfgs = "warn" unsafe_code is "deny" rather than the issue's "forbid" so the three Android JNI FFI modules (android_keystore, android_clipboard, safe_area) can opt back in with a scoped #![allow(unsafe_code)] — forbid cannot be locally overridden. Pure crates carry no unsafe and stay clean. Clean up the warnings the new lints surface: - 150ish unused_qualifications removed via `cargo fix` (purely syntactic redundant-path-prefix removals). - table_plugin: the TABLE_COLOUR import was #[cfg(test)]-gated while the camera clear-colour used the fully-qualified path; unqualifying it left a non-test build with no import. Made the import unconditional instead. - assets/sources: the `as &[u8]` casts in embed_*_svg! coerce each fixed-size &[u8; N] to a uniform slice so the tuples fit the &[(&str, &[u8])] arrays — load-bearing, so scoped #[allow(trivial_casts)]. Workspace clippy -D warnings and the full test suite pass. Android build not compiled here (needs the NDK; built separately per CLAUDE.md §15) — the deny + scoped-allow keeps the JNI unsafe blocks legal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
116 lines
4.0 KiB
Rust
116 lines
4.0 KiB
Rust
//! Downloads and caches the player's server avatar for display in the
|
|
//! profile modal.
|
|
//!
|
|
//! # Flow
|
|
//!
|
|
//! 1. After a successful login/register, `sync_setup_plugin` fires
|
|
//! [`AvatarFetchEvent`] with the server base URL and the relative
|
|
//! avatar path (e.g. `/avatars/{uuid}.png`).
|
|
//! 2. [`handle_avatar_fetch`] spawns an async task on the
|
|
//! [`AsyncComputeTaskPool`] that downloads the image bytes via
|
|
//! `reqwest` (reusing the same HTTP client pattern as the sync client).
|
|
//! 3. [`poll_avatar_task`] harvests the result, decodes the bytes into a
|
|
//! Bevy [`Image`] via `image::load_from_memory`, inserts it into
|
|
//! [`Assets<Image>`], and stores the [`Handle<Image>`] in
|
|
//! [`AvatarResource`].
|
|
//! 4. `profile_plugin` reads [`AvatarResource`] when the profile modal
|
|
//! opens and renders the avatar image (or an initials fallback when
|
|
//! `AvatarResource` is `None`).
|
|
|
|
use bevy::asset::RenderAssetUsages;
|
|
use bevy::prelude::*;
|
|
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
|
|
|
use crate::resources::TokioRuntimeResource;
|
|
|
|
/// Stores the loaded avatar [`Handle<Image>`], or `None` when no avatar
|
|
/// has been fetched yet (new account, no internet, or fetch in progress).
|
|
#[derive(Resource, Default)]
|
|
pub struct AvatarResource(pub Option<Handle<Image>>);
|
|
|
|
/// Fired by `sync_setup_plugin` after a successful login or register when
|
|
/// the server reports that the user has a profile picture set.
|
|
#[derive(Debug, Clone)]
|
|
pub struct AvatarFetchEvent {
|
|
/// Full HTTP(S) URL to the avatar image (base_url + avatar_url path).
|
|
pub url: String,
|
|
}
|
|
|
|
impl Message for AvatarFetchEvent {}
|
|
|
|
/// In-flight avatar download task. Returns the raw image bytes on success,
|
|
/// or `None` on any network / decode error.
|
|
#[derive(Resource, Default)]
|
|
struct PendingAvatarTask(Option<Task<Option<Vec<u8>>>>);
|
|
|
|
pub struct AvatarPlugin;
|
|
|
|
impl Plugin for AvatarPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.add_message::<AvatarFetchEvent>()
|
|
.init_resource::<AvatarResource>()
|
|
.init_resource::<PendingAvatarTask>()
|
|
.add_systems(Update, poll_avatar_task);
|
|
|
|
// Build the shared Tokio runtime; skip avatar download if the OS
|
|
// refuses to create threads (resource-limited / sandboxed environments).
|
|
match TokioRuntimeResource::new() {
|
|
Ok(rt) => {
|
|
app.insert_resource(rt)
|
|
.add_systems(Update, handle_avatar_fetch);
|
|
}
|
|
Err(e) => {
|
|
bevy::log::warn!(
|
|
"avatar_plugin: Tokio runtime unavailable — avatar fetch disabled: {e}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_avatar_fetch(
|
|
mut events: MessageReader<AvatarFetchEvent>,
|
|
rt: Res<TokioRuntimeResource>,
|
|
mut pending: ResMut<PendingAvatarTask>,
|
|
) {
|
|
for ev in events.read() {
|
|
// Cancel any in-flight task and restart with the new URL.
|
|
let url = ev.url.clone();
|
|
let rt = rt.0.clone();
|
|
pending.0 = Some(AsyncComputeTaskPool::get().spawn(async move {
|
|
rt.block_on(async move {
|
|
let client = reqwest::Client::new();
|
|
let bytes = client.get(&url).send().await.ok()?.bytes().await.ok()?;
|
|
Some(bytes.to_vec())
|
|
})
|
|
}));
|
|
}
|
|
}
|
|
|
|
fn poll_avatar_task(
|
|
mut pending: ResMut<PendingAvatarTask>,
|
|
mut avatar: ResMut<AvatarResource>,
|
|
mut images: ResMut<Assets<Image>>,
|
|
) {
|
|
let Some(task) = pending.0.as_mut() else {
|
|
return;
|
|
};
|
|
let Some(result) = future::block_on(future::poll_once(task)) else {
|
|
return;
|
|
};
|
|
pending.0 = None;
|
|
|
|
let Some(bytes) = result else { return };
|
|
|
|
match image::load_from_memory(&bytes) {
|
|
Ok(dyn_img) => {
|
|
let bevy_img = Image::from_dynamic(dyn_img, true, RenderAssetUsages::RENDER_WORLD);
|
|
let handle = images.add(bevy_img);
|
|
avatar.0 = Some(handle);
|
|
}
|
|
Err(e) => {
|
|
warn!("avatar_plugin: failed to decode avatar image: {e}");
|
|
}
|
|
}
|
|
}
|