Make Slint optional (#778)

* make slint optional

* more

* more
This commit is contained in:
Logan McNaughton 2026-04-19 15:15:56 -06:00 committed by GitHub
parent a488f0d91a
commit edb6960d4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 210 additions and 166 deletions

View File

@ -64,6 +64,7 @@ jobs:
matrix.llvm }}.tar.xz
- name: Build
run: |
cargo build --no-default-features -v --target=${{ matrix.target }}
cargo build --release -v --target=${{ matrix.target }}
mkdir output
cp target/${{ matrix.target }}/release/gopher64 output/gopher64-${{ matrix.arch }}
@ -113,6 +114,7 @@ jobs:
}}.exe
- name: Build
run: |
cargo build --no-default-features -v --target=${{ matrix.target }}
cargo build --release -v --target=${{ matrix.target }}
mkdir output
cp target/${{ matrix.target }}/release/gopher64.exe output/gopher64-${{ matrix.arch }}.exe
@ -182,6 +184,7 @@ jobs:
run: |
cargo install cargo-bundle
brew install molten-vk
cargo build --no-default-features -v --target=${{ matrix.target }}
cargo bundle --release --target=${{ matrix.target }} --format osx
install_name_tool -change /opt/homebrew/opt/freetype/lib/libfreetype.6.dylib \
@executable_path/../Frameworks/libfreetype.6.dylib target/${{ matrix.target }}/release/bundle/osx/Gopher64.app/Contents/MacOS/gopher64

View File

@ -49,3 +49,4 @@ jobs:
- name: Lint
run: |
cargo clippy -- -Dwarnings
cargo clippy --no-default-features -- -Dwarnings

View File

@ -4,7 +4,9 @@ version = "1.1.17"
edition = "2024"
rust-version = "1.95.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["gui"]
gui = ["dep:slint", "dep:slint-build"]
[dependencies]
dirs = "6.0"
@ -14,7 +16,7 @@ chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-big-array = "0.5"
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-skia"] }
slint = { version = "1.16", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-skia"], optional = true }
open = "5.3"
sha2 = "0.11"
sdl3-sys = { version = "0.6", features = ["build-from-source-static"] }
@ -38,7 +40,7 @@ reqwest = { version = "0.13", features = ["json", "query"] }
[build-dependencies]
winresource = "0.1"
bindgen = "0.72"
slint-build = "1.12"
slint-build = { version = "1.12", optional = true }
cc = { version = "1.2", features = ["parallel"] }
[profile.release]

View File

@ -3,8 +3,11 @@ fn main() {
println!("cargo::rerun-if-changed=retroachievements");
println!("cargo::rerun-if-changed=src/compat");
let slint_config = slint_build::CompilerConfiguration::new();
slint_build::compile_with_config("src/ui/gui/appwindow.slint", slint_config).unwrap();
#[cfg(feature = "gui")]
{
let slint_config = slint_build::CompilerConfiguration::new();
slint_build::compile_with_config("src/ui/gui/appwindow.slint", slint_config).unwrap();
}
let mut simd_build = cc::Build::new();
let mut volk_build = cc::Build::new();
@ -143,6 +146,9 @@ fn main() {
println!("cargo:rustc-link-search=native={}", runtime_dir);
println!("cargo:rustc-link-lib=static=clang_rt.osx");
}
if cfg!(not(feature = "gui")) && os == "linux" {
println!("cargo:rustc-link-lib=dylib=freetype");
}
volk_build.flag("-flto=thin");
rdp_build.flag("-flto=thin");
@ -155,7 +161,7 @@ fn main() {
let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
let retroachievements_bindings = bindgen::Builder::default()
let mut retroachievements_builder = bindgen::Builder::default()
.header("retroachievements/retroachievements.h")
.allowlist_function("ra_init_client")
.allowlist_function("ra_welcome")
@ -166,15 +172,21 @@ fn main() {
.allowlist_function("ra_do_frame")
.allowlist_function("ra_do_idle")
.allowlist_function("ra_http_callback")
.allowlist_function("ra_logout_user")
.allowlist_function("ra_login_user")
.allowlist_function("ra_login_token_user")
.allowlist_function("ra_is_user_logged_in")
.allowlist_function("ra_get_username")
.allowlist_function("ra_get_token")
.allowlist_function("ra_state_size")
.allowlist_function("ra_save_state")
.allowlist_function("ra_load_state")
.allowlist_function("ra_load_state");
if cfg!(feature = "gui") {
retroachievements_builder = retroachievements_builder
.allowlist_function("ra_logout_user")
.allowlist_function("ra_is_user_logged_in")
.allowlist_function("ra_get_username")
.allowlist_function("ra_get_token");
}
let retroachievements_bindings = retroachievements_builder
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");

View File

@ -15,16 +15,24 @@ pub struct Cheats {
pub enabled: bool,
}
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct CheatData {
pub note: String,
pub data: Vec<String>,
pub options: Option<std::collections::BTreeMap<String, String>>,
}
pub type Cheat = std::collections::BTreeMap<String, std::collections::BTreeMap<String, CheatData>>;
pub fn init(
device: &mut device::Device,
cheat_settings: std::collections::HashMap<String, Option<String>>,
) {
let cheats =
serde_json::from_slice::<ui::cheats::Cheats>(include_bytes!("../data/cheats.json"))
.unwrap()
.get(&ui::storage::get_game_crc(&device.cart.rom))
.cloned()
.unwrap_or_default();
let cheats = serde_json::from_slice::<Cheat>(include_bytes!("../data/cheats.json"))
.unwrap()
.get(&ui::storage::get_game_crc(&device.cart.rom))
.cloned()
.unwrap_or_default();
let re = regex::Regex::new(r"\?+").unwrap();
for cheat_setting in cheat_settings.iter() {

View File

@ -44,7 +44,7 @@ pub mod tlb;
pub mod unmapped;
pub mod vi;
pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::gui::GameSettings) {
pub fn run_game(device: &mut Device, rom_contents: &[u8], game_settings: ui::GameSettings) {
device.cpu.overclock = game_settings.overclock;
if game_settings.disable_expansion_pak {
device.rdram.size = 0x400000;

View File

@ -1,4 +1,6 @@
use crate::{device, ui};
use crate::device;
#[cfg(feature = "gui")]
use crate::ui;
use std::collections::HashMap;
const JCMD_VRU_READ: u8 = 0x09;
@ -179,7 +181,10 @@ pub fn process(device: &mut device::Device, channel: usize) {
device.pif.ram[device.pif.channels[channel].rx_buf.unwrap()] = 0;
}
JCMD_VRU_READ => {
#[cfg(feature = "gui")]
let index = ui::vru::prompt_for_match(&device.vru.words, device.vi.frame_time);
#[cfg(not(feature = "gui"))]
let index = 0x7FFF;
let num_results = if index == 0x7FFF { 0 } else { 1 };
let data: HashMap<usize, u16> = HashMap::from([
(0, 0x8000),

View File

@ -11,11 +11,10 @@ mod savestates;
mod ui;
use clap::Parser;
use std::io::Error;
use ui::gui;
/// N64 emulator
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[command(author, version, about, long_about = None, arg_required_else_help = if cfg!(feature = "gui") { false } else { true })]
struct Args {
game: Option<String>,
#[arg(short, long)]
@ -214,7 +213,7 @@ async fn main() -> std::io::Result<()> {
device::run_game(
&mut device,
&rom_contents,
ui::gui::GameSettings {
ui::GameSettings {
overclock,
disable_expansion_pak,
cheats,
@ -282,9 +281,12 @@ async fn main() -> std::io::Result<()> {
ui::input::bind_input_profile(&mut ui, profile, port);
}
} else {
retroachievements::init_client(false, false, false);
gui::app_window();
retroachievements::shutdown_client();
#[cfg(feature = "gui")]
{
retroachievements::init_client(false, false, false);
ui::gui::app_window();
retroachievements::shutdown_client();
}
}
Ok(())

View File

@ -1,7 +1,6 @@
include!(concat!(env!("OUT_DIR"), "/retroachievements_bindings.rs"));
use crate::ui;
use slint::ComponentHandle;
static WEB_CLIENT: std::sync::LazyLock<reqwest::Client> = std::sync::LazyLock::new(|| {
reqwest::Client::builder()
@ -144,115 +143,6 @@ pub extern "C" fn rust_server_call(
});
}
fn set_current_user_message(app: &ui::gui::AppWindow, rx: tokio::sync::oneshot::Receiver<bool>) {
app.set_ra_current_user_message("Logging in...".into());
let weak_app = app.as_weak();
tokio::spawn(async move {
rx.await.unwrap();
weak_app
.upgrade_in_event_loop(move |handle| {
if is_user_logged_in() {
handle.set_ra_current_user_message(
format!("Logged in as {}", handle.get_ra_username()).into(),
);
handle.set_ra_show_profile(true);
} else {
handle.set_ra_current_user_message("Login failed".into());
handle.set_ra_show_profile(false);
}
handle.set_ra_logging_in(false);
})
.unwrap();
});
}
pub fn ra_window(app: &ui::gui::AppWindow) {
let mut token = String::new();
if let Ok(ra_config) = std::fs::read(ui::get_dirs().config_dir.join("retroachievements.json"))
&& let Ok(result) = serde_json::from_slice::<RAConfig>(ra_config.as_ref())
{
app.set_ra_username(result.username.into());
app.set_ra_enabled(result.enabled);
app.set_ra_hardcore(result.hardcore);
app.set_ra_challenge(result.challenge);
app.set_ra_leaderboard(result.leaderboard);
token = result.token;
} else {
app.set_ra_hardcore(true);
}
if app.get_ra_enabled() && !app.get_ra_username().is_empty() {
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
login_token_user(app.get_ra_username().to_string(), token, tx);
set_current_user_message(app, rx);
} else {
app.set_ra_current_user_message("Not currently logged in".into());
}
let weak_app2 = app.as_weak();
app.on_ra_button_clicked(move |password| {
weak_app2
.upgrade_in_event_loop(move |handle| {
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
login_user(
handle.get_ra_username().to_string(),
password.to_string(),
tx,
);
set_current_user_message(&handle, rx);
})
.unwrap();
});
app.on_ra_toggled(move |enabled, hardcore, challenge, leaderboard| {
let file_path = ui::get_dirs().config_dir.join("retroachievements.json");
let raconfig = if let Ok(ra_config) = std::fs::read(&file_path)
&& let Ok(result) = serde_json::from_slice::<RAConfig>(ra_config.as_ref())
{
if !enabled {
unsafe { ra_logout_user() };
RAConfig {
username: String::new(),
token: String::new(),
enabled,
hardcore,
challenge,
leaderboard,
}
} else {
RAConfig {
username: result.username,
token: result.token,
enabled,
hardcore,
challenge,
leaderboard,
}
}
} else {
RAConfig {
username: String::new(),
token: String::new(),
enabled,
hardcore,
challenge,
leaderboard,
}
};
let f = std::fs::File::create(&file_path).unwrap();
serde_json::to_writer_pretty(f, &raconfig).unwrap();
});
app.on_ra_games_clicked(move || {
open::that_detached("https://retroachievements.org/system/2-nintendo-64/games").unwrap();
});
app.on_ra_show_profile_clicked(move |username| {
open::that_detached(format!("https://retroachievements.org/user/{}", username)).unwrap();
});
}
pub async fn load_game(rom: &[u8], rom_size: usize) {
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
unsafe {
@ -299,6 +189,11 @@ pub fn login_user(username: String, password: String, tx: tokio::sync::oneshot::
};
}
#[cfg(feature = "gui")]
pub fn logout_user() {
unsafe { ra_logout_user() };
}
pub fn login_token_user(username: String, token: String, tx: tokio::sync::oneshot::Sender<bool>) {
unsafe {
let tx_ptr = Box::into_raw(Box::new(tx)) as *mut std::ffi::c_void;
@ -308,10 +203,12 @@ pub fn login_token_user(username: String, token: String, tx: tokio::sync::onesho
};
}
#[cfg(feature = "gui")]
pub fn is_user_logged_in() -> bool {
unsafe { ra_is_user_logged_in() }
}
#[cfg(feature = "gui")]
pub fn get_username() -> &'static str {
let c_username = unsafe { ra_get_username() };
if c_username.is_null() {
@ -323,6 +220,7 @@ pub fn get_username() -> &'static str {
}
}
#[cfg(feature = "gui")]
pub fn get_token() -> &'static str {
let c_token = unsafe { ra_get_token() };
if c_token.is_null() {

View File

@ -1,12 +1,18 @@
pub mod audio;
#[cfg(feature = "gui")]
pub mod cheats;
pub mod config;
#[cfg(feature = "gui")]
pub mod gui;
pub mod input;
#[cfg(feature = "gui")]
pub mod netplay;
#[cfg(feature = "gui")]
pub mod retroachievements;
pub mod storage;
pub mod usb;
pub mod video;
#[cfg(feature = "gui")]
pub mod vru;
pub struct Dirs {
@ -47,6 +53,14 @@ pub struct Usb {
pub cart_rx: Option<tokio::sync::broadcast::Receiver<usb::UsbData>>,
}
#[derive(Clone)]
pub struct GameSettings {
pub overclock: bool,
pub disable_expansion_pak: bool,
pub cheats: std::collections::HashMap<String, Option<String>>,
pub load_savestate_slot: Option<u32>,
}
pub struct Ui {
pub dirs: Dirs,
pub config: config::Config,

View File

@ -1,18 +1,10 @@
use crate::cheats;
use crate::device;
use crate::ui;
use crate::ui::gui::AppWindow;
use crate::ui::gui::ErrorDialog;
use slint::ComponentHandle;
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct CheatData {
note: String,
pub data: Vec<String>,
pub options: Option<std::collections::BTreeMap<String, String>>,
}
pub type Cheats = std::collections::BTreeMap<String, std::collections::BTreeMap<String, CheatData>>;
pub fn cheats_window(app: &AppWindow) {
let weak = app.as_weak();
app.on_cheats_select_rom_clicked(move |rom_dir| {
@ -38,7 +30,7 @@ pub fn cheats_window(app: &AppWindow) {
handle.set_cheat_game_name(game_name.into());
})
.unwrap();
let cheats: Cheats =
let cheats: cheats::Cheat =
serde_json::from_slice(include_bytes!("../../data/cheats.json")).unwrap();
if let Some(cheat) = cheats.get(&game_crc) {
let cheat = cheat.clone();

View File

@ -18,14 +18,6 @@ pub struct NetplayDevice {
pub player_number: u8,
}
#[derive(Clone)]
pub struct GameSettings {
pub overclock: bool,
pub disable_expansion_pak: bool,
pub cheats: std::collections::HashMap<String, Option<String>>,
pub load_savestate_slot: Option<u32>,
}
#[derive(Clone)]
pub struct RASettings {
pub enabled: bool,
@ -358,7 +350,7 @@ fn about_window(app: &AppWindow) {
pub fn app_window() {
let app = AppWindow::new().unwrap();
about_window(&app);
retroachievements::ra_window(&app);
ui::retroachievements::ra_window(&app);
let mut controller_paths;
{
let game_ui = ui::Ui::new();
@ -378,7 +370,7 @@ pub fn app_window() {
pub fn run_rom(
file_path: std::path::PathBuf,
game_settings: GameSettings,
game_settings: ui::GameSettings,
netplay: Option<NetplayDevice>,
ra_settings: RASettings,
weak: slint::Weak<AppWindow>,
@ -473,7 +465,7 @@ fn open_rom(app: &AppWindow) {
if let Some(file) = select_rom.await {
run_rom(
file.path().to_path_buf(),
GameSettings {
ui::GameSettings {
overclock,
disable_expansion_pak,
cheats: std::collections::HashMap::new(), // will be filled in later

View File

@ -384,6 +384,7 @@ pub fn get_controller_names(game_ui: &ui::Ui) -> Vec<String> {
controllers
}
#[cfg(feature = "gui")]
pub fn get_controller_paths(game_ui: &ui::Ui) -> Vec<Option<String>> {
let mut controller_paths: Vec<Option<String>> = vec![];

View File

@ -1,8 +1,8 @@
use crate::device;
use crate::ui;
use crate::ui::gui::{
AppWindow, CustomNetplayServer, DispatcherDialog, ErrorDialog, GameSettings, NetplayCreate,
NetplayDevice, NetplayJoin, NetplayWait, RASettings, run_rom, save_settings,
AppWindow, CustomNetplayServer, DispatcherDialog, ErrorDialog, NetplayCreate, NetplayDevice,
NetplayJoin, NetplayWait, RASettings, run_rom, save_settings,
};
use futures::{SinkExt, StreamExt};
use sha2::{Digest, Sha256};
@ -247,7 +247,7 @@ fn show_custom_url_dialog(weak: slint::Weak<NetplayCreate>, server_url: slint::S
pub fn setup_create_window(
create_window: &NetplayCreate,
game_settings: GameSettings,
game_settings: ui::GameSettings,
ra_settings: RASettings,
rom_dir: slint::SharedString,
weak_app: slint::Weak<AppWindow>,
@ -613,7 +613,7 @@ fn create_session(
game_hash: String,
game_cheats: String,
password: String,
game_settings: GameSettings,
game_settings: ui::GameSettings,
ra_settings: RASettings,
weak_app: slint::Weak<AppWindow>,
weak: slint::Weak<NetplayCreate>,
@ -703,7 +703,7 @@ fn create_session(
handle.get_rom_path(),
message.player_name.as_ref().unwrap().into(),
session.port.unwrap(),
GameSettings {
ui::GameSettings {
overclock: overclock.parse().unwrap(),
disable_expansion_pak: disable_expansion_pak.parse().unwrap(),
cheats: serde_json::from_str(cheats).unwrap(),
@ -826,7 +826,7 @@ fn join_session(
handle.get_rom_path(),
message.player_name.as_ref().unwrap().into(),
session.port.unwrap(),
GameSettings {
ui::GameSettings {
overclock: overclock.parse().unwrap(),
disable_expansion_pak: disable_expansion_pak.parse().unwrap(),
cheats: serde_json::from_str(cheats).unwrap(),
@ -872,7 +872,7 @@ fn setup_wait_window(
rom_path: slint::SharedString,
player_name: slint::SharedString,
port: i32,
game_settings: GameSettings,
game_settings: ui::GameSettings,
ra_settings: RASettings,
peer_addr: slint::SharedString,
weak_app: slint::Weak<AppWindow>,
@ -1090,7 +1090,7 @@ fn setup_wait_window(
run_rom(
handle.get_rom_path().as_str().into(),
GameSettings {
ui::GameSettings {
overclock: game_settings.overclock,
disable_expansion_pak: game_settings.disable_expansion_pak,
cheats: game_settings.cheats,
@ -1214,7 +1214,7 @@ pub fn netplay_window(app: &AppWindow, controller_paths: &[Option<String>]) {
save_settings(&handle, &controller_paths);
setup_create_window(
&create_window,
GameSettings {
ui::GameSettings {
overclock: handle.get_overclock_n64_cpu(),
disable_expansion_pak: handle.get_disable_expansion_pak(),
cheats: std::collections::HashMap::new(), // not used here

114
src/ui/retroachievements.rs Normal file
View File

@ -0,0 +1,114 @@
use crate::retroachievements;
use crate::ui;
use slint::ComponentHandle;
pub fn ra_window(app: &ui::gui::AppWindow) {
let mut token = String::new();
if let Ok(ra_config) = std::fs::read(ui::get_dirs().config_dir.join("retroachievements.json"))
&& let Ok(result) =
serde_json::from_slice::<retroachievements::RAConfig>(ra_config.as_ref())
{
app.set_ra_username(result.username.into());
app.set_ra_enabled(result.enabled);
app.set_ra_hardcore(result.hardcore);
app.set_ra_challenge(result.challenge);
app.set_ra_leaderboard(result.leaderboard);
token = result.token;
} else {
app.set_ra_hardcore(true);
}
if app.get_ra_enabled() && !app.get_ra_username().is_empty() {
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
retroachievements::login_token_user(app.get_ra_username().to_string(), token, tx);
set_current_user_message(app, rx);
} else {
app.set_ra_current_user_message("Not currently logged in".into());
}
let weak_app2 = app.as_weak();
app.on_ra_button_clicked(move |password| {
weak_app2
.upgrade_in_event_loop(move |handle| {
let (tx, rx) = tokio::sync::oneshot::channel::<bool>();
retroachievements::login_user(
handle.get_ra_username().to_string(),
password.to_string(),
tx,
);
set_current_user_message(&handle, rx);
})
.unwrap();
});
app.on_ra_toggled(move |enabled, hardcore, challenge, leaderboard| {
let file_path = ui::get_dirs().config_dir.join("retroachievements.json");
let raconfig = if let Ok(ra_config) = std::fs::read(&file_path)
&& let Ok(result) =
serde_json::from_slice::<retroachievements::RAConfig>(ra_config.as_ref())
{
if !enabled {
retroachievements::logout_user();
retroachievements::RAConfig {
username: String::new(),
token: String::new(),
enabled,
hardcore,
challenge,
leaderboard,
}
} else {
retroachievements::RAConfig {
username: result.username,
token: result.token,
enabled,
hardcore,
challenge,
leaderboard,
}
}
} else {
retroachievements::RAConfig {
username: String::new(),
token: String::new(),
enabled,
hardcore,
challenge,
leaderboard,
}
};
let f = std::fs::File::create(&file_path).unwrap();
serde_json::to_writer_pretty(f, &raconfig).unwrap();
});
app.on_ra_games_clicked(move || {
open::that_detached("https://retroachievements.org/system/2-nintendo-64/games").unwrap();
});
app.on_ra_show_profile_clicked(move |username| {
open::that_detached(format!("https://retroachievements.org/user/{}", username)).unwrap();
});
}
fn set_current_user_message(app: &ui::gui::AppWindow, rx: tokio::sync::oneshot::Receiver<bool>) {
app.set_ra_current_user_message("Logging in...".into());
let weak_app = app.as_weak();
tokio::spawn(async move {
rx.await.unwrap();
weak_app
.upgrade_in_event_loop(move |handle| {
if retroachievements::is_user_logged_in() {
handle.set_ra_current_user_message(
format!("Logged in as {}", handle.get_ra_username()).into(),
);
handle.set_ra_show_profile(true);
} else {
handle.set_ra_current_user_message("Login failed".into());
handle.set_ra_show_profile(false);
}
handle.set_ra_logging_in(false);
})
.unwrap();
});
}

View File

@ -1,5 +1,5 @@
use crate::gui;
use crate::retroachievements;
use crate::ui::gui;
use slint::ComponentHandle;
pub fn prompt_for_match(words: &[String], frame_time: f64) -> u16 {