diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 71b3dd3..327508f 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -5,6 +5,11 @@ use crate::types::{AppConfig, ThemePalette}; use crate::util; #[tauri::command] 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); } diff --git a/src-tauri/src/commands/game.rs b/src-tauri/src/commands/game.rs index bb7dc56..822131d 100644 --- a/src-tauri/src/commands/game.rs +++ b/src-tauri/src/commands/game.rs @@ -9,6 +9,7 @@ use tauri_plugin_opener::OpenerExt; use tokio::io::AsyncRead; use tokio::io::AsyncReadExt; use tokio::sync::Mutex; +use crate::commands::goldmapper; use crate::commands::runners; use crate::config; use crate::types::AppConfig; @@ -139,8 +140,14 @@ async fn launch_game_desktop( all_args.push(a.to_string()); } all_args.extend(args.clone()); - all_args.push(game_exe.to_string_lossy().to_string()); - all_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 { + 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 mut cmd = tokio::process::Command::new(&final_prog); 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 mut cmd = tokio::process::Command::new(&final_prog); @@ -289,13 +302,22 @@ async fn launch_game_desktop( not(target_os = "android") ))] { - let exe_str = game_exe.to_string_lossy().to_string(); - let all_args: Vec = extra_args.clone(); - let (final_prog, final_args) = apply_launch_prefix(&exe_str, all_args, &config_val); + let goldmapper_args = + goldmapper::build_launch_args(&app, &config_val, &game_exe, &extra_args)?; + let (prog_str, all_args): (String, Vec) = 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); for a in &final_args { cmd.arg(a); } + #[cfg(target_os = "windows")] + cmd.creation_flags(0x08000000); #[cfg(unix)] cmd.process_group(0); apply_launch_env_vars(&mut cmd, &config_val); diff --git a/src-tauri/src/commands/goldmapper.rs b/src-tauri/src/commands/goldmapper.rs new file mode 100644 index 0000000..a928c3c --- /dev/null +++ b/src-tauri/src/commands/goldmapper.rs @@ -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, name: String) { + mappings.push(GoldMapperMapping { + to: name.clone(), + from: name, + }); +} + +pub fn default_mappings() -> Vec { + 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 { + 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 { + 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>, 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 { + default_mappings() +} + +#[tauri::command] +pub fn goldmapper_load_config(app: AppHandle) -> Vec { + config::load_config_raw(app.clone()) + .goldmapper_mappings + .unwrap_or_else(default_mappings) +} + +#[tauri::command] +pub fn goldmapper_save_config( + app: AppHandle, + mappings: Vec, +) -> 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 { + let mut config_val = config::load_config_raw(app.clone()); + config_val.goldmapper_mappings = None; + config::save_config_raw(&app, &config_val); + default_mappings() +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 4bebe11..379e62f 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -4,6 +4,7 @@ pub mod dlc; pub mod download; pub mod file_dialogs; pub mod game; +pub mod goldmapper; pub mod java2lce; pub mod macos_setup; pub mod platform; diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 28bd1fb..566b439 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -40,6 +40,8 @@ pub fn load_config_raw(app: AppHandle) -> AppConfig { instance_launch_args: None, android_runner: None, android_audio_backend: None, + goldmapper_enabled: Some(true), + goldmapper_mappings: None, } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e4547ab..6b760be 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -21,6 +21,7 @@ use commands::dlc; use commands::download; use commands::file_dialogs; use commands::game; +use commands::goldmapper; use commands::macos_setup; use commands::platform as platform_cmd; use commands::plugins; @@ -149,6 +150,10 @@ pub fn run() { game::switch_proton, game::install_latest_driver, game::set_audio_backend, + goldmapper::goldmapper_get_defaults, + goldmapper::goldmapper_load_config, + goldmapper::goldmapper_save_config, + goldmapper::goldmapper_reset_config, ]) .setup(|app| { let app_handle = app.handle().clone(); diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index e877357..49bed28 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -39,6 +39,12 @@ pub struct InstanceLaunchArgs { pub args: Vec, } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct GoldMapperMapping { + pub from: String, + pub to: String, +} + #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct AppConfig { @@ -69,6 +75,8 @@ pub struct AppConfig { pub instance_launch_args: Option>, pub android_runner: Option, pub android_audio_backend: Option, + pub goldmapper_enabled: Option, + pub goldmapper_mappings: Option>, } #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 15c32ba..15bd370 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -58,7 +58,7 @@ "bundleMediaFramework": true } }, - "resources": ["resources/DLC", "resources/GoldMapperLib.dll", "resources/GoldMapperLauncher.exe"], + "resources": ["resources/DLC", "resources/GoldMapperLib.dll", "resources/GoldMapperLauncher.exe", "resources/SDL2.dll"], "icon": [ "icons/32x32.png", "icons/64x64.png", diff --git a/src/components/views/CreditsView.tsx b/src/components/views/CreditsView.tsx index 1dfda28..70a894d 100644 --- a/src/components/views/CreditsView.tsx +++ b/src/components/views/CreditsView.tsx @@ -295,7 +295,10 @@ const CreditsView = memo(function CreditsView() { roles: [ { role: "", - members: [{ name: "faisal508508", url: "#" }], + members: [ + { name: "faisal508508", url: "#" }, + { name: "HingedxHooligan", url: "#" }, + ], }, ], }, diff --git a/src/components/views/GoldMapperView.tsx b/src/components/views/GoldMapperView.tsx new file mode 100644 index 0000000..dabfff1 --- /dev/null +++ b/src/components/views/GoldMapperView.tsx @@ -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 = { + 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(KEY_FALLBACK); + const [controllerIds, setControllerIds] = + useState(CONTROLLER_FALLBACK); + const [binds, setBinds] = useState>({}); + const [editing, setEditing] = useState(null); + const [modalFocusIndex, setModalFocusIndex] = useState(0); + const [keyInput, setKeyInput] = useState(""); + const [keyInputError, setKeyInputError] = useState(null); + const [focusIndex, setFocusIndex] = useState(null); + const containerRef = useRef(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 = {}; + 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): 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 ( +
+ + {row.label} + +
+ ); + } + + const isReset = row.kind === "reset"; + const isEnable = row.kind === "enable"; + const focusIdx = nextFocusIndex(); + const focused = focusIndex === focusIdx; + + return ( + + ); + }; + + return ( + +
+ {rows.map(renderRow)} +
+ + + + {editing !== null && ( +
{ + if (e.target === e.currentTarget) closeModal(); + }} + > +
+

+ Assign {displayName(editing)} +

+ +
+
+

+ Mouse +

+
+ {MOUSE_IDS.map((id, i) => ( + + ))} +
+
+ +
+

+ Controller +

+
+ {controllerIds.map((id, i) => { + const idx = MOUSE_IDS.length + i; + return ( + + ); + })} +
+
+ +
+

+ Keyboard +

+ { + 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 && ( +

+ {keyInputError} +

+ )} +
+
+ + +
+
+ )} +
+ ); +}); + +export default GoldMapperView; diff --git a/src/components/views/SettingsView.tsx b/src/components/views/SettingsView.tsx index c2ee6d6..a9fd860 100644 --- a/src/components/views/SettingsView.tsx +++ b/src/components/views/SettingsView.tsx @@ -318,6 +318,17 @@ const SettingsView = memo(function SettingsView() { setFocusIndex(0); }, }); + if (!isAndroid) { + items.push({ + id: "controls_menu", + label: "Controls", + type: "button", + onClick: () => { + playPressSound(); + setActiveView("goldmapper"); + }, + }); + } items.push({ id: "plugins_menu", label: "Plugins", diff --git a/src/components/views/SkinsView.tsx b/src/components/views/SkinsView.tsx index 9139724..6e2019c 100644 --- a/src/components/views/SkinsView.tsx +++ b/src/components/views/SkinsView.tsx @@ -70,7 +70,7 @@ const DEFAULT_SKINS: SavedSkin[] = [ url: "/Skins/PrismaChunk0.png", 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: "huckle", name: "Huckle", url: "/Skins/huckle.png", isSlim: true }, { @@ -604,9 +604,9 @@ const SkinsView = memo(function SkinsView() { playPressSound(); setActiveView("skin-editor"); }} - className={`w-40 h-10 flex items-center - justify-center transition-colors text-2xl - mc-text-shadow outline-none border-none hover:text-[#FFFF55] + className={`w-40 h-10 flex items-center + justify-center transition-colors text-2xl + mc-text-shadow outline-none border-none hover:text-[#FFFF55] ${focusIndex === 2 ? "text-[#FFFF55]" : "text-white"}`} style={{ backgroundImage: diff --git a/src/context/LauncherContext.tsx b/src/context/LauncherContext.tsx index 7a13be5..13babbb 100644 --- a/src/context/LauncherContext.tsx +++ b/src/context/LauncherContext.tsx @@ -111,6 +111,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) { configRaw.instanceLaunchArgs, configRaw.androidRunner, configRaw.androidAudioBackend, + configRaw.goldmapperEnabled, ], ); @@ -213,6 +214,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) { startFullscreen: config.startFullscreen, skipIntro: config.skipIntro, instanceLaunchArgs: config.instanceLaunchArgs, + goldmapperEnabled: config.goldmapperEnabled, }).catch(console.error); } }, [ @@ -239,6 +241,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) { config.startFullscreen, config.skipIntro, config.instanceLaunchArgs, + config.goldmapperEnabled, ]); useEffect(() => { diff --git a/src/hooks/useAppConfig.ts b/src/hooks/useAppConfig.ts index 0b44df7..4a2ed64 100644 --- a/src/hooks/useAppConfig.ts +++ b/src/hooks/useAppConfig.ts @@ -1,6 +1,10 @@ import { useState, useEffect } from "react"; import { useLocalStorage } from "./useLocalStorage"; -import { TauriService, type CustomEdition } from "../services/TauriService"; +import { + TauriService, + type CustomEdition, + type GoldMapperMapping, +} from "../services/TauriService"; export function useAppConfig() { const [username, setUsername] = useLocalStorage("lce-username", "Steve"); const [theme, setTheme] = useLocalStorage("lce-theme", "Modern"); @@ -31,6 +35,8 @@ export function useAppConfig() { >({}); const [androidRunner, setAndroidRunner] = useLocalStorage("lce-android-runner", undefined); const [androidAudioBackend, setAndroidAudioBackend] = useLocalStorage<"alsa" | "pulseaudio">("lce-android-audio", "pulseaudio"); + const [goldmapperEnabled, setGoldmapperEnabled] = useLocalStorage("lce-goldmapper", true); + const [goldmapperMappings, setGoldmapperMappings] = useState(); useEffect(() => { TauriService.loadConfig().then((config) => { if (config.username) setUsername(config.username); @@ -57,6 +63,8 @@ export function useAppConfig() { if (config.instanceLaunchArgs) setInstanceLaunchArgs(config.instanceLaunchArgs); if (config.androidRunner) setAndroidRunner(config.androidRunner); if (config.androidAudioBackend) setAndroidAudioBackend(config.androidAudioBackend); + if (config.goldmapperEnabled !== undefined) setGoldmapperEnabled(config.goldmapperEnabled); + if (config.goldmapperMappings) setGoldmapperMappings(config.goldmapperMappings); setIsLoaded(true); }); }, []); @@ -87,9 +95,11 @@ export function useAppConfig() { instanceLaunchArgs, androidRunner, androidAudioBackend, + goldmapperEnabled, + goldmapperMappings, }).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 { username, @@ -145,5 +155,9 @@ export function useAppConfig() { setAndroidRunner, androidAudioBackend, setAndroidAudioBackend, + goldmapperEnabled, + setGoldmapperEnabled, + goldmapperMappings, + setGoldmapperMappings, }; } diff --git a/src/hooks/useGameManager.ts b/src/hooks/useGameManager.ts index b08d38c..b0d8e66 100644 --- a/src/hooks/useGameManager.ts +++ b/src/hooks/useGameManager.ts @@ -74,12 +74,12 @@ export const BASE_EDITIONS = [ id: "360revived", name: "360 Revived", 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", supportsSlimSkins: false, logo: "/images/360_revived.png", 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", diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 57b51fa..7de3617 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -3,6 +3,7 @@ import { motion, AnimatePresence, MotionConfig } from "framer-motion"; import "../css/App.css"; import HomeView from "../components/views/HomeView"; import SettingsView from "../components/views/SettingsView"; +import GoldMapperView from "../components/views/GoldMapperView"; import VersionsView from "../components/views/VersionsView"; import DevtoolsView from "../components/views/DevtoolsView"; import GuidesView from "../components/views/GuidesView"; @@ -305,6 +306,7 @@ export default function App() { "options-editor", "model-editor", "swf-editor", + "goldmapper", ]); useEffect(() => { const handleContextMenu = (e: MouseEvent) => e.preventDefault(); @@ -671,6 +673,9 @@ export default function App() { {activeView === "settings" && ( )} + {activeView === "goldmapper" && ( + + )} {activeView === "versions" && ( )} diff --git a/src/services/TauriService.ts b/src/services/TauriService.ts index 34150a6..5299353 100644 --- a/src/services/TauriService.ts +++ b/src/services/TauriService.ts @@ -23,6 +23,11 @@ export interface CustomEdition { logo?: string; } +export interface GoldMapperMapping { + from: string; + to: string; +} + export interface AppConfig { username: string; linuxRunner?: string; @@ -54,6 +59,8 @@ export interface AppConfig { >; androidRunner?: string; androidAudioBackend?: "alsa" | "pulseaudio"; + goldmapperEnabled?: boolean; + goldmapperMappings?: GoldMapperMapping[]; } export interface ThemePalette { @@ -494,4 +501,22 @@ export class TauriService { static async setAudioBackend(backend: string): Promise { return invoke("set_audio_backend", { backend }); } + + static async goldMapperGetDefaults(): Promise { + return invoke("goldmapper_get_defaults"); + } + + static async goldMapperLoadConfig(): Promise { + return invoke("goldmapper_load_config"); + } + + static async goldMapperSaveConfig( + mappings: GoldMapperMapping[], + ): Promise { + return invoke("goldmapper_save_config", { mappings }); + } + + static async goldMapperResetConfig(): Promise { + return invoke("goldmapper_reset_config"); + } }