gui (#24)
* gui * name * more * more * fix * warn * more * remove dnd * more * more * more * more * more * more * more * more * more * linter * more * more * more * more * linter * more * linter * fix * more
This commit is contained in:
parent
268048f76c
commit
cce163921e
@ -13,9 +13,11 @@ sevenz-rust = "0.5"
|
||||
chrono = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
egui = "0.23"
|
||||
eframe = "0.23"
|
||||
rfd = {version = "0.12", default-features = false, features = ["xdg-portal"] }
|
||||
sha2 = "0.10"
|
||||
rusttype = "0.9"
|
||||
futures = "0.3"
|
||||
sdl2-sys = { git = "https://github.com/Rust-SDL2/rust-sdl2.git" }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
|
||||
|
||||
105
src/device.rs
105
src/device.rs
@ -1,4 +1,6 @@
|
||||
use crate::ui;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
|
||||
pub mod ai;
|
||||
pub mod cache;
|
||||
@ -32,6 +34,109 @@ pub mod tlb;
|
||||
pub mod unmapped;
|
||||
pub mod vi;
|
||||
|
||||
pub fn run_game(file_path: &std::path::Path, device: &mut Device, fullscreen: bool) {
|
||||
let rom_contents = get_rom_contents(file_path);
|
||||
|
||||
cart_rom::init(device, rom_contents); // cart needs to come before rdram
|
||||
|
||||
// rdram pointer is shared with parallel-rdp
|
||||
let (rdram_ptr, rdram_size) = rdram::init(device);
|
||||
|
||||
ui::audio::init(&mut device.ui, 33600);
|
||||
ui::video::init(&mut device.ui, rdram_ptr, rdram_size, fullscreen);
|
||||
ui::input::init(&mut device.ui);
|
||||
|
||||
mi::init(device);
|
||||
pif::init(device);
|
||||
memory::init(device);
|
||||
rsp_interface::init(device);
|
||||
rdp::init(device);
|
||||
vi::init(device);
|
||||
cpu::init(device);
|
||||
|
||||
ui::storage::init(&mut device.ui);
|
||||
ui::storage::load_saves(&mut device.ui);
|
||||
cart_rom::load_rom_save(device);
|
||||
|
||||
cpu::run(device);
|
||||
}
|
||||
|
||||
fn swap_rom(contents: Vec<u8>) -> Vec<u8> {
|
||||
let test = u32::from_be_bytes(contents[0..4].try_into().unwrap());
|
||||
if test == 0x80371240 {
|
||||
// z64
|
||||
contents
|
||||
} else if test == 0x37804012 {
|
||||
// v64
|
||||
let mut data: Vec<u8> = vec![0; contents.len()];
|
||||
for i in (0..contents.len()).step_by(2) {
|
||||
let temp = u16::from_ne_bytes(contents[i..i + 2].try_into().unwrap());
|
||||
data[i..i + 2].copy_from_slice(&temp.to_be_bytes());
|
||||
}
|
||||
return data;
|
||||
} else if test == 0x40123780 {
|
||||
// n64
|
||||
let mut data: Vec<u8> = vec![0; contents.len()];
|
||||
for i in (0..contents.len()).step_by(4) {
|
||||
let temp = u32::from_ne_bytes(contents[i..i + 4].try_into().unwrap());
|
||||
data[i..i + 4].copy_from_slice(&temp.to_be_bytes());
|
||||
}
|
||||
return data;
|
||||
} else {
|
||||
panic!("unknown rom format")
|
||||
}
|
||||
}
|
||||
|
||||
fn get_rom_contents(file_path: &std::path::Path) -> Vec<u8> {
|
||||
let mut contents = vec![];
|
||||
if file_path.extension().unwrap().to_ascii_lowercase() == "zip" {
|
||||
let zip_file = fs::File::open(file_path).unwrap();
|
||||
let mut archive = zip::ZipArchive::new(zip_file).unwrap();
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i).unwrap();
|
||||
let extension = file
|
||||
.enclosed_name()
|
||||
.unwrap()
|
||||
.extension()
|
||||
.unwrap()
|
||||
.to_ascii_lowercase();
|
||||
if extension == "z64" || extension == "n64" || extension == "v64" {
|
||||
file.read_to_end(&mut contents)
|
||||
.expect("could not read zip file");
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if file_path.extension().unwrap().to_ascii_lowercase() == "7z" {
|
||||
let mut archive =
|
||||
sevenz_rust::SevenZReader::open(file_path, sevenz_rust::Password::empty()).unwrap();
|
||||
|
||||
let mut found = false;
|
||||
archive
|
||||
.for_each_entries(
|
||||
&mut |entry: &sevenz_rust::SevenZArchiveEntry, reader: &mut dyn std::io::Read| {
|
||||
let name = entry.name().to_ascii_lowercase();
|
||||
if !found
|
||||
&& (name.ends_with("z64") || name.ends_with("n64") || name.ends_with("v64"))
|
||||
{
|
||||
reader
|
||||
.read_to_end(&mut contents)
|
||||
.expect("could not read zip file");
|
||||
found = true;
|
||||
} else {
|
||||
//skip other files
|
||||
std::io::copy(reader, &mut std::io::sink())?;
|
||||
}
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.expect("ok");
|
||||
} else {
|
||||
contents = fs::read(file_path).expect("Should have been able to read the file");
|
||||
}
|
||||
|
||||
swap_rom(contents)
|
||||
}
|
||||
|
||||
pub struct Device {
|
||||
pub ui: ui::Ui,
|
||||
byte_swap: usize,
|
||||
|
||||
221
src/main.rs
221
src/main.rs
@ -1,7 +1,7 @@
|
||||
#![feature(round_ties_even)]
|
||||
#![feature(string_remove_matches)]
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
mod device;
|
||||
mod ui;
|
||||
use clap::Parser;
|
||||
@ -55,158 +55,79 @@ struct Args {
|
||||
clear_input_bindings: bool,
|
||||
}
|
||||
|
||||
fn swap_rom(contents: Vec<u8>) -> Vec<u8> {
|
||||
let test = u32::from_be_bytes(contents[0..4].try_into().unwrap());
|
||||
if test == 0x80371240 {
|
||||
// z64
|
||||
contents
|
||||
} else if test == 0x37804012 {
|
||||
// v64
|
||||
let mut data: Vec<u8> = vec![0; contents.len()];
|
||||
for i in (0..contents.len()).step_by(2) {
|
||||
let temp = u16::from_ne_bytes(contents[i..i + 2].try_into().unwrap());
|
||||
data[i..i + 2].copy_from_slice(&temp.to_be_bytes());
|
||||
}
|
||||
return data;
|
||||
} else if test == 0x40123780 {
|
||||
// n64
|
||||
let mut data: Vec<u8> = vec![0; contents.len()];
|
||||
for i in (0..contents.len()).step_by(4) {
|
||||
let temp = u32::from_ne_bytes(contents[i..i + 4].try_into().unwrap());
|
||||
data[i..i + 4].copy_from_slice(&temp.to_be_bytes());
|
||||
}
|
||||
return data;
|
||||
} else {
|
||||
panic!("unknown rom format")
|
||||
}
|
||||
}
|
||||
fn main() {
|
||||
let cache_dir = dirs::cache_dir().unwrap().join("gopher64");
|
||||
|
||||
fn get_rom_contents(file_path: &std::path::Path) -> Vec<u8> {
|
||||
let mut contents = vec![];
|
||||
if file_path.extension().unwrap().to_ascii_lowercase() == "zip" {
|
||||
let zip_file = fs::File::open(file_path).unwrap();
|
||||
let mut archive = zip::ZipArchive::new(zip_file).unwrap();
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i).unwrap();
|
||||
let extension = file
|
||||
.enclosed_name()
|
||||
.unwrap()
|
||||
.extension()
|
||||
.unwrap()
|
||||
.to_ascii_lowercase();
|
||||
if extension == "z64" || extension == "n64" || extension == "v64" {
|
||||
file.read_to_end(&mut contents)
|
||||
.expect("could not read zip file");
|
||||
break;
|
||||
let _ = std::fs::create_dir_all(cache_dir.clone());
|
||||
let _ = std::fs::remove_file(cache_dir.clone().join("game_running"));
|
||||
|
||||
let args = Args::parse();
|
||||
let args_as_strings: Vec<String> = std::env::args().collect();
|
||||
let args_count = args_as_strings.len();
|
||||
if args_count > 1 {
|
||||
let mut device = device::Device::new();
|
||||
|
||||
if args.clear_input_bindings {
|
||||
ui::input::clear_bindings(&mut device.ui);
|
||||
return;
|
||||
}
|
||||
if args.port.is_some() {
|
||||
let port = args.port.unwrap();
|
||||
if !(1..=4).contains(&port) {
|
||||
println!("Port must be betwen 1 and 4");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if file_path.extension().unwrap().to_ascii_lowercase() == "7z" {
|
||||
let mut archive =
|
||||
sevenz_rust::SevenZReader::open(file_path, sevenz_rust::Password::empty()).unwrap();
|
||||
if args.list_controllers {
|
||||
ui::input::list_controllers(&device.ui);
|
||||
return;
|
||||
}
|
||||
if args.assign_controller.is_some() {
|
||||
if args.port.is_none() {
|
||||
println!("Must specify port number");
|
||||
return;
|
||||
}
|
||||
ui::input::assign_controller(
|
||||
&mut device.ui,
|
||||
args.assign_controller.unwrap(),
|
||||
args.port.unwrap(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if args.bind_input_profile.is_some() {
|
||||
if args.port.is_none() {
|
||||
println!("Must specify port number");
|
||||
return;
|
||||
}
|
||||
ui::input::bind_input_profile(
|
||||
&mut device.ui,
|
||||
args.bind_input_profile.unwrap(),
|
||||
args.port.unwrap(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if args.configure_input_profile.is_some() {
|
||||
ui::input::configure_input_profile(
|
||||
&mut device.ui,
|
||||
args.configure_input_profile.unwrap(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut found = false;
|
||||
archive
|
||||
.for_each_entries(
|
||||
&mut |entry: &sevenz_rust::SevenZArchiveEntry, reader: &mut dyn std::io::Read| {
|
||||
let name = entry.name().to_ascii_lowercase();
|
||||
if !found
|
||||
&& (name.ends_with("z64") || name.ends_with("n64") || name.ends_with("v64"))
|
||||
{
|
||||
reader
|
||||
.read_to_end(&mut contents)
|
||||
.expect("could not read zip file");
|
||||
found = true;
|
||||
} else {
|
||||
//skip other files
|
||||
std::io::copy(reader, &mut std::io::sink())?;
|
||||
}
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.expect("ok");
|
||||
if args.game.is_some() {
|
||||
let file_path = std::path::Path::new(args.game.as_ref().unwrap());
|
||||
device::run_game(file_path, &mut device, args.fullscreen);
|
||||
}
|
||||
} else {
|
||||
contents = fs::read(file_path).expect("Should have been able to read the file");
|
||||
}
|
||||
|
||||
swap_rom(contents)
|
||||
}
|
||||
|
||||
fn run_game(file_path: &std::path::Path, device: &mut device::Device, fullscreen: bool) {
|
||||
let rom_contents = get_rom_contents(file_path);
|
||||
|
||||
device::cart_rom::init(device, rom_contents); // cart needs to come before rdram
|
||||
|
||||
// rdram pointer is shared with parallel-rdp
|
||||
let (rdram_ptr, rdram_size) = device::rdram::init(device);
|
||||
|
||||
ui::audio::init(&mut device.ui, 33600);
|
||||
ui::video::init(&mut device.ui, rdram_ptr, rdram_size, fullscreen);
|
||||
ui::input::init(&mut device.ui);
|
||||
|
||||
device::mi::init(device);
|
||||
device::pif::init(device);
|
||||
device::memory::init(device);
|
||||
device::rsp_interface::init(device);
|
||||
device::rdp::init(device);
|
||||
device::vi::init(device);
|
||||
device::cpu::init(device);
|
||||
|
||||
ui::storage::init(&mut device.ui);
|
||||
ui::storage::load_saves(&mut device.ui);
|
||||
device::cart_rom::load_rom_save(device);
|
||||
|
||||
device::cpu::run(device);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
let mut device = device::Device::new();
|
||||
|
||||
if args.clear_input_bindings {
|
||||
ui::input::clear_bindings(&mut device.ui);
|
||||
return;
|
||||
}
|
||||
if args.port.is_some() {
|
||||
let port = args.port.unwrap();
|
||||
if !(1..=4).contains(&port) {
|
||||
println!("Port must be betwen 1 and 4");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if args.list_controllers {
|
||||
ui::input::list_controllers(&device.ui);
|
||||
return;
|
||||
}
|
||||
if args.assign_controller.is_some() {
|
||||
if args.port.is_none() {
|
||||
println!("Must specify port number");
|
||||
return;
|
||||
}
|
||||
ui::input::assign_controller(
|
||||
&mut device.ui,
|
||||
args.assign_controller.unwrap(),
|
||||
args.port.unwrap(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if args.bind_input_profile.is_some() {
|
||||
if args.port.is_none() {
|
||||
println!("Must specify port number");
|
||||
return;
|
||||
}
|
||||
ui::input::bind_input_profile(
|
||||
&mut device.ui,
|
||||
args.bind_input_profile.unwrap(),
|
||||
args.port.unwrap(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if args.configure_input_profile.is_some() {
|
||||
ui::input::configure_input_profile(&mut device.ui, args.configure_input_profile.unwrap());
|
||||
return;
|
||||
}
|
||||
if args.game.is_some() {
|
||||
let file_path = std::path::Path::new(args.game.as_ref().unwrap());
|
||||
run_game(file_path, &mut device, args.fullscreen);
|
||||
let options = eframe::NativeOptions {
|
||||
initial_window_size: Some(eframe::egui::vec2(640.0, 480.0)),
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
"gopher64",
|
||||
options,
|
||||
Box::new(|_cc| Box::new(ui::gui::GopherEguiApp::new())),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
pub mod audio;
|
||||
pub mod config;
|
||||
pub mod gui;
|
||||
pub mod input;
|
||||
pub mod storage;
|
||||
pub mod video;
|
||||
|
||||
220
src/ui/gui.rs
Normal file
220
src/ui/gui.rs
Normal file
@ -0,0 +1,220 @@
|
||||
use crate::device;
|
||||
use crate::ui;
|
||||
use eframe::egui;
|
||||
|
||||
pub struct GopherEguiApp {
|
||||
configure_profile: bool,
|
||||
profile_name: String,
|
||||
controllers: Vec<String>,
|
||||
selected_controller: [i32; 4],
|
||||
selected_profile: [String; 4],
|
||||
input_profiles: Vec<String>,
|
||||
}
|
||||
|
||||
fn get_input_profiles(game_ui: &ui::Ui) -> Vec<String> {
|
||||
let mut profiles = vec![];
|
||||
for key in game_ui.config.input.input_profiles.keys() {
|
||||
profiles.push((*key).clone())
|
||||
}
|
||||
profiles
|
||||
}
|
||||
|
||||
fn get_controllers(game_ui: &ui::Ui) -> Vec<String> {
|
||||
let mut controllers: Vec<String> = vec![];
|
||||
|
||||
let joystick_subsystem = game_ui.joystick_subsystem.as_ref().unwrap();
|
||||
let num_joysticks = joystick_subsystem.num_joysticks().unwrap();
|
||||
for i in 0..num_joysticks {
|
||||
controllers.push(joystick_subsystem.name_for_index(i).unwrap());
|
||||
}
|
||||
controllers
|
||||
}
|
||||
|
||||
impl GopherEguiApp {
|
||||
pub fn new() -> GopherEguiApp {
|
||||
let game_ui = ui::Ui::new();
|
||||
let joystick_subsystem = game_ui.joystick_subsystem.as_ref().unwrap();
|
||||
let num_joysticks = joystick_subsystem.num_joysticks().unwrap();
|
||||
let mut guids: Vec<String> = vec![];
|
||||
for i in 0..num_joysticks {
|
||||
guids.push(joystick_subsystem.device_guid(i).unwrap().to_string());
|
||||
}
|
||||
let mut selected_controller = [-1, -1, -1, -1];
|
||||
for (pos, item) in game_ui
|
||||
.config
|
||||
.input
|
||||
.controller_assignment
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
if item.is_some() {
|
||||
for (guid_pos, guid) in guids.iter().enumerate() {
|
||||
if item.as_deref().unwrap() == *guid {
|
||||
selected_controller[pos] = guid_pos as i32;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GopherEguiApp {
|
||||
configure_profile: false,
|
||||
profile_name: "".to_string(),
|
||||
selected_profile: game_ui.config.input.input_profile_binding.clone(),
|
||||
selected_controller,
|
||||
controllers: get_controllers(&game_ui),
|
||||
input_profiles: get_input_profiles(&game_ui),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_config(game_ui: &mut ui::Ui, selected_controller: [i32; 4], selected_profile: [String; 4]) {
|
||||
let joystick_subsystem = game_ui.joystick_subsystem.as_ref().unwrap();
|
||||
for (pos, item) in selected_controller.iter().enumerate() {
|
||||
if *item != -1 {
|
||||
game_ui.config.input.controller_assignment[pos] = Some(
|
||||
joystick_subsystem
|
||||
.device_guid(*item as u32)
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
);
|
||||
} else {
|
||||
game_ui.config.input.controller_assignment[pos] = None
|
||||
}
|
||||
}
|
||||
|
||||
game_ui.config.input.input_profile_binding = selected_profile;
|
||||
}
|
||||
|
||||
impl Drop for GopherEguiApp {
|
||||
fn drop(&mut self) {
|
||||
let mut game_ui = ui::Ui::new();
|
||||
save_config(
|
||||
&mut game_ui,
|
||||
self.selected_controller,
|
||||
self.selected_profile.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for GopherEguiApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
if self.configure_profile {
|
||||
egui::Window::new("Configure Input Profile")
|
||||
// .open(&mut self.configure_profile)
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let name_label = ui.label("Profile Name: ");
|
||||
ui.text_edit_singleline(&mut self.profile_name)
|
||||
.labelled_by(name_label.id);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Configure Profile").clicked() {
|
||||
let profile_name = self.profile_name.clone();
|
||||
execute(async {
|
||||
let mut game_ui = ui::Ui::new();
|
||||
ui::input::configure_input_profile(&mut game_ui, profile_name);
|
||||
});
|
||||
self.configure_profile = false;
|
||||
if !self.profile_name.is_empty() && self.profile_name != "default" {
|
||||
self.input_profiles.push(self.profile_name.clone())
|
||||
}
|
||||
};
|
||||
if ui.button("Close").clicked() {
|
||||
self.configure_profile = false
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.set_enabled(!self.configure_profile);
|
||||
|
||||
if ui.button("Open ROM").clicked() {
|
||||
// Spawn dialog on main thread
|
||||
let task = rfd::AsyncFileDialog::new().pick_file();
|
||||
let selected_controller = self.selected_controller;
|
||||
let selected_profile = self.selected_profile.clone();
|
||||
execute(async move {
|
||||
let file = task.await;
|
||||
|
||||
if let Some(file) = file {
|
||||
let running_file = dirs::cache_dir()
|
||||
.unwrap()
|
||||
.join("gopher64")
|
||||
.join("game_running");
|
||||
if running_file.exists() {
|
||||
return;
|
||||
}
|
||||
let _ = std::fs::File::create(running_file.clone());
|
||||
let mut device = device::Device::new();
|
||||
save_config(&mut device.ui, selected_controller, selected_profile);
|
||||
device::run_game(std::path::Path::new(file.path()), &mut device, false);
|
||||
let _ = std::fs::remove_file(running_file.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ui.button("Configure Input Profile").clicked() {
|
||||
let running_file = dirs::cache_dir()
|
||||
.unwrap()
|
||||
.join("gopher64")
|
||||
.join("game_running");
|
||||
if running_file.exists() {
|
||||
return;
|
||||
}
|
||||
self.configure_profile = true;
|
||||
}
|
||||
|
||||
ui.add_space(32.0);
|
||||
ui.label("Controller Config:");
|
||||
egui::Grid::new("some_unique_id").show(ui, |ui| {
|
||||
ui.label("Port");
|
||||
ui.label("Profile");
|
||||
ui.label("Controller");
|
||||
ui.end_row();
|
||||
for i in 0..4 {
|
||||
ui.label(format!("{}", i + 1));
|
||||
|
||||
egui::ComboBox::from_id_source(format!("profile-combo-{}", i))
|
||||
.selected_text(self.selected_profile[i].clone())
|
||||
.show_ui(ui, |ui| {
|
||||
for j in 0..self.input_profiles.len() {
|
||||
ui.selectable_value(
|
||||
&mut self.selected_profile[i],
|
||||
self.input_profiles[j].clone(),
|
||||
self.input_profiles[j].clone(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let controller_text = if self.selected_controller[i] == -1 {
|
||||
"None".to_string()
|
||||
} else {
|
||||
self.controllers[self.selected_controller[i] as usize].clone()
|
||||
};
|
||||
egui::ComboBox::from_id_source(format!("controller-combo-{}", i))
|
||||
.selected_text(controller_text)
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(
|
||||
&mut self.selected_controller[i],
|
||||
-1,
|
||||
"None".to_string(),
|
||||
);
|
||||
for j in 0..self.controllers.len() {
|
||||
ui.selectable_value(
|
||||
&mut self.selected_controller[i],
|
||||
j as i32,
|
||||
self.controllers[j].clone(),
|
||||
);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn execute<F: std::future::Future<Output = ()> + Send + 'static>(f: F) {
|
||||
std::thread::spawn(move || futures::executor::block_on(f));
|
||||
}
|
||||
@ -298,6 +298,10 @@ pub fn configure_input_profile(ui: &mut ui::Ui, profile: String) {
|
||||
println!("Profile name cannot be default");
|
||||
return;
|
||||
}
|
||||
if profile.is_empty() {
|
||||
println!("Profile name cannot be empty");
|
||||
return;
|
||||
}
|
||||
let joystick_subsystem = ui.sdl_context.as_ref().unwrap().joystick().unwrap();
|
||||
let mut joysticks = vec![];
|
||||
for i in 0..joystick_subsystem.num_joysticks().unwrap() {
|
||||
|
||||
Reference in New Issue
Block a user