Compare commits

..
21 Commits
Author SHA1 Message Date
SonicSwordcane 5d86ceddd7 chore: Release lemur version 0.13.0 2026-07-16 23:57:36 -04:00
SonicSwordcane d934c843eb Display (Player 1) or (Player 2) consistently 2026-07-16 23:56:20 -04:00
SonicSwordcane b283b69f74 Fix windows growing over time 2026-07-16 23:11:14 -04:00
SonicSwordcane 828acebb89 Fix cpal initialization on windows 2026-07-14 00:13:20 -04:00
SonicSwordcane 466cf29eb7 Update cpal 2026-07-13 23:16:27 -04:00
SonicSwordcane c7628c42d8 Update several dependencies 2026-07-13 23:03:35 -04:00
SonicSwordcane 2ce0edee76 chore: Release lemur version 0.12.4 2026-05-22 00:02:15 -04:00
SonicSwordcane d21cb4e5d0 Fix controller code 2026-05-21 23:45:00 -04:00
SonicSwordcane 6e36254203 Detect diagonals better on controllers 2026-05-21 23:03:15 -04:00
SonicSwordcane 5fd1957d74 Update world inspector too 2026-05-21 21:46:16 -04:00
SonicSwordcane 19c595dd7d Update shrooms-vb-core to address affine bug 2026-05-21 17:27:58 -04:00
SonicSwordcane bb43c9aad8 chore: Release lemur version 0.12.3 2026-04-27 22:03:08 -04:00
SonicSwordcane 9256c513da Update LLVM version 2026-04-27 22:02:33 -04:00
SonicSwordcane 218a1764bc Update deps 2026-04-27 21:21:57 -04:00
SonicSwordcane ea9ca614e9 Explicitly request redraw when needed 2026-04-27 21:17:13 -04:00
SonicSwordcane dd9e04f636 Update to rust 1.95 2026-04-18 11:23:01 -04:00
SonicSwordcane f37ee878e1 chore: Release lemur version 0.12.2 2026-04-12 14:08:44 -04:00
SonicSwordcane bd0a4294e8 Update deps 2026-04-12 14:05:49 -04:00
SonicSwordcane 9934566414 Avoid unnecessary frequent rerenders 2026-04-12 14:01:34 -04:00
SonicSwordcane 5aeb81e800 chore: Release lemur version 0.12.1 2026-04-07 22:33:24 -04:00
SonicSwordcane ec1fe6e0d3 Persist window positions when possible 2026-04-07 22:32:43 -04:00
24 changed files with 855 additions and 1166 deletions
Generated
+536 -1007
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -4,18 +4,18 @@ description = "An emulator for the Virtual Boy."
repository = "https://git.virtual-boy.com/PVB/lemur"
publish = false
license = "MIT"
version = "0.12.0"
version = "0.13.0"
edition = "2024"
[dependencies]
anyhow = "1"
atoi = "2"
audioadapter-buffers = "3"
atoi = "3"
audioadapter-buffers = "4"
atomic = "0.6"
bitflags = { version = "2", features = ["serde"] }
bytemuck = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] }
cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" }
cpal = "0.18"
directories = "6"
egui = { version = "0.34", features = ["persistence", "serde"] }
egui_extras = { version = "0.34", features = ["image"] }
@@ -25,21 +25,21 @@ egui-wgpu = { version = "0.34", features = ["winit"] }
fxprof-processed-profile = "0.8"
fixed = { version = "1.28", features = ["num-traits"] }
gilrs = { version = "0.11", features = ["serde-serialize"] }
gimli = "0.33"
gimli = "0.34"
hex = "0.4"
image = { version = "0.25", default-features = false, features = ["png"] }
itertools = "0.14"
itertools = "0.15"
normpath = "1"
notify = "8"
num-derive = "0.4"
num-traits = "0.2"
object = "0.39"
oneshot = { version = "0.2", features = ["async", "std"] }
pollster = "0.4"
pollster = "1"
rand = "0.10"
rfd = "0.17"
rtrb = "0.3"
rubato = "2"
rubato = "4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thread-priority = "3"
+14 -18
View File
@@ -1,7 +1,7 @@
# This Dockerfile produces a base image for builds.
# It includes all dependencies necessary to cross-compile for Windows/MacOS/Linux.
FROM debian:bookworm AS osxcross
FROM debian:trixie AS osxcross
ADD --chmod=644 "https://apt.llvm.org/llvm-snapshot.gpg.key" /etc/apt/trusted.gpg.d/apt.llvm.org.asc
WORKDIR /build/osxcross
ADD "https://github.com/tpoechtrager/osxcross.git#47936a512273bb3b414b5a2e83043c92eabc7ae7" .
@@ -9,35 +9,31 @@ ADD "https://github.com/joseluisq/macosx-sdks/releases/download/14.5/MacOSX14.5.
RUN apt-get update && \
apt-get install -y ca-certificates
COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
COPY install-llvm.sh .
RUN apt-get update && \
./install-llvm.sh && \
apt-get install -y bash bzip2 git make patch xz-utils && \
ln -s $(which clang-21) /usr/bin/clang && \
ln -s $(which clang++-21) /usr/bin/clang++ && \
ln -s $(which ld64.lld-21) /usr/bin/ld64.lld && \
apt-get install -y bash bzip2 clang-22 git lld-22 llvm-22 make patch xz-utils && \
ln -s $(which clang-22) /usr/bin/clang && \
ln -s $(which clang++-22) /usr/bin/clang++ && \
ln -s $(which ld64.lld-22) /usr/bin/ld64.lld && \
SDK_VERSION=14.5 UNATTENDED=1 ENABLE_COMPILER_RT_INSTALL=1 TARGET_DIR=/osxcross ./build.sh
FROM rust:1.94-bookworm
FROM rust:1.97-trixie
ADD --chmod=644 "https://apt.llvm.org/llvm-snapshot.gpg.key" /etc/apt/trusted.gpg.d/apt.llvm.org.asc
COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
COPY install-llvm.sh .
RUN rustup target add x86_64-pc-windows-msvc && \
rustup target add x86_64-apple-darwin && \
rustup target add aarch64-apple-darwin && \
apt-get update && \
./install-llvm.sh && \
apt-get install -y libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \
apt-get install -y clang-22 lld-22 libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \
cargo install cargo-bundle xwin && \
xwin --accept-license splat --output xwin && \
rm -rf .xwin-cache && \
ln -s $(which clang-21) /usr/bin/clang && \
ln -s $(which clang++-21) /usr/bin/clang++
ln -s $(which clang-22) /usr/bin/clang && \
ln -s $(which clang++-22) /usr/bin/clang++
COPY --from=osxcross /osxcross /osxcross
ENV PATH="/osxcross/bin:$PATH" \
LD_LIBRARY_PATH="/osxcross/lib" \
CC="clang-21" CXX="clang++-21" AR="llvm-ar-21" \
CC="clang-22" CXX="clang++-22" AR="llvm-ar-22" \
CC_x86_64-apple-darwin="o64-clang" \
CXX_x86_64-apple-darwin="o64-clang++" \
CC_aarch64-apple-darwin="oa64-clang" \
@@ -47,11 +43,11 @@ ENV PATH="/osxcross/bin:$PATH" \
SHROOMS_CFLAGS_x86_64-pc-windows-msvc="-flto" \
CFLAGS_x86_64-pc-windows-msvc="-I/xwin/crt/include -I/xwin/sdk/include/ucrt -I/xwin/sdk/include/um -I/xwin/sdk/include/shared" \
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64 -Clinker-plugin-lto -Clink-arg=-fuse-ld=lld" \
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-21" \
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-22" \
CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \
CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-21" \
CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-22" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-21" \
RC_PATH="llvm-rc-21" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-22" \
RC_PATH="llvm-rc-22" \
MACOSX_DEPLOYMENT_TARGET="14.5"
ENTRYPOINT ["bash"]
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
# Hopefully temporary script to manually install working llvm packages.
# The apt index for these is broken in bookworm, and upgrading to trixie
# would make them depend on too new of a libc version.
# https://apt.llvm.org/bookworm/pool/main/l/llvm-toolchain-21/clang-21_21.1.5~%2B%2B20251023083151%2B45afac62e373-1~exp1~20251023083333.51_amd64.deb
PACKAGES=('clang-21' 'clang-tools-21' 'libclang-common-21-dev' 'libclang-cpp21' 'libclang-rt-21-dev' 'libclang1-21' 'libllvm21' 'lld-21' 'llvm-21' 'llvm-21-dev' 'llvm-21-linker-tools' 'llvm-21-runtime' 'llvm-21-tools')
FILES=()
URL='https://apt.llvm.org/bookworm/pool/main/l/llvm-toolchain-21'
VERSION='21.1.5~%2B%2B20251023083151%2B45afac62e373-1~exp1~20251023083333.51_amd64.deb'
apt-get install -y curl python3
for package in "${PACKAGES[@]}"; do
curl -O -L "$URL/${package}_$VERSION"
FILES+=("./${package}_$VERSION")
done
apt-get install -y "${FILES[@]}"
+2 -2
View File
@@ -1,4 +1,4 @@
Types: deb deb-src
URIs: http://apt.llvm.org/bookworm/
Suites: llvm-toolchain-bookworm-21
URIs: http://apt.llvm.org/trixie/
Suites: llvm-toolchain-trixie-22
Components: main
+10 -7
View File
@@ -288,6 +288,7 @@ impl Application {
}
}
let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point);
viewport.window.pre_present_notify();
self.shared.painter.paint_and_update_textures(
viewport.id,
output.pixels_per_point,
@@ -296,7 +297,6 @@ impl Application {
&output.textures_delta,
vec![],
);
viewport.window.pre_present_notify();
}
let mut active_viewports = ViewportIdSet::default();
for (id, update) in updates {
@@ -398,22 +398,25 @@ impl ApplicationHandler<UserEvent> for Application {
if close_requested && viewport_id == ViewportId::ROOT {
event_loop.exit();
}
if response.repaint {
viewport.window.request_redraw();
}
if !response.consumed {
match event {
WindowEvent::KeyboardInput { event, .. } => {
if !self.app.handle_key_event(viewport_id, &event) {
WindowEvent::KeyboardInput { event, .. }
if !self.app.handle_key_event(viewport_id, &event) =>
{
self.controllers.handle_key_event(&event);
}
}
WindowEvent::Focused(new_focused) => {
self.focused = new_focused.then_some(viewport_id);
}
WindowEvent::RedrawRequested => {
self.repaint_all(event_loop);
}
_ => {}
}
}
if response.repaint {
self.repaint_all(event_loop);
}
self.check_repaint(event_loop);
}
+9 -9
View File
@@ -3,8 +3,8 @@ use std::time::Duration;
use anyhow::{Result, bail};
use audioadapter_buffers::direct::InterleavedSlice;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use rubato::Resampler;
use tracing::error;
use rubato::{Adjustable as _, Resampler};
use tracing::warn;
pub struct Audio {
#[allow(unused)]
@@ -23,14 +23,14 @@ impl Audio {
let Some(device) = host.default_output_device() else {
bail!("No output device available");
};
let Some(config) = device
.supported_output_configs()?
.find(|c| c.channels() == 2 && c.sample_format().is_float())
else {
let default_rate = device.default_output_config()?.sample_rate();
let Some(config) = device.supported_output_configs()?.find(|c| {
c.channels() == 2 && c.sample_format().is_float() && c.contains_rate(default_rate)
}) else {
bail!("No suitable output config available");
};
let mut config = config.with_max_sample_rate().config();
let resample_ratio = config.sample_rate.0 as f64 / VB_FREQUENCY as f64;
let resample_ratio = config.sample_rate as f64 / VB_FREQUENCY as f64;
let chunk_size = (834.0 * resample_ratio) as usize;
let sampler = rubato::Async::new_poly(
resample_ratio,
@@ -48,7 +48,7 @@ impl Audio {
rtrb::RingBuffer::new(sampler.output_frames_max() * 4);
let stream = device.build_output_stream(
&config,
config,
move |data: &mut [f32], _| {
let requested = data.len();
let chunk = match sample_source.read_chunk(data.len()) {
@@ -66,7 +66,7 @@ impl Audio {
}
chunk.commit_all();
},
move |error| error!(%error, "stream error"),
move |error| warn!(%error, "stream error"),
None,
)?;
stream.play()?;
+4 -1
View File
@@ -1,6 +1,6 @@
use anyhow::Result;
use clap::Parser;
use egui::{Color32, Vec2};
use egui::{Color32, Pos2, Vec2};
use serde::{Deserialize, Serialize};
use crate::{emulator::SimId, persistence::Persistence, window::DisplayMode};
@@ -75,6 +75,8 @@ pub struct SimConfig {
pub dimensions: Vec2,
#[serde(default = "default_audio_enabled")]
pub audio_enabled: bool,
#[serde(default)]
pub position: Option<Pos2>,
}
impl SimConfig {
@@ -87,6 +89,7 @@ impl SimConfig {
colors: COLOR_PRESETS[0],
dimensions: DisplayMode::Anaglyph.proportions() + Vec2::new(0.0, 22.0),
audio_enabled: true,
position: None,
}
}
+117 -17
View File
@@ -1,4 +1,7 @@
use std::sync::{Arc, RwLock};
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use gilrs::{Event as GamepadEvent, EventType, GamepadId, ev::Code};
use winit::{
@@ -8,12 +11,13 @@ use winit::{
use crate::{
emulator::{EmulatorClient, EmulatorCommand, SimId, VBKey},
input::{InputMapping, MappingProvider},
input::{AxisMapping, InputMapping, MappingProvider},
};
pub struct Controller {
pub sim_id: SimId,
state: VBKey,
axis_values: HashMap<(GamepadId, Code), f32>,
mapping: Arc<RwLock<InputMapping>>,
}
@@ -22,6 +26,7 @@ impl Controller {
Self {
sim_id,
state: VBKey::SGN,
axis_values: HashMap::new(),
mapping: mappings.for_sim(sim_id).clone(),
}
}
@@ -45,20 +50,23 @@ impl Controller {
(VBKey::empty(), mappings)
}
EventType::AxisChanged(_, value, code) => {
let (neg, pos) = self.map_axis(&event.id, &code)?;
let mut pressed = VBKey::empty();
let mut released = VBKey::empty();
if value < -0.75 {
pressed = pressed.union(neg);
}
if value > 0.75 {
pressed = pressed.union(pos);
}
if value > -0.65 {
released = released.union(neg);
}
if value < 0.65 {
released = released.union(pos);
let mapping = self.map_axis(&event.id, &code)?;
self.axis_values.insert((event.id, code), value);
let pair_value = mapping
.pair
.and_then(|p| self.axis_values.get(&(event.id, p)))
.copied()
.unwrap_or_default();
let neg = mapping.neg;
let pos = mapping.pos;
let (mut pressed, mut released) = axis_presses(value, pair_value, neg, pos);
if let Some(other) = mapping.pair.and_then(|p| self.map_axis(&event.id, &p)) {
let (other_pressed, other_released) =
axis_presses(pair_value, value, other.neg, other.pos);
pressed = pressed.union(other_pressed);
released = released.union(other_released);
}
(pressed, released)
}
@@ -87,7 +95,7 @@ impl Controller {
self.mapping.read().unwrap().map_button(id, code)
}
fn map_axis(&self, id: &GamepadId, code: &Code) -> Option<(VBKey, VBKey)> {
fn map_axis(&self, id: &GamepadId, code: &Code) -> Option<AxisMapping> {
self.mapping.read().unwrap().map_axis(id, code)
}
}
@@ -126,3 +134,95 @@ impl ControllerManager {
}
}
}
fn axis_presses(value: f32, pair_value: f32, neg: VBKey, pos: VBKey) -> (VBKey, VBKey) {
use std::f32::consts::FRAC_PI_3;
let mut pressed = VBKey::empty();
let mut released = VBKey::empty();
let magnitude = value.hypot(pair_value);
let abs_angle = pair_value.atan2(value).abs();
if magnitude < 0.65 {
released = released.union(neg).union(pos);
} else if abs_angle <= FRAC_PI_3 {
// stick tilted towards positive
released = released.union(neg);
if magnitude >= 0.75 {
pressed = pressed.union(pos);
}
} else if abs_angle >= 2.0 * FRAC_PI_3 {
// stick tilted towards negative
released = released.union(pos);
if magnitude >= 0.75 {
pressed = pressed.union(neg);
}
} else {
released = released.union(neg).union(pos);
}
(pressed, released)
}
#[cfg(test)]
mod tests {
use super::{VBKey, axis_presses};
const NEG: VBKey = VBKey::LL;
const POS: VBKey = VBKey::LR;
const NONE: VBKey = VBKey::empty();
const BOTH: VBKey = NEG.union(POS);
#[test]
fn detects_no_input() {
let (pressed, released) = axis_presses(0.0, 0.0, NEG, POS);
assert_eq!(pressed, NONE);
assert_eq!(released, BOTH);
}
#[test]
fn detects_pos_input() {
let (pressed, released) = axis_presses(1.0, 0.0, NEG, POS);
assert_eq!(pressed, POS);
assert_eq!(released, NEG);
}
#[test]
fn detects_neg_input() {
let (pressed, released) = axis_presses(-1.0, 0.0, NEG, POS);
assert_eq!(pressed, NEG);
assert_eq!(released, POS);
}
#[test]
fn respects_dead_zone() {
let (pressed, released) = axis_presses(0.70, 0.0, NEG, POS);
assert_eq!(pressed, NONE);
assert_eq!(released, NEG);
}
#[test]
fn handles_diagonals_ok() {
let (pressed, released) = axis_presses(0.6, 0.6, NEG, POS);
assert_eq!(pressed, POS);
assert_eq!(released, NEG);
}
#[test]
fn ignores_diagonal_noise() {
let (pressed, released) = axis_presses(-1.0, -0.1484245, NEG, POS);
assert_eq!(pressed, NEG);
assert_eq!(released, POS);
}
#[test]
fn ignores_small_values() {
let (pressed, released) = axis_presses(-0.1484245, -1.0, NEG, POS);
assert_eq!(pressed, NONE);
assert_eq!(released, BOTH);
}
}
+3
View File
@@ -946,6 +946,9 @@ pub struct EmulatorClient {
}
impl EmulatorClient {
pub fn has_player_2(&self) -> bool {
self.sim_state(SimId::Player2) != SimState::Uninitialized
}
pub fn sim_state(&self, sim_id: SimId) -> SimState {
self.sim_state[sim_id.to_index()].load(Ordering::Acquire)
}
+34 -23
View File
@@ -52,12 +52,28 @@ pub trait Mappings {
fn use_default_mappings(&mut self);
}
#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct AxisMapping {
pub neg: VBKey,
pub pos: VBKey,
pub pair: Option<Code>,
}
impl Default for AxisMapping {
fn default() -> Self {
Self {
neg: VBKey::empty(),
pos: VBKey::empty(),
pair: None,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct GamepadMapping {
buttons: HashMap<Code, VBKey>,
axes: HashMap<Code, (VBKey, VBKey)>,
axes: HashMap<Code, AxisMapping>,
default_buttons: HashMap<Code, VBKey>,
default_axes: HashMap<Code, (VBKey, VBKey)>,
default_axes: HashMap<Code, AxisMapping>,
}
impl GamepadMapping {
@@ -78,7 +94,8 @@ impl GamepadMapping {
let mut default_axes = HashMap::new();
let mut default_axis = |axis: Axis, neg: VBKey, pos: VBKey| {
if let Some(code) = gamepad.axis_code(axis) {
default_axes.insert(code, (neg, pos));
let pair = axis.second_axis().and_then(|a| gamepad.axis_code(a));
default_axes.insert(code, AxisMapping { neg, pos, pair });
}
};
default_axis(Axis::LeftStickX, VBKey::LL, VBKey::LR);
@@ -102,19 +119,13 @@ impl GamepadMapping {
}
pub fn add_axis_neg_mapping(&mut self, key: VBKey, code: Code) {
let entry = self
.axes
.entry(code)
.or_insert((VBKey::empty(), VBKey::empty()));
entry.0 = entry.0.union(key);
let entry = self.axes.entry(code).or_default();
entry.neg = entry.neg.union(key);
}
pub fn add_axis_pos_mapping(&mut self, key: VBKey, code: Code) {
let entry = self
.axes
.entry(code)
.or_insert((VBKey::empty(), VBKey::empty()));
entry.1 = entry.1.union(key);
let entry = self.axes.entry(code).or_default();
entry.pos = entry.pos.union(key);
}
fn save_mappings(&self) -> PersistedGamepadMapping {
@@ -145,11 +156,11 @@ impl GamepadMapping {
impl Mappings for GamepadMapping {
fn mapping_names(&self) -> HashMap<VBKey, Vec<String>> {
let mut results: HashMap<VBKey, Vec<String>> = HashMap::new();
for (axis, (left_keys, right_keys)) in &self.axes {
for key in left_keys.iter() {
for (axis, mapping) in &self.axes {
for key in mapping.neg.iter() {
results.entry(key).or_default().push(format!("-{axis}"));
}
for key in right_keys.iter() {
for key in mapping.pos.iter() {
results.entry(key).or_default().push(format!("+{axis}"));
}
}
@@ -162,10 +173,10 @@ impl Mappings for GamepadMapping {
}
fn clear_mappings(&mut self, key: VBKey) {
self.axes.retain(|_, (left, right)| {
*left = left.difference(key);
*right = right.difference(key);
!(left.is_empty() && right.is_empty())
self.axes.retain(|_, mapping| {
mapping.neg = mapping.neg.difference(key);
mapping.pos = mapping.pos.difference(key);
!(mapping.neg.is_empty() && mapping.pos.is_empty())
});
self.buttons.retain(|_, keys| {
*keys = keys.difference(key);
@@ -200,7 +211,7 @@ impl InputMapping {
mappings.buttons.get(code).copied()
}
pub fn map_axis(&self, id: &GamepadId, code: &Code) -> Option<(VBKey, VBKey)> {
pub fn map_axis(&self, id: &GamepadId, code: &Code) -> Option<AxisMapping> {
let mappings = self.gamepads.get(id)?.read().unwrap();
mappings.axes.get(code).copied()
}
@@ -459,9 +470,9 @@ struct PersistedKeyboardMapping {
#[derive(Serialize, Deserialize)]
struct PersistedGamepadMapping {
buttons: Vec<(Code, VBKey)>,
axes: Vec<(Code, (VBKey, VBKey))>,
axes: Vec<(Code, AxisMapping)>,
default_buttons: Vec<(Code, VBKey)>,
default_axes: Vec<(Code, (VBKey, VBKey))>,
default_axes: Vec<(Code, AxisMapping)>,
}
#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
+3 -3
View File
@@ -28,11 +28,11 @@ mod utils;
mod vip;
pub trait AppWindow {
fn sim_id(&self) -> SimId {
SimId::Player1
fn sim_id(&self) -> Option<SimId> {
None
}
fn image_url(&self, name: &str) -> String {
format!("vip://{}/{name}", self.sim_id())
format!("vip://{}/{name}", self.sim_id().unwrap_or(SimId::Player1))
}
fn initial_viewport(&self) -> ViewportBuilder;
fn show(&mut self, ui: &mut Ui);
+74 -15
View File
@@ -20,10 +20,11 @@ use crate::{
};
use anyhow::Context as _;
use egui::{
Align2, Button, CentralPanel, Color32, Context, Frame, MenuBar, Panel, Ui, Vec2,
Align2, Button, CentralPanel, Color32, Context, Frame, MenuBar, Panel, Pos2, Ui, Vec2,
ViewportBuilder, ViewportCommand, ViewportId, ViewportIdMap, Window,
};
use egui_notify::{Anchor, Toast, Toasts};
use serde::{Deserialize, Serialize};
use winit::{event::KeyEvent, event_loop::EventLoopProxy};
use super::{
@@ -50,8 +51,9 @@ pub struct GameWindow {
images: Arc<ImageTextureLoader>,
mappings: MappingProvider,
children: ViewportIdMap<ChildWindowWrapper>,
child_sizes: UiData<ViewportIdMap<Vec2>>,
child_states: UiData<ViewportIdMap<ChildState>>,
queued_children: Vec<ChildWindow>,
show_player_name: bool,
}
impl GameWindow {
@@ -91,8 +93,9 @@ impl GameWindow {
images: images.clone(),
mappings: mappings.clone(),
children: ViewportIdMap::default(),
child_sizes: UiData::new(),
child_states: UiData::new(),
queued_children: vec![],
show_player_name: false,
}
}
@@ -175,13 +178,18 @@ impl GameWindow {
};
let mut viewport = child.initial_viewport();
if let Some(size) = self.child_sizes.get(&viewport_id) {
viewport.inner_size = Some(*size);
let title = viewport.title.clone().unwrap();
let sim_id = child.sim_id();
if let Some(state) = self.child_states.get(&viewport_id) {
viewport.position = Some(state.position);
viewport.inner_size = Some(state.size);
}
self.children.insert(
viewport_id,
ChildWindowWrapper {
app: Arc::new(Mutex::new(child)),
title,
sim_id,
updates: Some(viewport),
close_requested: Arc::new(AtomicBool::new(false)),
},
@@ -369,7 +377,7 @@ impl GameWindow {
});
ui.menu_button("Options", |ui| self.show_options_menu(ui));
ui.menu_button("Multiplayer", |ui| {
let has_player_2 = self.client.sim_state(SimId::Player2) != SimState::Uninitialized;
let has_player_2 = self.client.has_player_2();
if self.sim_id == SimId::Player1
&& !has_player_2
&& ui.button("Open Player 2").clicked()
@@ -621,23 +629,56 @@ impl GameWindow {
}
impl AppWindow for GameWindow {
fn sim_id(&self) -> SimId {
self.sim_id
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
let builder = ViewportBuilder::default()
.with_title("Lemur")
.with_inner_size(self.config.dimensions)
.with_inner_size(self.config.dimensions);
if let Some(position) = self.config.position {
builder.with_position(position)
} else {
builder
}
}
fn show(&mut self, ui: &mut Ui) {
self.child_sizes.load(ui);
let has_player_2 = self.client.has_player_2();
if !self.show_player_name && has_player_2 {
ui.send_viewport_cmd(ViewportCommand::Title(format!("Lemur ({})", self.sim_id)));
for (viewport_id, child) in &self.children {
if let Some(sim_id) = child.sim_id {
ui.send_viewport_cmd_to(
*viewport_id,
ViewportCommand::Title(format!("{} ({sim_id})", child.title)),
);
}
}
self.show_player_name = true;
} else if self.show_player_name && !has_player_2 {
ui.send_viewport_cmd(ViewportCommand::Title("Lemur".to_string()));
for (viewport_id, child) in &self.children {
if child.sim_id.is_some() {
ui.send_viewport_cmd_to(
*viewport_id,
ViewportCommand::Title(child.title.clone()),
);
}
}
self.show_player_name = false;
}
self.child_states.load(ui);
let dimensions = {
let bounds = ui.content_rect();
bounds.max - bounds.min
};
self.update_config(|c| c.dimensions = dimensions);
let position = ui.input(|i| i.viewport().outer_rect.map(|r| r.min));
self.update_config(|c| {
c.dimensions = dimensions;
c.position = position;
});
while let Ok(toast) = self.messages.try_recv() {
self.toasts.add(toast);
@@ -679,8 +720,18 @@ impl AppWindow for GameWindow {
let app = child.app.clone();
let viewport_builder = child.updates.take().unwrap_or_default();
let close_requested = child.close_requested.clone();
let size = ui.input_for(*id, |v| v.viewport_rect().size());
self.child_sizes.insert(*id, size);
if let (Some(outer), Some(inner)) = ui.input_for(*id, |inp| {
let viewport = inp.viewport();
(viewport.outer_rect, viewport.inner_rect)
}) {
self.child_states.insert(
*id,
ChildState {
position: outer.min,
size: inner.size(),
},
);
}
ui.show_viewport_deferred(*id, viewport_builder, move |ui, _| {
app.lock().unwrap().show(ui);
if ui.input(|s| s.viewport().close_requested()) {
@@ -689,7 +740,7 @@ impl AppWindow for GameWindow {
});
true
});
self.child_sizes.save(ui);
self.child_states.save(ui);
ui.request_repaint_after(Duration::from_millis(10));
}
@@ -724,6 +775,8 @@ struct ColorPickerState {
struct ChildWindowWrapper {
app: Arc<Mutex<AppWrapper>>,
title: String,
sim_id: Option<SimId>,
updates: Option<ViewportBuilder>,
close_requested: Arc<AtomicBool>,
}
@@ -795,3 +848,9 @@ impl ChildWindow {
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
struct ChildState {
position: Pos2,
size: Vec2,
}
+3 -3
View File
@@ -42,13 +42,13 @@ impl GdbServerWindow {
}
impl AppWindow for GdbServerWindow {
fn sim_id(&self) -> SimId {
self.sim_id
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("GDB Server ({})", self.sim_id))
.with_title("GDB Server")
.with_inner_size((300.0, 200.0))
}
+3 -3
View File
@@ -82,13 +82,13 @@ impl ProfileWindow {
}
impl AppWindow for ProfileWindow {
fn sim_id(&self) -> SimId {
self.sim_id
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Profiler ({})", self.sim_id))
.with_title("Profiler")
.with_inner_size((300.0, 200.0))
}
+3 -3
View File
@@ -29,13 +29,13 @@ impl TerminalWindow {
}
impl AppWindow for TerminalWindow {
fn sim_id(&self) -> SimId {
self.sim_id
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Terminal ({})", self.sim_id))
.with_title("Terminal")
.with_inner_size((640.0, 480.0))
}
+10 -6
View File
@@ -1,6 +1,6 @@
use std::{
fmt::{Display, UpperHex},
ops::{Bound, Deref, DerefMut, RangeBounds},
ops::{AddAssign, Bound, Deref, DerefMut, MulAssign, RangeBounds},
str::FromStr,
};
@@ -10,7 +10,7 @@ use egui::{
RichText, Sense, Shape, Stroke, StrokeKind, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText,
ecolor::HexColor, util::id_type_map::SerializableAny,
};
use num_traits::{CheckedAdd, CheckedSub, One};
use num_traits::{CheckedAdd, CheckedSub, One, Zero};
pub trait UiExt {
fn section(&mut self, title: impl Into<String>, add_contents: impl FnOnce(&mut Ui));
@@ -105,14 +105,16 @@ enum Direction {
pub trait Number:
Copy
+ Zero
+ One
+ AddAssign
+ MulAssign
+ CheckedAdd
+ CheckedSub
+ Eq
+ Ord
+ Display
+ FromStr
+ FromRadix16
+ UpperHex
+ Send
+ Sync
@@ -121,14 +123,16 @@ pub trait Number:
}
impl<
T: Copy
+ Zero
+ One
+ AddAssign
+ MulAssign
+ CheckedAdd
+ CheckedSub
+ Eq
+ Ord
+ Display
+ FromStr
+ FromRadix16
+ UpperHex
+ Send
+ Sync
@@ -209,8 +213,8 @@ impl<T: Number> Widget for NumberEdit<'_, T> {
let from_string = |val: &str| {
if self.hex {
let bytes = val.as_bytes();
let (result, consumed) = T::from_radix_16(bytes);
(consumed == bytes.len()).then_some(result)
let (result, consumed) = atoi::Integer::<T>::from_radix_16(bytes);
(consumed == bytes.len()).then_some(result.0)
} else {
val.parse::<T>().ok()
}
+3 -3
View File
@@ -191,13 +191,13 @@ impl BgMapWindow {
}
impl AppWindow for BgMapWindow {
fn sim_id(&self) -> SimId {
self.sim_id
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("BG Map Data ({})", self.sim_id))
.with_title("BG Map Data")
.with_inner_size((640.0, 480.0))
}
+3 -3
View File
@@ -251,13 +251,13 @@ impl CharacterDataWindow {
}
impl AppWindow for CharacterDataWindow {
fn sim_id(&self) -> SimId {
self.sim_id
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Character Data ({})", self.sim_id))
.with_title("Character Data")
.with_inner_size((640.0, 480.0))
}
+3 -3
View File
@@ -151,13 +151,13 @@ impl FrameBufferWindow {
}
impl AppWindow for FrameBufferWindow {
fn sim_id(&self) -> SimId {
self.sim_id
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Frame Buffers ({})", self.sim_id))
.with_title("Frame Buffers")
.with_inner_size((640.0, 480.0))
}
+3 -3
View File
@@ -207,13 +207,13 @@ impl ObjectWindow {
}
impl AppWindow for ObjectWindow {
fn sim_id(&self) -> SimId {
self.sim_id
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Object Data ({})", self.sim_id))
.with_title("Object Data")
.with_inner_size((640.0, 500.0))
}
+3 -3
View File
@@ -630,13 +630,13 @@ fn read_address<T: MemoryValue>(registers: &MemoryRef, address: usize) -> T {
}
impl AppWindow for RegisterWindow {
fn sim_id(&self) -> SimId {
self.sim_id
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Registers ({})", self.sim_id))
.with_title("Registers")
.with_inner_size((800.0, 480.0))
}
+5 -5
View File
@@ -527,13 +527,13 @@ impl WorldWindow {
}
impl AppWindow for WorldWindow {
fn sim_id(&self) -> SimId {
self.sim_id
fn sim_id(&self) -> Option<SimId> {
Some(self.sim_id)
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Worlds ({})", self.sim_id))
.with_title("Worlds")
.with_inner_size((640.0, 520.0))
}
@@ -1081,8 +1081,8 @@ impl<'a> SourceCoordCalculator<'a> {
(sx, sy)
}
SourceParam::Affine(affine) => {
let sx = affine_coord(affine.src_x, x, affine.dx, affine.src_parallax.min(0));
let sy = affine_coord(affine.src_y, x, affine.dy, affine.src_parallax.min(0));
let sx = affine_coord(affine.src_x, x, affine.dx, -(affine.src_parallax.min(0)));
let sy = affine_coord(affine.src_y, x, affine.dy, -(affine.src_parallax.min(0)));
(sx, sy)
}
}