mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-09-24 17:11:00 +00:00
feat: initial goldmapper support
This commit is contained in:
@@ -5,6 +5,11 @@ use crate::types::{AppConfig, ThemePalette};
|
|||||||
use crate::util;
|
use crate::util;
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn save_config(app: AppHandle, config_val: AppConfig) {
|
pub fn save_config(app: AppHandle, config_val: AppConfig) {
|
||||||
|
let mut config_val = config_val;
|
||||||
|
if config_val.goldmapper_mappings.is_none() {
|
||||||
|
let existing = config::load_config_raw(app.clone());
|
||||||
|
config_val.goldmapper_mappings = existing.goldmapper_mappings;
|
||||||
|
}
|
||||||
config::save_config_raw(&app, &config_val);
|
config::save_config_raw(&app, &config_val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use tauri_plugin_opener::OpenerExt;
|
|||||||
use tokio::io::AsyncRead;
|
use tokio::io::AsyncRead;
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
use crate::commands::goldmapper;
|
||||||
use crate::commands::runners;
|
use crate::commands::runners;
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::types::AppConfig;
|
use crate::types::AppConfig;
|
||||||
@@ -139,8 +140,14 @@ async fn launch_game_desktop(
|
|||||||
all_args.push(a.to_string());
|
all_args.push(a.to_string());
|
||||||
}
|
}
|
||||||
all_args.extend(args.clone());
|
all_args.extend(args.clone());
|
||||||
all_args.push(game_exe.to_string_lossy().to_string());
|
let goldmapper_args =
|
||||||
all_args.extend(extra_args.clone());
|
goldmapper::build_launch_args(&app, &config_val, &game_exe, &extra_args)?;
|
||||||
|
if let Some(gm_args) = goldmapper_args {
|
||||||
|
all_args.extend(gm_args);
|
||||||
|
} else {
|
||||||
|
all_args.push(game_exe.to_string_lossy().to_string());
|
||||||
|
all_args.extend(extra_args.clone());
|
||||||
|
}
|
||||||
let (final_prog, final_args) = apply_launch_prefix(prog, all_args, &config_val);
|
let (final_prog, final_args) = apply_launch_prefix(prog, all_args, &config_val);
|
||||||
let mut cmd = tokio::process::Command::new(&final_prog);
|
let mut cmd = tokio::process::Command::new(&final_prog);
|
||||||
for a in &final_args {
|
for a in &final_args {
|
||||||
@@ -228,7 +235,13 @@ async fn launch_game_desktop(
|
|||||||
])
|
])
|
||||||
};
|
};
|
||||||
|
|
||||||
mac_args.extend(extra_args.clone());
|
let goldmapper_args =
|
||||||
|
goldmapper::build_launch_args(&app, &config_val, &game_exe, &extra_args)?;
|
||||||
|
if let Some(gm_args) = goldmapper_args {
|
||||||
|
mac_args.extend(gm_args);
|
||||||
|
} else {
|
||||||
|
mac_args.extend(extra_args.clone());
|
||||||
|
}
|
||||||
|
|
||||||
let (final_prog, final_args) = apply_launch_prefix(&mac_prog, mac_args, &config_val);
|
let (final_prog, final_args) = apply_launch_prefix(&mac_prog, mac_args, &config_val);
|
||||||
let mut cmd = tokio::process::Command::new(&final_prog);
|
let mut cmd = tokio::process::Command::new(&final_prog);
|
||||||
@@ -289,13 +302,22 @@ async fn launch_game_desktop(
|
|||||||
not(target_os = "android")
|
not(target_os = "android")
|
||||||
))]
|
))]
|
||||||
{
|
{
|
||||||
let exe_str = game_exe.to_string_lossy().to_string();
|
let goldmapper_args =
|
||||||
let all_args: Vec<String> = extra_args.clone();
|
goldmapper::build_launch_args(&app, &config_val, &game_exe, &extra_args)?;
|
||||||
let (final_prog, final_args) = apply_launch_prefix(&exe_str, all_args, &config_val);
|
let (prog_str, all_args): (String, Vec<String>) = match goldmapper_args {
|
||||||
|
Some(gm_args) => (
|
||||||
|
gm_args[0].clone(),
|
||||||
|
gm_args[1..].to_vec(),
|
||||||
|
),
|
||||||
|
None => (game_exe.to_string_lossy().to_string(), extra_args.clone()),
|
||||||
|
};
|
||||||
|
let (final_prog, final_args) = apply_launch_prefix(&prog_str, all_args, &config_val);
|
||||||
let mut cmd = tokio::process::Command::new(&final_prog);
|
let mut cmd = tokio::process::Command::new(&final_prog);
|
||||||
for a in &final_args {
|
for a in &final_args {
|
||||||
cmd.arg(a);
|
cmd.arg(a);
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
cmd.creation_flags(0x08000000);
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
cmd.process_group(0);
|
cmd.process_group(0);
|
||||||
apply_launch_env_vars(&mut cmd, &config_val);
|
apply_launch_env_vars(&mut cmd, &config_val);
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
use crate::config;
|
||||||
|
use crate::types::{AppConfig, GoldMapperMapping};
|
||||||
|
use crate::util;
|
||||||
|
|
||||||
|
const CONTROLLER_TARGETS: [&str; 14] = [
|
||||||
|
"PAD_A",
|
||||||
|
"PAD_B",
|
||||||
|
"PAD_X",
|
||||||
|
"PAD_Y",
|
||||||
|
"PAD_LB",
|
||||||
|
"PAD_RB",
|
||||||
|
"PAD_BACK",
|
||||||
|
"PAD_START",
|
||||||
|
"PAD_LTHUMB",
|
||||||
|
"PAD_RTHUMB",
|
||||||
|
"PAD_DPAD_UP",
|
||||||
|
"PAD_DPAD_DOWN",
|
||||||
|
"PAD_DPAD_LEFT",
|
||||||
|
"PAD_DPAD_RIGHT",
|
||||||
|
];
|
||||||
|
|
||||||
|
fn push_key(mappings: &mut Vec<GoldMapperMapping>, name: String) {
|
||||||
|
mappings.push(GoldMapperMapping {
|
||||||
|
to: name.clone(),
|
||||||
|
from: name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_mappings() -> Vec<GoldMapperMapping> {
|
||||||
|
let mut mappings = Vec::new();
|
||||||
|
for b in b'A'..=b'Z' {
|
||||||
|
push_key(&mut mappings, format!("KEY_{}", b as char));
|
||||||
|
}
|
||||||
|
for n in 0..=9 {
|
||||||
|
push_key(&mut mappings, format!("KEY_{n}"));
|
||||||
|
}
|
||||||
|
for f in 1..=12 {
|
||||||
|
push_key(&mut mappings, format!("KEY_F{f}"));
|
||||||
|
}
|
||||||
|
for k in [
|
||||||
|
"KEY_SPACE",
|
||||||
|
"KEY_RETURN",
|
||||||
|
"KEY_ESCAPE",
|
||||||
|
"KEY_TAB",
|
||||||
|
"KEY_BACKSPACE",
|
||||||
|
"KEY_DELETE",
|
||||||
|
"KEY_INSERT",
|
||||||
|
"KEY_HOME",
|
||||||
|
"KEY_END",
|
||||||
|
"KEY_PAGEUP",
|
||||||
|
"KEY_PAGEDOWN",
|
||||||
|
"KEY_UP",
|
||||||
|
"KEY_DOWN",
|
||||||
|
"KEY_LEFT",
|
||||||
|
"KEY_RIGHT",
|
||||||
|
"KEY_PRINTSCREEN",
|
||||||
|
"KEY_PAUSE",
|
||||||
|
"KEY_CAPSLOCK",
|
||||||
|
"KEY_NUMLOCK",
|
||||||
|
"KEY_SCROLLLOCK",
|
||||||
|
"KEY_LSHIFT",
|
||||||
|
"KEY_RSHIFT",
|
||||||
|
"KEY_LCTRL",
|
||||||
|
"KEY_RCTRL",
|
||||||
|
"KEY_LALT",
|
||||||
|
"KEY_RALT",
|
||||||
|
"KEY_LWIN",
|
||||||
|
"KEY_RWIN",
|
||||||
|
"KEY_APPS",
|
||||||
|
] {
|
||||||
|
push_key(&mut mappings, k.to_string());
|
||||||
|
}
|
||||||
|
for n in 0..=9 {
|
||||||
|
push_key(&mut mappings, format!("KEY_NUMPAD{n}"));
|
||||||
|
}
|
||||||
|
for k in [
|
||||||
|
"KEY_MULTIPLY",
|
||||||
|
"KEY_ADD",
|
||||||
|
"KEY_SUBTRACT",
|
||||||
|
"KEY_DECIMAL",
|
||||||
|
"KEY_DIVIDE",
|
||||||
|
"KEY_SEMICOLON",
|
||||||
|
"KEY_EQUALS",
|
||||||
|
"KEY_COMMA",
|
||||||
|
"KEY_MINUS",
|
||||||
|
"KEY_PERIOD",
|
||||||
|
"KEY_SLASH",
|
||||||
|
"KEY_GRAVE",
|
||||||
|
"KEY_LBRACKET",
|
||||||
|
"KEY_BACKSLASH",
|
||||||
|
"KEY_RBRACKET",
|
||||||
|
"KEY_APOSTROPHE",
|
||||||
|
] {
|
||||||
|
push_key(&mut mappings, k.to_string());
|
||||||
|
}
|
||||||
|
for m in ["MOUSE_LEFT", "MOUSE_RIGHT", "MOUSE_MIDDLE"] {
|
||||||
|
push_key(&mut mappings, m.to_string());
|
||||||
|
}
|
||||||
|
for (i, target) in CONTROLLER_TARGETS.iter().enumerate() {
|
||||||
|
mappings.push(GoldMapperMapping {
|
||||||
|
from: format!("DINPUT_{i}"),
|
||||||
|
to: (*target).to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
mappings
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_resource(app: &AppHandle, name: &str) -> Option<PathBuf> {
|
||||||
|
use tauri::path::BaseDirectory;
|
||||||
|
if let Ok(p) = app.path().resolve(format!("resources/{name}"), BaseDirectory::Resource) {
|
||||||
|
if p.exists() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(current) = std::env::current_dir() {
|
||||||
|
let p = current.join("src-tauri").join("resources").join(name);
|
||||||
|
if p.exists() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
let p = current.join("resources").join(name);
|
||||||
|
if p.exists() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_runtime_config(app: &AppHandle, config_val: &AppConfig) -> Result<PathBuf, String> {
|
||||||
|
let dir = util::get_app_dir(app).join("GoldMapper");
|
||||||
|
fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||||
|
let path = dir.join("config.json");
|
||||||
|
let mappings = config_val
|
||||||
|
.goldmapper_mappings
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(default_mappings);
|
||||||
|
let json = serde_json::to_string(&serde_json::json!({ "mappings": mappings }))
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
fs::write(&path, json).map_err(|e| e.to_string())?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_launch_args(
|
||||||
|
app: &AppHandle,
|
||||||
|
config_val: &AppConfig,
|
||||||
|
game_exe: &PathBuf,
|
||||||
|
extra_args: &[String],
|
||||||
|
) -> Result<Option<Vec<String>>, String> {
|
||||||
|
if !config_val.goldmapper_enabled.unwrap_or(true) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let launcher =
|
||||||
|
resolve_resource(app, "GoldMapperLauncher.exe").ok_or_else(|| "GoldMapperLauncher.exe not found in resources".to_string())?;
|
||||||
|
let config_path = write_runtime_config(app, config_val)?;
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let mut args = vec![
|
||||||
|
launcher.to_string_lossy().to_string(),
|
||||||
|
config_path.to_string_lossy().to_string(),
|
||||||
|
game_exe.to_string_lossy().to_string(),
|
||||||
|
];
|
||||||
|
args.extend(extra_args.iter().cloned());
|
||||||
|
Ok(Some(args))
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let mut args = vec![
|
||||||
|
util::unix_path_to_wine_z_path(&launcher),
|
||||||
|
util::unix_path_to_wine_z_path(&config_path),
|
||||||
|
util::unix_path_to_wine_z_path(game_exe),
|
||||||
|
];
|
||||||
|
args.extend(extra_args.iter().cloned());
|
||||||
|
Ok(Some(args))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn goldmapper_get_defaults() -> Vec<GoldMapperMapping> {
|
||||||
|
default_mappings()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn goldmapper_load_config(app: AppHandle) -> Vec<GoldMapperMapping> {
|
||||||
|
config::load_config_raw(app.clone())
|
||||||
|
.goldmapper_mappings
|
||||||
|
.unwrap_or_else(default_mappings)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn goldmapper_save_config(
|
||||||
|
app: AppHandle,
|
||||||
|
mappings: Vec<GoldMapperMapping>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut config_val = config::load_config_raw(app.clone());
|
||||||
|
config_val.goldmapper_mappings = Some(mappings);
|
||||||
|
println!(
|
||||||
|
"[GoldMapper] config saved: {}",
|
||||||
|
serde_json::json!({ "mappings": config_val.goldmapper_mappings })
|
||||||
|
);
|
||||||
|
config::save_config_raw(&app, &config_val);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn goldmapper_reset_config(app: AppHandle) -> Vec<GoldMapperMapping> {
|
||||||
|
let mut config_val = config::load_config_raw(app.clone());
|
||||||
|
config_val.goldmapper_mappings = None;
|
||||||
|
config::save_config_raw(&app, &config_val);
|
||||||
|
default_mappings()
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ pub mod dlc;
|
|||||||
pub mod download;
|
pub mod download;
|
||||||
pub mod file_dialogs;
|
pub mod file_dialogs;
|
||||||
pub mod game;
|
pub mod game;
|
||||||
|
pub mod goldmapper;
|
||||||
pub mod java2lce;
|
pub mod java2lce;
|
||||||
pub mod macos_setup;
|
pub mod macos_setup;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ pub fn load_config_raw(app: AppHandle) -> AppConfig {
|
|||||||
instance_launch_args: None,
|
instance_launch_args: None,
|
||||||
android_runner: None,
|
android_runner: None,
|
||||||
android_audio_backend: None,
|
android_audio_backend: None,
|
||||||
|
goldmapper_enabled: Some(true),
|
||||||
|
goldmapper_mappings: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use commands::dlc;
|
|||||||
use commands::download;
|
use commands::download;
|
||||||
use commands::file_dialogs;
|
use commands::file_dialogs;
|
||||||
use commands::game;
|
use commands::game;
|
||||||
|
use commands::goldmapper;
|
||||||
use commands::macos_setup;
|
use commands::macos_setup;
|
||||||
use commands::platform as platform_cmd;
|
use commands::platform as platform_cmd;
|
||||||
use commands::plugins;
|
use commands::plugins;
|
||||||
@@ -149,6 +150,10 @@ pub fn run() {
|
|||||||
game::switch_proton,
|
game::switch_proton,
|
||||||
game::install_latest_driver,
|
game::install_latest_driver,
|
||||||
game::set_audio_backend,
|
game::set_audio_backend,
|
||||||
|
goldmapper::goldmapper_get_defaults,
|
||||||
|
goldmapper::goldmapper_load_config,
|
||||||
|
goldmapper::goldmapper_save_config,
|
||||||
|
goldmapper::goldmapper_reset_config,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let app_handle = app.handle().clone();
|
let app_handle = app.handle().clone();
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ pub struct InstanceLaunchArgs {
|
|||||||
pub args: Vec<String>,
|
pub args: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct GoldMapperMapping {
|
||||||
|
pub from: String,
|
||||||
|
pub to: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
@@ -69,6 +75,8 @@ pub struct AppConfig {
|
|||||||
pub instance_launch_args: Option<std::collections::HashMap<String, InstanceLaunchArgs>>,
|
pub instance_launch_args: Option<std::collections::HashMap<String, InstanceLaunchArgs>>,
|
||||||
pub android_runner: Option<String>,
|
pub android_runner: Option<String>,
|
||||||
pub android_audio_backend: Option<String>,
|
pub android_audio_backend: Option<String>,
|
||||||
|
pub goldmapper_enabled: Option<bool>,
|
||||||
|
pub goldmapper_mappings: Option<Vec<GoldMapperMapping>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
"bundleMediaFramework": true
|
"bundleMediaFramework": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resources": ["resources/DLC", "resources/GoldMapperLib.dll", "resources/GoldMapperLauncher.exe"],
|
"resources": ["resources/DLC", "resources/GoldMapperLib.dll", "resources/GoldMapperLauncher.exe", "resources/SDL2.dll"],
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/64x64.png",
|
"icons/64x64.png",
|
||||||
|
|||||||
@@ -295,7 +295,10 @@ const CreditsView = memo(function CreditsView() {
|
|||||||
roles: [
|
roles: [
|
||||||
{
|
{
|
||||||
role: "",
|
role: "",
|
||||||
members: [{ name: "faisal508508", url: "#" }],
|
members: [
|
||||||
|
{ name: "faisal508508", url: "#" },
|
||||||
|
{ name: "HingedxHooligan", url: "#" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,583 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback, useMemo, memo } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import {
|
||||||
|
TauriService,
|
||||||
|
type GoldMapperMapping,
|
||||||
|
} from "../../services/TauriService";
|
||||||
|
import { useUI, useConfig, useAudio } from "../../context/LauncherContext";
|
||||||
|
|
||||||
|
const EMERALD_ICON = "/images/emerald_0.png";
|
||||||
|
|
||||||
|
const KEY_FALLBACK = [
|
||||||
|
"KEY_A",
|
||||||
|
"KEY_D",
|
||||||
|
"KEY_S",
|
||||||
|
"KEY_W",
|
||||||
|
"KEY_SPACE",
|
||||||
|
"KEY_RETURN",
|
||||||
|
"KEY_ESCAPE",
|
||||||
|
"KEY_LSHIFT",
|
||||||
|
"KEY_LCTRL",
|
||||||
|
];
|
||||||
|
|
||||||
|
const MOUSE_IDS = ["MOUSE_LEFT", "MOUSE_MIDDLE", "MOUSE_RIGHT"];
|
||||||
|
|
||||||
|
const MOUSE_LABELS: Record<string, string> = {
|
||||||
|
MOUSE_LEFT: "Left Click",
|
||||||
|
MOUSE_MIDDLE: "Middle Click",
|
||||||
|
MOUSE_RIGHT: "Right Click",
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONTROLLER_FALLBACK = [
|
||||||
|
"PAD_A",
|
||||||
|
"PAD_B",
|
||||||
|
"PAD_X",
|
||||||
|
"PAD_Y",
|
||||||
|
"PAD_LB",
|
||||||
|
"PAD_RB",
|
||||||
|
"PAD_BACK",
|
||||||
|
"PAD_START",
|
||||||
|
"PAD_LTHUMB",
|
||||||
|
"PAD_RTHUMB",
|
||||||
|
"PAD_DPAD_UP",
|
||||||
|
"PAD_DPAD_DOWN",
|
||||||
|
"PAD_DPAD_LEFT",
|
||||||
|
"PAD_DPAD_RIGHT",
|
||||||
|
];
|
||||||
|
|
||||||
|
const DINPUT_ROWS: GoldMapperMapping[] = CONTROLLER_FALLBACK.map(
|
||||||
|
(target, i) => ({ from: `DINPUT_${i}`, to: target }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayName = (id: string) =>
|
||||||
|
id.replace(/^(KEY|PAD)_/, "").replace(/_/g, " ");
|
||||||
|
|
||||||
|
type Row =
|
||||||
|
| { kind: "reset"; key: string }
|
||||||
|
| { kind: "enable"; key: string }
|
||||||
|
| { kind: "header"; key: string; label: string }
|
||||||
|
| { kind: "bind"; key: string; id: string; label: string };
|
||||||
|
|
||||||
|
const stoneButtonStyle = (highlighted: boolean) => ({
|
||||||
|
backgroundImage: highlighted
|
||||||
|
? "url('/images/button_highlighted.png')"
|
||||||
|
: "url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
imageRendering: "pixelated" as const,
|
||||||
|
});
|
||||||
|
|
||||||
|
const GoldMapperView = memo(function GoldMapperView() {
|
||||||
|
const { setActiveView } = useUI();
|
||||||
|
const { animationsEnabled, goldmapperEnabled, setGoldmapperEnabled } =
|
||||||
|
useConfig();
|
||||||
|
const { playPressSound, playBackSound } = useAudio();
|
||||||
|
const [keyboardIds, setKeyboardIds] = useState<string[]>(KEY_FALLBACK);
|
||||||
|
const [controllerIds, setControllerIds] =
|
||||||
|
useState<string[]>(CONTROLLER_FALLBACK);
|
||||||
|
const [binds, setBinds] = useState<Record<string, string>>({});
|
||||||
|
const [editing, setEditing] = useState<string | null>(null);
|
||||||
|
const [modalFocusIndex, setModalFocusIndex] = useState(0);
|
||||||
|
const [keyInput, setKeyInput] = useState("");
|
||||||
|
const [keyInputError, setKeyInputError] = useState<string | null>(null);
|
||||||
|
const [focusIndex, setFocusIndex] = useState<number | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
TauriService.goldMapperGetDefaults()
|
||||||
|
.then((defaults) => {
|
||||||
|
setKeyboardIds(
|
||||||
|
defaults.filter((m) => m.from.startsWith("KEY_")).map((m) => m.from),
|
||||||
|
);
|
||||||
|
setControllerIds(
|
||||||
|
Array.from(
|
||||||
|
new Set(
|
||||||
|
defaults
|
||||||
|
.filter((m) => m.from.startsWith("DINPUT_"))
|
||||||
|
.map((m) => m.to),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
TauriService.goldMapperLoadConfig()
|
||||||
|
.then((rows) => {
|
||||||
|
const loaded: Record<string, string> = {};
|
||||||
|
for (const m of rows) {
|
||||||
|
if (/^(KEY|MOUSE|PAD)_/.test(m.from)) {
|
||||||
|
loaded[m.from] = m.to;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setBinds(loaded);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getTo = useCallback(
|
||||||
|
(id: string) => binds[id] ?? id,
|
||||||
|
[binds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const buildPayload = useCallback(
|
||||||
|
(nextBinds: Record<string, string>): GoldMapperMapping[] => {
|
||||||
|
const rows: GoldMapperMapping[] = [];
|
||||||
|
for (const id of [...keyboardIds, ...MOUSE_IDS, ...controllerIds]) {
|
||||||
|
const to = nextBinds[id] ?? id;
|
||||||
|
if (to !== id) {
|
||||||
|
rows.push({ from: id, to });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.push(...DINPUT_ROWS);
|
||||||
|
return rows;
|
||||||
|
},
|
||||||
|
[keyboardIds, controllerIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleResetToDefaults = useCallback(() => {
|
||||||
|
playPressSound();
|
||||||
|
setBinds({});
|
||||||
|
console.log("[GoldMapper] reset to defaults");
|
||||||
|
TauriService.goldMapperResetConfig().catch(console.error);
|
||||||
|
}, [playPressSound]);
|
||||||
|
|
||||||
|
const handleToggleEnabled = useCallback(() => {
|
||||||
|
playPressSound();
|
||||||
|
setGoldmapperEnabled(!goldmapperEnabled);
|
||||||
|
}, [playPressSound, goldmapperEnabled, setGoldmapperEnabled]);
|
||||||
|
|
||||||
|
const handleBack = useCallback(() => {
|
||||||
|
playBackSound();
|
||||||
|
setActiveView("settings");
|
||||||
|
}, [playBackSound, setActiveView]);
|
||||||
|
|
||||||
|
const openBind = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
playPressSound();
|
||||||
|
setEditing(id);
|
||||||
|
setModalFocusIndex(0);
|
||||||
|
setKeyInput(
|
||||||
|
/^KEY_/.test(getTo(id)) ? displayName(getTo(id)) : "",
|
||||||
|
);
|
||||||
|
setKeyInputError(null);
|
||||||
|
},
|
||||||
|
[playPressSound, getTo],
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeModal = useCallback(() => {
|
||||||
|
playBackSound();
|
||||||
|
(document.activeElement as HTMLElement | null)?.blur();
|
||||||
|
setEditing(null);
|
||||||
|
}, [playBackSound]);
|
||||||
|
|
||||||
|
const pickTarget = useCallback(
|
||||||
|
(sourceId: string, targetId: string) => {
|
||||||
|
playPressSound();
|
||||||
|
const next = { ...binds, [sourceId]: targetId };
|
||||||
|
setBinds(next);
|
||||||
|
const payload = buildPayload(next);
|
||||||
|
console.log(
|
||||||
|
"[GoldMapper] saving config:",
|
||||||
|
JSON.stringify({ mappings: payload }),
|
||||||
|
);
|
||||||
|
TauriService.goldMapperSaveConfig(payload).catch(console.error);
|
||||||
|
(document.activeElement as HTMLElement | null)?.blur();
|
||||||
|
setEditing(null);
|
||||||
|
},
|
||||||
|
[playPressSound, binds, buildPayload],
|
||||||
|
);
|
||||||
|
|
||||||
|
const submitKeyInput = useCallback(() => {
|
||||||
|
if (editing === null) return;
|
||||||
|
const norm = keyInput
|
||||||
|
.trim()
|
||||||
|
.toUpperCase()
|
||||||
|
.replace(/\s+/g, "_")
|
||||||
|
.replace(/^KEY_/, "");
|
||||||
|
if (!norm || !keyboardIds.includes(`KEY_${norm}`)) {
|
||||||
|
setKeyInputError("Unknown key name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pickTarget(editing, `KEY_${norm}`);
|
||||||
|
}, [editing, keyInput, keyboardIds, pickTarget]);
|
||||||
|
|
||||||
|
const rows: Row[] = useMemo(() => {
|
||||||
|
const list: Row[] = [{ kind: "reset", key: "reset" }];
|
||||||
|
list.push({ kind: "enable", key: "enable" });
|
||||||
|
list.push({ kind: "header", key: "controller_header", label: "Controller" });
|
||||||
|
for (const id of controllerIds) {
|
||||||
|
list.push({
|
||||||
|
kind: "bind",
|
||||||
|
key: `controller_${id}`,
|
||||||
|
id,
|
||||||
|
label: displayName(id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
list.push({ kind: "header", key: "mouse_header", label: "Mouse" });
|
||||||
|
for (const id of MOUSE_IDS) {
|
||||||
|
list.push({
|
||||||
|
kind: "bind",
|
||||||
|
key: `mouse_${id}`,
|
||||||
|
id,
|
||||||
|
label: MOUSE_LABELS[id],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
list.push({ kind: "header", key: "keyboard_header", label: "Keyboard" });
|
||||||
|
for (const id of keyboardIds) {
|
||||||
|
list.push({
|
||||||
|
kind: "bind",
|
||||||
|
key: `keyboard_${id}`,
|
||||||
|
id,
|
||||||
|
label: displayName(id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}, [controllerIds, keyboardIds]);
|
||||||
|
|
||||||
|
const focusableCount = rows.filter((r) => r.kind !== "header").length;
|
||||||
|
const modalItemCount = MOUSE_IDS.length + controllerIds.length + 2;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (editing !== null) {
|
||||||
|
const activeTag = document.activeElement?.tagName;
|
||||||
|
if (activeTag === "INPUT") {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
closeModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "ArrowDown" || e.key === "Tab") {
|
||||||
|
e.preventDefault();
|
||||||
|
setModalFocusIndex((prev) => (prev + 1) % modalItemCount);
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
setModalFocusIndex(
|
||||||
|
(prev) => (prev - 1 + modalItemCount) % modalItemCount,
|
||||||
|
);
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (modalFocusIndex < MOUSE_IDS.length + controllerIds.length) {
|
||||||
|
const allTargets = [...MOUSE_IDS, ...controllerIds];
|
||||||
|
pickTarget(editing, allTargets[modalFocusIndex]);
|
||||||
|
} else if (
|
||||||
|
modalFocusIndex ===
|
||||||
|
MOUSE_IDS.length + controllerIds.length
|
||||||
|
) {
|
||||||
|
submitKeyInput();
|
||||||
|
} else {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
handleBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
setFocusIndex((prev) =>
|
||||||
|
prev === null || prev >= focusableCount - 1 ? 0 : prev + 1,
|
||||||
|
);
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
setFocusIndex((prev) =>
|
||||||
|
prev === null || prev <= 0 ? focusableCount - 1 : prev - 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [
|
||||||
|
focusableCount,
|
||||||
|
handleBack,
|
||||||
|
editing,
|
||||||
|
closeModal,
|
||||||
|
modalItemCount,
|
||||||
|
modalFocusIndex,
|
||||||
|
controllerIds.length,
|
||||||
|
pickTarget,
|
||||||
|
submitKeyInput,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (focusIndex === null || editing !== null) return;
|
||||||
|
const el = containerRef.current?.querySelector(
|
||||||
|
`[data-focus-index="${focusIndex}"]`,
|
||||||
|
) as HTMLElement | null;
|
||||||
|
el?.focus();
|
||||||
|
}, [focusIndex, editing]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editing === null) return;
|
||||||
|
const el = document.querySelector(
|
||||||
|
`[data-modal-index="${modalFocusIndex}"]`,
|
||||||
|
) as HTMLElement | null;
|
||||||
|
el?.focus();
|
||||||
|
}, [modalFocusIndex, editing]);
|
||||||
|
|
||||||
|
let focusCounter = 0;
|
||||||
|
const nextFocusIndex = () => {
|
||||||
|
const idx = focusCounter;
|
||||||
|
focusCounter += 1;
|
||||||
|
return idx;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRow = (row: Row) => {
|
||||||
|
if (row.kind === "header") {
|
||||||
|
return (
|
||||||
|
<div key={row.key} className="w-full px-1 pt-3 pb-1">
|
||||||
|
<span className="text-sm text-black tracking-widest">
|
||||||
|
{row.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isReset = row.kind === "reset";
|
||||||
|
const isEnable = row.kind === "enable";
|
||||||
|
const focusIdx = nextFocusIndex();
|
||||||
|
const focused = focusIndex === focusIdx;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={row.key}
|
||||||
|
data-focus-index={focusIdx}
|
||||||
|
onFocus={() => setFocusIndex(focusIdx)}
|
||||||
|
onMouseEnter={() => setFocusIndex(focusIdx)}
|
||||||
|
onClick={
|
||||||
|
isReset
|
||||||
|
? handleResetToDefaults
|
||||||
|
: isEnable
|
||||||
|
? handleToggleEnabled
|
||||||
|
: () => openBind(row.id)
|
||||||
|
}
|
||||||
|
className={`w-full h-10 flex items-center pl-6 pr-4 outline-none border-none shrink-0 transition-colors ${
|
||||||
|
focused
|
||||||
|
? "text-[#ffff00]"
|
||||||
|
: isEnable
|
||||||
|
? "text-[#333333]"
|
||||||
|
: "text-white"
|
||||||
|
} ${isReset ? "justify-center hover:text-[#ffff00]" : ""}`}
|
||||||
|
style={isEnable ? undefined : stoneButtonStyle(focused)}
|
||||||
|
>
|
||||||
|
{isEnable && (
|
||||||
|
<div className="relative w-6 h-6 mr-3 shrink-0 flex items-center justify-center">
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
focused
|
||||||
|
? "/images/checkbox_highlighted.png"
|
||||||
|
: "/images/checkbox.png"
|
||||||
|
}
|
||||||
|
alt="checkbox"
|
||||||
|
className="absolute inset-0 w-full h-full object-contain"
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
|
{goldmapperEnabled && (
|
||||||
|
<img
|
||||||
|
src="/images/check.png"
|
||||||
|
alt="checked"
|
||||||
|
className="relative z-10 w-6 h-6 object-contain"
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`tracking-widest text-lg mc-text-shadow truncate ${
|
||||||
|
isReset ? "" : "flex-1 text-left"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isReset
|
||||||
|
? "Reset to Defaults"
|
||||||
|
: isEnable
|
||||||
|
? "Enable GoldMapper"
|
||||||
|
: row.label}
|
||||||
|
</span>
|
||||||
|
{!isReset &&
|
||||||
|
!isEnable &&
|
||||||
|
row.kind === "bind" && (
|
||||||
|
<span className="tracking-widest text-base opacity-70 ml-3 shrink-0">
|
||||||
|
{displayName(getTo(row.id))}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isEnable && (
|
||||||
|
<img
|
||||||
|
src={EMERALD_ICON}
|
||||||
|
alt=""
|
||||||
|
className="w-6 h-6 object-contain shrink-0 ml-3"
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
|
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||||
|
className="flex flex-col items-center w-full max-w-5xl"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="w-[720px] max-w-[92vw] h-[560px] max-h-[62vh] p-4 flex flex-col gap-2 overflow-y-auto settings-scrollbar mc-options-bg"
|
||||||
|
>
|
||||||
|
{rows.map(renderRow)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onMouseEnter={() => setFocusIndex(null)}
|
||||||
|
onClick={handleBack}
|
||||||
|
className="w-40 h-10 flex items-center justify-center transition-colors text-xl mc-text-shadow outline-none border-none hover:text-[#ffff00] mt-4 text-white"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{editing !== null && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 outline-none border-none"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
if (e.target === e.currentTarget) closeModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="relative w-[620px] max-w-[95vw] h-[580px] max-h-[88vh] p-5 flex flex-col font-['Mojangles'] mc-options-bg">
|
||||||
|
<h2 className="text-xl text-black mc-text-shadow mb-4 text-center">
|
||||||
|
Assign {displayName(editing)}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="w-full flex-1 min-h-0 overflow-y-auto custom-scrollbar mb-4">
|
||||||
|
<div className="mb-3">
|
||||||
|
<h3 className="text-[#333333] mc-text-shadow uppercase tracking-widest text-sm px-3 pt-2 pb-1">
|
||||||
|
Mouse
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-4 gap-2 p-1 content-start">
|
||||||
|
{MOUSE_IDS.map((id, i) => (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
data-modal-index={i}
|
||||||
|
onFocus={() => setModalFocusIndex(i)}
|
||||||
|
onMouseEnter={() => setModalFocusIndex(i)}
|
||||||
|
onClick={() => pickTarget(editing, id)}
|
||||||
|
className={`h-10 px-2 flex items-center justify-center text-sm tracking-widest outline-none border-none cursor-pointer transition-colors ${
|
||||||
|
getTo(editing) === id
|
||||||
|
? "text-[#ffff00]"
|
||||||
|
: "text-white"
|
||||||
|
}`}
|
||||||
|
style={stoneButtonStyle(modalFocusIndex === i)}
|
||||||
|
>
|
||||||
|
<span className="truncate">{MOUSE_LABELS[id]}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<h3 className="text-[#333333] mc-text-shadow uppercase tracking-widest text-sm px-3 pt-2 pb-1">
|
||||||
|
Controller
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-4 gap-2 p-1 content-start">
|
||||||
|
{controllerIds.map((id, i) => {
|
||||||
|
const idx = MOUSE_IDS.length + i;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
data-modal-index={idx}
|
||||||
|
onFocus={() => setModalFocusIndex(idx)}
|
||||||
|
onMouseEnter={() => setModalFocusIndex(idx)}
|
||||||
|
onClick={() => pickTarget(editing, id)}
|
||||||
|
className={`h-10 px-2 flex items-center justify-center text-sm tracking-widest outline-none border-none cursor-pointer transition-colors ${
|
||||||
|
getTo(editing) === id
|
||||||
|
? "text-[#ffff00]"
|
||||||
|
: "text-white"
|
||||||
|
}`}
|
||||||
|
style={stoneButtonStyle(modalFocusIndex === idx)}
|
||||||
|
>
|
||||||
|
<span className="truncate">{displayName(id)}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<h3 className="text-[#333333] mc-text-shadow uppercase tracking-widest text-sm px-3 pt-2 pb-1">
|
||||||
|
Keyboard
|
||||||
|
</h3>
|
||||||
|
<input
|
||||||
|
data-modal-index={MOUSE_IDS.length + controllerIds.length}
|
||||||
|
value={keyInput}
|
||||||
|
onChange={(e) => {
|
||||||
|
setKeyInput(e.target.value);
|
||||||
|
setKeyInputError(null);
|
||||||
|
}}
|
||||||
|
onFocus={() =>
|
||||||
|
setModalFocusIndex(MOUSE_IDS.length + controllerIds.length)
|
||||||
|
}
|
||||||
|
onMouseEnter={() =>
|
||||||
|
setModalFocusIndex(MOUSE_IDS.length + controllerIds.length)
|
||||||
|
}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") submitKeyInput();
|
||||||
|
}}
|
||||||
|
placeholder="Type a key name and press Enter"
|
||||||
|
className={`w-full h-10 px-3 bg-black/40 border-2 text-white text-base outline-none text-center ${
|
||||||
|
keyInputError
|
||||||
|
? "border-red-600"
|
||||||
|
: "border-[#373737] focus:border-[#FFFF55]"
|
||||||
|
}`}
|
||||||
|
style={{ imageRendering: "pixelated" }}
|
||||||
|
/>
|
||||||
|
{keyInputError && (
|
||||||
|
<p className="text-red-600 text-xs mt-1 px-3">
|
||||||
|
{keyInputError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
data-modal-index={MOUSE_IDS.length + controllerIds.length + 1}
|
||||||
|
onFocus={() =>
|
||||||
|
setModalFocusIndex(MOUSE_IDS.length + controllerIds.length + 1)
|
||||||
|
}
|
||||||
|
onMouseEnter={() =>
|
||||||
|
setModalFocusIndex(MOUSE_IDS.length + controllerIds.length + 1)
|
||||||
|
}
|
||||||
|
onClick={closeModal}
|
||||||
|
className={`w-full h-12 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none cursor-pointer ${
|
||||||
|
modalFocusIndex ===
|
||||||
|
MOUSE_IDS.length + controllerIds.length + 1
|
||||||
|
? "text-[#ffff00]"
|
||||||
|
: "text-white"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
modalFocusIndex ===
|
||||||
|
MOUSE_IDS.length + controllerIds.length + 1
|
||||||
|
? "url('/images/button_highlighted.png')"
|
||||||
|
: "url('/images/Button_Background.png')",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
imageRendering: "pixelated",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default GoldMapperView;
|
||||||
@@ -318,6 +318,17 @@ const SettingsView = memo(function SettingsView() {
|
|||||||
setFocusIndex(0);
|
setFocusIndex(0);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (!isAndroid) {
|
||||||
|
items.push({
|
||||||
|
id: "controls_menu",
|
||||||
|
label: "Controls",
|
||||||
|
type: "button",
|
||||||
|
onClick: () => {
|
||||||
|
playPressSound();
|
||||||
|
setActiveView("goldmapper");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
items.push({
|
items.push({
|
||||||
id: "plugins_menu",
|
id: "plugins_menu",
|
||||||
label: "Plugins",
|
label: "Plugins",
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const DEFAULT_SKINS: SavedSkin[] = [
|
|||||||
url: "/Skins/PrismaChunk0.png",
|
url: "/Skins/PrismaChunk0.png",
|
||||||
isSlim: false,
|
isSlim: false,
|
||||||
},
|
},
|
||||||
{ id: "amy", name: "Amy", url: "/Skins/amy.png", isSlim: true }, //neo: she's the best btw
|
{ id: "amy", name: "Amy", url: "/Skins/amy.png", isSlim: true }, //neo: :c
|
||||||
{ id: "avalilac", name: "AvaLilac", url: "/Skins/ava.png", isSlim: true },
|
{ id: "avalilac", name: "AvaLilac", url: "/Skins/ava.png", isSlim: true },
|
||||||
{ id: "huckle", name: "Huckle", url: "/Skins/huckle.png", isSlim: true },
|
{ id: "huckle", name: "Huckle", url: "/Skins/huckle.png", isSlim: true },
|
||||||
{
|
{
|
||||||
@@ -604,9 +604,9 @@ const SkinsView = memo(function SkinsView() {
|
|||||||
playPressSound();
|
playPressSound();
|
||||||
setActiveView("skin-editor");
|
setActiveView("skin-editor");
|
||||||
}}
|
}}
|
||||||
className={`w-40 h-10 flex items-center
|
className={`w-40 h-10 flex items-center
|
||||||
justify-center transition-colors text-2xl
|
justify-center transition-colors text-2xl
|
||||||
mc-text-shadow outline-none border-none hover:text-[#FFFF55]
|
mc-text-shadow outline-none border-none hover:text-[#FFFF55]
|
||||||
${focusIndex === 2 ? "text-[#FFFF55]" : "text-white"}`}
|
${focusIndex === 2 ? "text-[#FFFF55]" : "text-white"}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage:
|
backgroundImage:
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
|||||||
configRaw.instanceLaunchArgs,
|
configRaw.instanceLaunchArgs,
|
||||||
configRaw.androidRunner,
|
configRaw.androidRunner,
|
||||||
configRaw.androidAudioBackend,
|
configRaw.androidAudioBackend,
|
||||||
|
configRaw.goldmapperEnabled,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -213,6 +214,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
|||||||
startFullscreen: config.startFullscreen,
|
startFullscreen: config.startFullscreen,
|
||||||
skipIntro: config.skipIntro,
|
skipIntro: config.skipIntro,
|
||||||
instanceLaunchArgs: config.instanceLaunchArgs,
|
instanceLaunchArgs: config.instanceLaunchArgs,
|
||||||
|
goldmapperEnabled: config.goldmapperEnabled,
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
@@ -239,6 +241,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
|||||||
config.startFullscreen,
|
config.startFullscreen,
|
||||||
config.skipIntro,
|
config.skipIntro,
|
||||||
config.instanceLaunchArgs,
|
config.instanceLaunchArgs,
|
||||||
|
config.goldmapperEnabled,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useLocalStorage } from "./useLocalStorage";
|
import { useLocalStorage } from "./useLocalStorage";
|
||||||
import { TauriService, type CustomEdition } from "../services/TauriService";
|
import {
|
||||||
|
TauriService,
|
||||||
|
type CustomEdition,
|
||||||
|
type GoldMapperMapping,
|
||||||
|
} from "../services/TauriService";
|
||||||
export function useAppConfig() {
|
export function useAppConfig() {
|
||||||
const [username, setUsername] = useLocalStorage("lce-username", "Steve");
|
const [username, setUsername] = useLocalStorage("lce-username", "Steve");
|
||||||
const [theme, setTheme] = useLocalStorage("lce-theme", "Modern");
|
const [theme, setTheme] = useLocalStorage("lce-theme", "Modern");
|
||||||
@@ -31,6 +35,8 @@ export function useAppConfig() {
|
|||||||
>({});
|
>({});
|
||||||
const [androidRunner, setAndroidRunner] = useLocalStorage<string | undefined>("lce-android-runner", undefined);
|
const [androidRunner, setAndroidRunner] = useLocalStorage<string | undefined>("lce-android-runner", undefined);
|
||||||
const [androidAudioBackend, setAndroidAudioBackend] = useLocalStorage<"alsa" | "pulseaudio">("lce-android-audio", "pulseaudio");
|
const [androidAudioBackend, setAndroidAudioBackend] = useLocalStorage<"alsa" | "pulseaudio">("lce-android-audio", "pulseaudio");
|
||||||
|
const [goldmapperEnabled, setGoldmapperEnabled] = useLocalStorage("lce-goldmapper", true);
|
||||||
|
const [goldmapperMappings, setGoldmapperMappings] = useState<GoldMapperMapping[] | undefined>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
TauriService.loadConfig().then((config) => {
|
TauriService.loadConfig().then((config) => {
|
||||||
if (config.username) setUsername(config.username);
|
if (config.username) setUsername(config.username);
|
||||||
@@ -57,6 +63,8 @@ export function useAppConfig() {
|
|||||||
if (config.instanceLaunchArgs) setInstanceLaunchArgs(config.instanceLaunchArgs);
|
if (config.instanceLaunchArgs) setInstanceLaunchArgs(config.instanceLaunchArgs);
|
||||||
if (config.androidRunner) setAndroidRunner(config.androidRunner);
|
if (config.androidRunner) setAndroidRunner(config.androidRunner);
|
||||||
if (config.androidAudioBackend) setAndroidAudioBackend(config.androidAudioBackend);
|
if (config.androidAudioBackend) setAndroidAudioBackend(config.androidAudioBackend);
|
||||||
|
if (config.goldmapperEnabled !== undefined) setGoldmapperEnabled(config.goldmapperEnabled);
|
||||||
|
if (config.goldmapperMappings) setGoldmapperMappings(config.goldmapperMappings);
|
||||||
setIsLoaded(true);
|
setIsLoaded(true);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
@@ -87,9 +95,11 @@ export function useAppConfig() {
|
|||||||
instanceLaunchArgs,
|
instanceLaunchArgs,
|
||||||
androidRunner,
|
androidRunner,
|
||||||
androidAudioBackend,
|
androidAudioBackend,
|
||||||
|
goldmapperEnabled,
|
||||||
|
goldmapperMappings,
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
}, [username, theme, linuxRunner, perfBoost, profile, customEditions, customPaths, customizations, animationsEnabled, vfxEnabled, rpcEnabled, startFullscreen, musicVol, sfxVol, legacyMode, mangohudEnabled, extraLaunchArgs, launchPrefix, launchEnvVars, skipIntro, isLoaded, instanceLaunchArgs, androidRunner, androidAudioBackend]);
|
}, [username, theme, linuxRunner, perfBoost, profile, customEditions, customPaths, customizations, animationsEnabled, vfxEnabled, rpcEnabled, startFullscreen, musicVol, sfxVol, legacyMode, mangohudEnabled, extraLaunchArgs, launchPrefix, launchEnvVars, skipIntro, isLoaded, instanceLaunchArgs, androidRunner, androidAudioBackend, goldmapperEnabled, goldmapperMappings]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
username,
|
username,
|
||||||
@@ -145,5 +155,9 @@ export function useAppConfig() {
|
|||||||
setAndroidRunner,
|
setAndroidRunner,
|
||||||
androidAudioBackend,
|
androidAudioBackend,
|
||||||
setAndroidAudioBackend,
|
setAndroidAudioBackend,
|
||||||
|
goldmapperEnabled,
|
||||||
|
setGoldmapperEnabled,
|
||||||
|
goldmapperMappings,
|
||||||
|
setGoldmapperMappings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,12 +74,12 @@ export const BASE_EDITIONS = [
|
|||||||
id: "360revived",
|
id: "360revived",
|
||||||
name: "360 Revived",
|
name: "360 Revived",
|
||||||
desc: "PC port of Xbox 360 Edition TU19",
|
desc: "PC port of Xbox 360 Edition TU19",
|
||||||
url: "https://github.com/BlackHoleSpirit/360-Revived/releases/download/nightly/360-Revived.zip",
|
url: HIDDEN_INSTANCE_URL, //neo: was "https://github.com/BlackHoleSpirit/360-Revived/releases/download/nightly/360-Revived.zip"
|
||||||
titleImage: "/images/minecraft_title_360revived.png",
|
titleImage: "/images/minecraft_title_360revived.png",
|
||||||
supportsSlimSkins: false,
|
supportsSlimSkins: false,
|
||||||
logo: "/images/360_revived.png",
|
logo: "/images/360_revived.png",
|
||||||
panorama: "360revived",
|
panorama: "360revived",
|
||||||
hideOnAndroid: true, //neo: 360revived shows a black screen on Android
|
hideOnAndroid: true, //neo: [NOT EFFECT BECAUSE OF `url`] 360revived shows a black screen on Android
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "legacy_nether_fork",
|
id: "legacy_nether_fork",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { motion, AnimatePresence, MotionConfig } from "framer-motion";
|
|||||||
import "../css/App.css";
|
import "../css/App.css";
|
||||||
import HomeView from "../components/views/HomeView";
|
import HomeView from "../components/views/HomeView";
|
||||||
import SettingsView from "../components/views/SettingsView";
|
import SettingsView from "../components/views/SettingsView";
|
||||||
|
import GoldMapperView from "../components/views/GoldMapperView";
|
||||||
import VersionsView from "../components/views/VersionsView";
|
import VersionsView from "../components/views/VersionsView";
|
||||||
import DevtoolsView from "../components/views/DevtoolsView";
|
import DevtoolsView from "../components/views/DevtoolsView";
|
||||||
import GuidesView from "../components/views/GuidesView";
|
import GuidesView from "../components/views/GuidesView";
|
||||||
@@ -305,6 +306,7 @@ export default function App() {
|
|||||||
"options-editor",
|
"options-editor",
|
||||||
"model-editor",
|
"model-editor",
|
||||||
"swf-editor",
|
"swf-editor",
|
||||||
|
"goldmapper",
|
||||||
]);
|
]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleContextMenu = (e: MouseEvent) => e.preventDefault();
|
const handleContextMenu = (e: MouseEvent) => e.preventDefault();
|
||||||
@@ -671,6 +673,9 @@ export default function App() {
|
|||||||
{activeView === "settings" && (
|
{activeView === "settings" && (
|
||||||
<SettingsView key="settings-view" />
|
<SettingsView key="settings-view" />
|
||||||
)}
|
)}
|
||||||
|
{activeView === "goldmapper" && (
|
||||||
|
<GoldMapperView key="goldmapper-view" />
|
||||||
|
)}
|
||||||
{activeView === "versions" && (
|
{activeView === "versions" && (
|
||||||
<VersionsView key="versions-view" />
|
<VersionsView key="versions-view" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ export interface CustomEdition {
|
|||||||
logo?: string;
|
logo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GoldMapperMapping {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
username: string;
|
username: string;
|
||||||
linuxRunner?: string;
|
linuxRunner?: string;
|
||||||
@@ -54,6 +59,8 @@ export interface AppConfig {
|
|||||||
>;
|
>;
|
||||||
androidRunner?: string;
|
androidRunner?: string;
|
||||||
androidAudioBackend?: "alsa" | "pulseaudio";
|
androidAudioBackend?: "alsa" | "pulseaudio";
|
||||||
|
goldmapperEnabled?: boolean;
|
||||||
|
goldmapperMappings?: GoldMapperMapping[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThemePalette {
|
export interface ThemePalette {
|
||||||
@@ -494,4 +501,22 @@ export class TauriService {
|
|||||||
static async setAudioBackend(backend: string): Promise<void> {
|
static async setAudioBackend(backend: string): Promise<void> {
|
||||||
return invoke("set_audio_backend", { backend });
|
return invoke("set_audio_backend", { backend });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async goldMapperGetDefaults(): Promise<GoldMapperMapping[]> {
|
||||||
|
return invoke("goldmapper_get_defaults");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async goldMapperLoadConfig(): Promise<GoldMapperMapping[]> {
|
||||||
|
return invoke("goldmapper_load_config");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async goldMapperSaveConfig(
|
||||||
|
mappings: GoldMapperMapping[],
|
||||||
|
): Promise<void> {
|
||||||
|
return invoke("goldmapper_save_config", { mappings });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async goldMapperResetConfig(): Promise<GoldMapperMapping[]> {
|
||||||
|
return invoke("goldmapper_reset_config");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user