feat: initial goldmapper support

This commit is contained in:
neoapps-dev
2026-08-25 14:05:23 +03:00
parent d5a36ba3b2
commit 683339ae4e
17 changed files with 915 additions and 16 deletions
+5
View File
@@ -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);
}
+28 -6
View File
@@ -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<String> = 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<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);
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);
+212
View File
@@ -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()
}
+1
View File
@@ -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;
+2
View File
@@ -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,
}
}
+5
View File
@@ -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();
+8
View File
@@ -39,6 +39,12 @@ pub struct InstanceLaunchArgs {
pub args: Vec<String>,
}
#[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<std::collections::HashMap<String, InstanceLaunchArgs>>,
pub android_runner: Option<String>,
pub android_audio_backend: Option<String>,
pub goldmapper_enabled: Option<bool>,
pub goldmapper_mappings: Option<Vec<GoldMapperMapping>>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
+1 -1
View File
@@ -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",
+4 -1
View File
@@ -295,7 +295,10 @@ const CreditsView = memo(function CreditsView() {
roles: [
{
role: "",
members: [{ name: "faisal508508", url: "#" }],
members: [
{ name: "faisal508508", url: "#" },
{ name: "HingedxHooligan", url: "#" },
],
},
],
},
+583
View File
@@ -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;
+11
View File
@@ -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",
+4 -4
View File
@@ -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:
+3
View File
@@ -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(() => {
+16 -2
View File
@@ -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<string | undefined>("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<GoldMapperMapping[] | undefined>();
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,
};
}
+2 -2
View File
@@ -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",
+5
View File
@@ -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" && (
<SettingsView key="settings-view" />
)}
{activeView === "goldmapper" && (
<GoldMapperView key="goldmapper-view" />
)}
{activeView === "versions" && (
<VersionsView key="versions-view" />
)}
+25
View File
@@ -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<void> {
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");
}
}