Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93da0d1ea3 | ||
|
|
10eb72b368 |
+2
-33
@@ -63,6 +63,7 @@ jobs:
|
||||
libxkbcommon-dev \
|
||||
libxkbcommon-x11-0 \
|
||||
mesa-vulkan-drivers \
|
||||
blender \
|
||||
mold \
|
||||
pkg-config
|
||||
|
||||
@@ -129,54 +130,22 @@ jobs:
|
||||
libusb-1.0-0-dev \
|
||||
libwayland-dev \
|
||||
libx11-dev \
|
||||
libxfixes3 \
|
||||
libxi6 \
|
||||
libxkbcommon-dev \
|
||||
libxkbcommon-x11-0 \
|
||||
blender \
|
||||
lld-19 \
|
||||
llvm-19 \
|
||||
mesa-vulkan-drivers \
|
||||
mold \
|
||||
blender \
|
||||
pkg-config
|
||||
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-19 100
|
||||
sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-19 100
|
||||
echo "/usr/lib/llvm-19/bin" >> "$GITHUB_PATH"
|
||||
rm -f "$HOME/.cache/cargo-xwin/clang-cl"
|
||||
|
||||
- name: Install pinned Blender
|
||||
run: |
|
||||
scripts/install-blender.sh "$RUNNER_TEMP/blender"
|
||||
echo "BLENDER=$RUNNER_TEMP/blender/blender" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Preprocess assets
|
||||
run: cargo run --locked --bin process-assets --no-default-features --features debug_features -j 4
|
||||
|
||||
- name: Verify processed battlefield asset
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import struct
|
||||
|
||||
path = "imported_assets/Default/stages/battlefield/bf2.blend"
|
||||
with open(path, "rb") as asset:
|
||||
header = asset.read(12)
|
||||
if len(header) != 12:
|
||||
raise SystemExit(f"processed asset is truncated: {path}")
|
||||
magic, version, length = struct.unpack("<4sII", header)
|
||||
if magic != b"glTF" or version != 2 or length != asset.seek(0, 2):
|
||||
raise SystemExit(f"processed asset is not a valid GLB: {path}")
|
||||
asset.seek(12)
|
||||
json_length, chunk_type = struct.unpack("<II", asset.read(8))
|
||||
if chunk_type != 0x4E4F534A:
|
||||
raise SystemExit(f"processed asset has no JSON chunk: {path}")
|
||||
document = json.loads(asset.read(json_length))
|
||||
meshes = document.get("meshes", [])
|
||||
if not meshes:
|
||||
raise SystemExit(f"processed asset contains no meshes: {path}")
|
||||
print(f"{path}: {len(meshes)} meshes, {length} bytes")
|
||||
PY
|
||||
|
||||
- name: Build production binary
|
||||
run: cargo build --locked --profile production --no-default-features --features debug_features -j 4
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
version="5.2.2"
|
||||
sha256="84098912789dc450e95697c4184fb8a90acbe5111c2ba4aede3fecb57806a168"
|
||||
install_dir="${1:-${BLENDER_INSTALL_DIR:-${HOME}/.local/blender-${version}}}"
|
||||
|
||||
if [[ "$(uname -s)" != Linux || "$(uname -m)" != x86_64 ]]; then
|
||||
echo "error: this installer currently supports Linux x86_64 only" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -x "$install_dir/blender" ]]; then
|
||||
"$install_dir/blender" --version | head -n 1
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$install_dir")"
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
archive="blender-${version}-linux-x64.tar.xz"
|
||||
url="https://download.blender.org/release/Blender5.2/${archive}"
|
||||
|
||||
curl --fail --location --silent --show-error --output "$tmp_dir/$archive" "$url"
|
||||
printf '%s %s\n' "$sha256" "$tmp_dir/$archive" | sha256sum --check --status
|
||||
tar --extract --xz --file "$tmp_dir/$archive" --directory "$tmp_dir"
|
||||
mv "$tmp_dir/blender-${version}-linux-x64" "$install_dir"
|
||||
|
||||
"$install_dir/blender" --version | head -n 1
|
||||
+4
-22
@@ -84,11 +84,10 @@ impl AssetLoader for BlendSourceLoader {
|
||||
.arg(&blend_path)
|
||||
.arg("--python")
|
||||
.arg(&script)
|
||||
.arg("--")
|
||||
.arg(&output)
|
||||
.arg(&actions_path)
|
||||
.arg(&blend_path)
|
||||
.output()
|
||||
.arg("--")
|
||||
.arg(&output)
|
||||
.arg(&actions_path)
|
||||
.output()
|
||||
.map_err(|error| io::Error::other(format!("failed to start Blender: {error}")))?;
|
||||
if !result.status.success() {
|
||||
return Err(io::Error::other(format!(
|
||||
@@ -107,7 +106,6 @@ impl AssetLoader for BlendSourceLoader {
|
||||
let mut dependencies = manifest.dependencies;
|
||||
let document: serde_json::Value = serde_json::from_slice(&fs::read(&output)?)
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||
validate_export(&document, &output)?;
|
||||
if let Some(images) = document.get("images").and_then(serde_json::Value::as_array) {
|
||||
for image in images {
|
||||
if let Some(uri) = image.get("uri").and_then(serde_json::Value::as_str) {
|
||||
@@ -144,22 +142,6 @@ impl AssetLoader for BlendSourceLoader {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_export(document: &serde_json::Value, output: &Path) -> io::Result<()> {
|
||||
for key in ["scenes", "nodes", "meshes"] {
|
||||
let count = document
|
||||
.get(key)
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map_or(0, Vec::len);
|
||||
if count == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Blender export {} contains no {key}", output.display()),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn referenced_actions(
|
||||
load_context: &mut LoadContext<'_>,
|
||||
root: &Path,
|
||||
|
||||
@@ -7,15 +7,6 @@ import sys
|
||||
def main():
|
||||
output = sys.argv[sys.argv.index("--") + 1]
|
||||
actions_path = sys.argv[sys.argv.index("--") + 2]
|
||||
source_path = sys.argv[sys.argv.index("--") + 3]
|
||||
loaded_path = os.path.realpath(bpy.data.filepath)
|
||||
expected_path = os.path.realpath(source_path)
|
||||
if loaded_path != expected_path:
|
||||
raise RuntimeError(
|
||||
f"Blender loaded {loaded_path!r} instead of {expected_path!r}"
|
||||
)
|
||||
if not bpy.data.objects or not bpy.data.meshes:
|
||||
raise RuntimeError("Blender loaded an empty scene")
|
||||
with open(actions_path) as file:
|
||||
actions_to_keep = set(json.load(file))
|
||||
|
||||
|
||||
+4
-23
@@ -3,9 +3,11 @@
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
use bevy::{
|
||||
camera::{CameraOutputMode, visibility::RenderLayers}, input::InputSystems, prelude::*, render::render_resource::BlendState, settings::{ReflectSettingsGroup, SaveSettingsDeferred, SettingsGroup},
|
||||
input::InputSystems,
|
||||
prelude::*,
|
||||
settings::{ReflectSettingsGroup, SaveSettingsDeferred, SettingsGroup},
|
||||
};
|
||||
use bevy_egui::{EguiContexts, EguiGlobalSettings, EguiPrimaryContextPass, PrimaryEguiContext, egui};
|
||||
use bevy_egui::{EguiContexts, EguiPrimaryContextPass, egui};
|
||||
use bevy_ggrs::{RollbackFrameRate, RunGgrsSystems};
|
||||
use enum_cycling::EnumCycle;
|
||||
use enum_cycling_derive::EnumCycle;
|
||||
@@ -395,7 +397,6 @@ impl Plugin for DebugToolsPlugin {
|
||||
.before(RunGgrsSystems)
|
||||
.run_if(in_state(AppState::InMatch)),
|
||||
)
|
||||
.add_systems(Startup, spawn_egui_camera)
|
||||
.add_systems(PreUpdate, restore_frame_step_time.after(RunGgrsSystems))
|
||||
.add_systems(
|
||||
Update,
|
||||
@@ -407,26 +408,6 @@ impl Plugin for DebugToolsPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_egui_camera(mut commands: Commands, mut egui_global_settings: ResMut<EguiGlobalSettings>,) {
|
||||
egui_global_settings.auto_create_primary_context = false;
|
||||
commands.spawn((
|
||||
// The `PrimaryEguiContext` component requires everything needed to render a primary context.
|
||||
PrimaryEguiContext,
|
||||
Camera2d,
|
||||
// Setting RenderLayers to none makes sure we won't render anything apart from the UI.
|
||||
RenderLayers::none(),
|
||||
Camera {
|
||||
order: 2,
|
||||
output_mode: CameraOutputMode::Write {
|
||||
blend_state: Some(BlendState::ALPHA_BLENDING),
|
||||
clear_color: ClearColorConfig::None,
|
||||
},
|
||||
clear_color: ClearColorConfig::Custom(Color::NONE),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn player_handles(players: &Query<&Player>) -> Vec<usize> {
|
||||
let mut handles = players
|
||||
.iter()
|
||||
|
||||
+10
-74
@@ -1,8 +1,6 @@
|
||||
use std::panic::{self, AssertUnwindSafe};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::JoinHandle;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bevy::prelude::*;
|
||||
use gc_adapter_neo::{Controller, GcAdapter};
|
||||
@@ -57,11 +55,10 @@ impl GcPortState {
|
||||
/// Shared, `Send + Sync` handle to the latest snapshot from the poll
|
||||
/// thread. Safe to store as an ordinary [`Resource`] (no `NonSend` needed)
|
||||
/// because the `GcAdapter` itself never leaves its thread.
|
||||
#[derive(Resource)]
|
||||
#[derive(Resource, Clone)]
|
||||
pub struct GcAdapterSnapshot {
|
||||
ports: Arc<Mutex<[GcPortState; 4]>>,
|
||||
shutdown: Arc<AtomicBool>,
|
||||
poll_thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl GcAdapterSnapshot {
|
||||
@@ -72,38 +69,14 @@ impl GcAdapterSnapshot {
|
||||
|
||||
impl Drop for GcAdapterSnapshot {
|
||||
fn drop(&mut self) {
|
||||
self.shutdown.store(true, Ordering::Release);
|
||||
|
||||
if let Some(poll_thread) = self.poll_thread.take() {
|
||||
if let Err(payload) = poll_thread.join() {
|
||||
error!(
|
||||
"GameCube Controller Adapter poll thread panicked during shutdown: {payload:?}"
|
||||
);
|
||||
}
|
||||
// Only actually signals shutdown once the last clone (this resource
|
||||
// is never cloned in practice, but be defensive) is dropped.
|
||||
if Arc::strong_count(&self.shutdown) <= 1 {
|
||||
self.shutdown.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const INITIAL_ERROR_BACKOFF: Duration = Duration::from_millis(1);
|
||||
const MAX_ERROR_BACKOFF: Duration = Duration::from_millis(100);
|
||||
|
||||
fn next_error_backoff(backoff: Duration) -> Duration {
|
||||
backoff.saturating_mul(2).min(MAX_ERROR_BACKOFF)
|
||||
}
|
||||
|
||||
/// Wait for a retry while still allowing the owner to shut the poll thread down promptly.
|
||||
fn wait_for_retry(shutdown: &AtomicBool, delay: Duration) -> bool {
|
||||
let deadline = Instant::now() + delay;
|
||||
while !shutdown.load(Ordering::Acquire) {
|
||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||
if remaining.is_zero() {
|
||||
return true;
|
||||
}
|
||||
std::thread::sleep(remaining.min(Duration::from_millis(10)));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Connects to the first GameCube adapter found on the USB bus and starts
|
||||
/// polling it on a dedicated thread. Returns `None` (after logging a
|
||||
/// warning) if no adapter is plugged in
|
||||
@@ -121,7 +94,7 @@ pub fn spawn_poll_thread() -> Result<GcAdapterSnapshot, gc_adapter_neo::UsbError
|
||||
|
||||
let thread_ports = ports.clone();
|
||||
let thread_shutdown = shutdown.clone();
|
||||
let poll_thread = std::thread::Builder::new()
|
||||
std::thread::Builder::new()
|
||||
.name("gc-adapter-poll".into())
|
||||
.spawn(move || {
|
||||
// Flush whatever the adapter had buffered before we started
|
||||
@@ -134,27 +107,17 @@ pub fn spawn_poll_thread() -> Result<GcAdapterSnapshot, gc_adapter_neo::UsbError
|
||||
let mut rate_window_start = std::time::Instant::now();
|
||||
#[cfg(debug_assertions)]
|
||||
let mut rate_window_count: u32 = 0;
|
||||
let mut error_backoff = INITIAL_ERROR_BACKOFF;
|
||||
|
||||
while !thread_shutdown.load(Ordering::Acquire) {
|
||||
while !thread_shutdown.load(Ordering::Relaxed) {
|
||||
let result = panic::catch_unwind(AssertUnwindSafe(|| adapter.read_controllers()));
|
||||
|
||||
let controllers = match result {
|
||||
Ok(Ok(controllers)) => {
|
||||
error_backoff = INITIAL_ERROR_BACKOFF;
|
||||
controllers
|
||||
}
|
||||
Ok(Ok(controllers)) => controllers,
|
||||
// Ignore this, for some reason we get a bunch of these when we start reading
|
||||
Ok(Err(
|
||||
gc_adapter_neo::AdapterError::Parse(_)
|
||||
| gc_adapter_neo::AdapterError::ShortRead(_),
|
||||
)) => {
|
||||
if !wait_for_retry(&thread_shutdown, error_backoff) {
|
||||
break;
|
||||
}
|
||||
error_backoff = next_error_backoff(error_backoff);
|
||||
continue;
|
||||
}
|
||||
)) => continue,
|
||||
Ok(Err(gc_adapter_neo::AdapterError::Usb(err))) => {
|
||||
error!(
|
||||
"GameCube Controller Adapter USB read failed ({err:?}); stopping \
|
||||
@@ -197,32 +160,5 @@ pub fn spawn_poll_thread() -> Result<GcAdapterSnapshot, gc_adapter_neo::UsbError
|
||||
})
|
||||
.expect("failed to spawn gc-adapter poll thread");
|
||||
|
||||
Ok(GcAdapterSnapshot {
|
||||
ports,
|
||||
shutdown,
|
||||
poll_thread: Some(poll_thread),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn error_backoff_is_capped() {
|
||||
let mut backoff = INITIAL_ERROR_BACKOFF;
|
||||
for _ in 0..16 {
|
||||
backoff = next_error_backoff(backoff);
|
||||
}
|
||||
|
||||
assert_eq!(backoff, MAX_ERROR_BACKOFF);
|
||||
assert_eq!(next_error_backoff(MAX_ERROR_BACKOFF), MAX_ERROR_BACKOFF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_wait_stops_when_shutdown_is_requested() {
|
||||
let shutdown = AtomicBool::new(true);
|
||||
|
||||
assert!(!wait_for_retry(&shutdown, Duration::from_secs(1)));
|
||||
}
|
||||
Ok(GcAdapterSnapshot { ports, shutdown })
|
||||
}
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
use bevy::{picking::pointer::PointerId, prelude::*};
|
||||
|
||||
use crate::{
|
||||
AppState, fighter::manifest::{FighterManifest, FighterManifestRegistry}, game_settings::CommonAssets, input::{LocalInputAssignments, LocalInputSource}, menus::{
|
||||
MenuCameraMarker, MenuMarker, button::FGUiButton, character_select::{character_box::CharacterBox, token::CharacterSelectToken}, cursor::{FGMenuCursor, MenuCursorInputSource}, errors::MenuErrors::FighterManifestNotFound, match_config::{MenuMatchConfig, MenuMatchPlayerSlot}, scaling, spawn_menu, style::{FGUiButtonType, FGUiStyle},
|
||||
AppState,
|
||||
fighter::manifest::{FighterManifest, FighterManifestRegistry},
|
||||
game_settings::CommonAssets,
|
||||
input::{LocalInputAssignments, LocalInputSource},
|
||||
menus::{
|
||||
MenuMarker,
|
||||
button::FGUiButton,
|
||||
character_select::{character_box::CharacterBox, token::CharacterSelectToken},
|
||||
cursor::{FGMenuCursor, MenuCursorInputSource},
|
||||
errors::MenuErrors::FighterManifestNotFound,
|
||||
match_config::{MenuMatchConfig, MenuMatchPlayerSlot},
|
||||
scaling, spawn_menu,
|
||||
style::{FGUiButtonType, FGUiStyle},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -230,7 +241,7 @@ pub fn copy_cursor_transform_to_token(
|
||||
character_boxes: Query<(&CharacterBox, &ComputedNode, &UiGlobalTransform)>,
|
||||
match_config: Res<MenuMatchConfig>,
|
||||
cursors: Query<&UiGlobalTransform, With<FGMenuCursor>>,
|
||||
cam: Single<&Camera, With<MenuCameraMarker>>,
|
||||
cam: Single<&Camera>,
|
||||
ui_scale: Res<UiScale>,
|
||||
) -> Result {
|
||||
let camera = cam.into_inner();
|
||||
|
||||
+6
-3
@@ -9,8 +9,11 @@ use bevy::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
input::{LocalInputAssignments, LocalInputSource}, menus::{
|
||||
MenuCameraMarker, input::{MenuInput, MenuInputState}, scaling, style::FGUiStyle,
|
||||
input::{LocalInputAssignments, LocalInputSource},
|
||||
menus::{
|
||||
input::{MenuInput, MenuInputState},
|
||||
scaling,
|
||||
style::FGUiStyle,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -178,7 +181,7 @@ pub fn cursor_input(
|
||||
}
|
||||
|
||||
pub fn update_cursor_transform(
|
||||
camera: Single<&Camera, With<MenuCameraMarker>>,
|
||||
camera: Single<&Camera>,
|
||||
query: Query<(&mut Node, &PointerLocation), With<FGMenuCursor>>,
|
||||
ui_scale: Res<UiScale>,
|
||||
) {
|
||||
|
||||
+4
-1
@@ -53,7 +53,6 @@ pub fn wrap_menu(menu: impl Scene) -> impl Scene {
|
||||
#MenuCamera
|
||||
Camera
|
||||
Camera2d
|
||||
MenuCameraMarker
|
||||
),
|
||||
(
|
||||
#MenuContainer
|
||||
@@ -74,6 +73,7 @@ pub fn wrap_menu(menu: impl Scene) -> impl Scene {
|
||||
|
||||
pub fn despawn_menu(
|
||||
existing_menu: Option<Single<Entity, With<MenuMarker>>>,
|
||||
existing_menu_camera: Option<Single<Entity, With<MenuCameraMarker>>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
if let Some(entity) = existing_menu {
|
||||
@@ -82,6 +82,9 @@ pub fn despawn_menu(
|
||||
.despawn();
|
||||
}
|
||||
|
||||
if let Some(menu_cam) = existing_menu_camera {
|
||||
commands.entity(*menu_cam).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_menu(
|
||||
|
||||
Reference in New Issue
Block a user