From 4a0a329f6b7aee706a95911152a3f0e7ee38cd98 Mon Sep 17 00:00:00 2001 From: neoapps-dev Date: Wed, 15 Apr 2026 22:19:27 +0300 Subject: [PATCH] feat: game options editor --- public/images/tools/options.png | Bin 0 -> 10320 bytes src/components/views/DevtoolsView.tsx | 3 +- src/components/views/OptionsEditorView.tsx | 241 +++++++++++++++++++++ src/pages/App.tsx | 4 + src/services/OptionsService.ts | 167 ++++++++++++++ src/types/options.ts | 69 ++++++ 6 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 public/images/tools/options.png create mode 100644 src/components/views/OptionsEditorView.tsx create mode 100644 src/services/OptionsService.ts create mode 100644 src/types/options.ts diff --git a/public/images/tools/options.png b/public/images/tools/options.png new file mode 100644 index 0000000000000000000000000000000000000000..64a7ea2d02be1cd09a362088968e34826d3d29e8 GIT binary patch literal 10320 zcmeHMe@Giw9DiEf{88OH3&Id~q=oDclQUX36B4tit2$~rthN_hs>c)WtxzRR!Qh=HmvAnnd^o?Xp z%9mWWG8+J2pH5z(;?a4cr=&^*AoZI3i@70u$p8>v!&V%Y4wvmS(E_KZ7{LMce$Int z17NoLJrrFJCBgw+EN{`y4E~@cSjM6~lv|cn<|%}AY)P{h9%!~#(9QL4?GEP%@(caJubY{`iNs`Azl8udx`oY-3=E~Cm>h$S$;n~f%r)d?I}Hv^XnTeZ@9yz=-OAn!O+q*1ATNoeL2r;TGqg!! zB{x*j+uYK#p?94ZA4jNDJ}3KVG~6tnMwK9+-$zuLE9pZ z6B857pvuOP{0 z$&!`(rb-cd3k6QrvWg*U(ygdtMS|rh7bF@fk#IpCdMODq1SJu&dyB}wH|mN^Nh+fl zNI06plU%G+=X1!SBkO%$frs@1-?WD(D4q$=35k(hr+^3t*?9LGb)bx(NQB%g9Ji2y zi2r-61x?7KNZE)&;_2@5Rsb|DrA4+1f6a8jaD2kKb1(JJiOg%AmJjz79{l(oH~M+} zfbU}E_U#7^`_7Ga?)>(}^mpDLT>Ro#aMt{=x_8HGCtIpfNX{GY-UdKxob^_W5~3IW zO;cJxl&I|ogn9wA$EgDeuiyzX1DJALrN)PwDpm8Drp_-Y@Ggn^fpIPn!19vX> zi4S4}{kJ;K98qnC#f*878Q2=ZO{(sC*u5)g9=432pnPg+E*La6oEV?27`}I9MvNV^ zPhB4ANOlL_(AvGBv}02_C4Q19-i9BTYS7uh!)>mri~gu*?#a_Ut$ucKd8!9xC-*JZ z)=$&Y^HTT!{MGbwz7{bEuCz7W7bj4PwXm^5qKydW9m%b$t0pa2 z$hy+@_k|^LWhp?4?Jorv|G0^y-r(=8;eMYt#&|CJjs~yC_j6~{wBM_%;1^8HgUkRf zIH)?nJYXJV2BJ`3`YiBG=8X(vaB{lAeq?NJ{;n|wTpetAZy{T&_hg*!JrtO4vxDE* zqjynO&IKl}m+G-CqGStPR#l`G&8PpP{>+<(;*qA6<$0uMp|&=NO4u`xFIiV{ItkQ#$U9y zc1`#;mx`3V(n+IccE^Asdl1b$2!F{}IL0s5s??qX507d)ercKd>(|m#@{*-*+l%@N Hj>!vI1u literal 0 HcmV?d00001 diff --git a/src/components/views/DevtoolsView.tsx b/src/components/views/DevtoolsView.tsx index 622149b..00b4fe7 100644 --- a/src/components/views/DevtoolsView.tsx +++ b/src/components/views/DevtoolsView.tsx @@ -14,7 +14,8 @@ const DEV_TOOLS: DevTool[] = [ { id: "arc", name: "ARC Editor", view: "arc-editor", comingSoon: false }, { id: "loc", name: "LOC Editor", view: "loc-editor", comingSoon: false }, { id: "grf", name: "GRF Editor", view: "grf-editor", comingSoon: false }, - { id: "col", name: "COL Editor", view: "col-editor", comingSoon: false } + { id: "col", name: "COL Editor", view: "col-editor", comingSoon: false }, + { id: "options", name: "Options Editor", view: "options-editor", comingSoon: false } ]; export default function DevtoolsView() { diff --git a/src/components/views/OptionsEditorView.tsx b/src/components/views/OptionsEditorView.tsx new file mode 100644 index 0000000..830f02c --- /dev/null +++ b/src/components/views/OptionsEditorView.tsx @@ -0,0 +1,241 @@ +import { useState, useRef } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useUI, useAudio, useConfig } from "../../context/LauncherContext"; +import { OptionsService } from "../../services/OptionsService"; +import { OptionsFile } from "../../types/options"; +const BUTTONS: Record = { + 0x00: "NONE", 0x01: "A", 0x02: "B", 0x03: "X", 0x04: "Y", + 0x05: "D-Pad Left", 0x06: "D-Pad Right", 0x07: "D-Pad Up", 0x08: "D-Pad Down", + 0x09: "RB", 0x0A: "LB", 0x0B: "RT", 0x0C: "LT", + 0x0D: "RS", 0x0E: "LS" +}; + +export default function OptionsEditorView() { + const { setActiveView } = useUI(); + const { playPressSound, playBackSound } = useAudio(); + const { animationsEnabled } = useConfig(); + const [opt, setOpt] = useState(null); + const [notification, setNotification] = useState<{ message: string, type: "success" | "error" } | null>(null); + const fileInputRef = useRef(null); + const [activeTab, setActiveTab] = useState<"settings" | "skins" | "actions">("settings"); + const showNotification = (message: string, type: "success" | "error" = "success") => { + setNotification({ message, type }); + setTimeout(() => setNotification(null), 3000); + }; + + const handleFileLoad = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + playPressSound(); + const buffer = await file.arrayBuffer(); + try { + const parsed = OptionsService.readOptions(buffer); + setOpt(parsed); + showNotification(`Loaded options.dat`); + } catch (err: any) { + console.error("Failed to parse Options", err); + showNotification(err.message || "Failed to parse Options", "error"); + } + e.target.value = ""; + }; + + const handleSaveOptions = () => { + if (!opt) return; + playPressSound(); + try { + const buffer = OptionsService.serializeOptions(opt); + const blob = new Blob([buffer]); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "options.dat"; + a.click(); + URL.revokeObjectURL(url); + showNotification("Options Saved"); + } catch (err: any) { + showNotification("Failed to save", "error"); + } + }; + + const updateSetting = (field: keyof OptionsFile, value: any) => { + if (!opt) return; + setOpt({ ...opt, [field]: value }); + }; + + const updateAction = (field: keyof OptionsFile["actions"], value: number) => { + if (!opt) return; + setOpt({ ...opt, actions: { ...opt.actions, [field]: value } }); + }; + + const updateFavSkin = (idx: number, value: number) => { + if (!opt) return; + const newFav = [...opt.favoriteSkins]; + newFav[idx] = value; + setOpt({ ...opt, favoriteSkins: newFav }); + }; + + return ( + + +
+
+

Options Editor

+
+
+ + +
+
+ + {!opt ? ( +
+

Open settings.dat to begin

+
+ ) : ( +
+
+ {(["settings", "skins", "actions"] as const).map(tab => ( + + ))} +
+ +
+ {activeTab === "settings" && ( +
+ {Object.keys(opt).filter(k => k !== "actions" && k !== "rawData" && k !== "chosenSkin" && k !== "playerCape" && k !== "favoriteSkins" && k !== "endianness").map((k) => { + const val = opt[k as keyof OptionsFile]; + if (typeof val === "boolean") { + return ( +
+
+ )} + +
+ +
+ + + {notification && ( + + + {notification.message} + + + )} + + + ); +} diff --git a/src/pages/App.tsx b/src/pages/App.tsx index cebf4fe..26cc858 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -12,6 +12,7 @@ import { ArcEditorView } from "../components/views/ArcEditorView"; import LocEditorView from "../components/views/LocEditorView"; import GrfEditorView from "../components/views/GrfEditorView"; import ColEditorView from "../components/views/ColEditorView"; +import OptionsEditorView from "../components/views/OptionsEditorView"; import ScreenshotsView from "../components/views/ScreenshotsView"; import SkinViewer from "../components/common/SkinViewer"; import TeamModal from "../components/modals/TeamModal"; @@ -360,6 +361,9 @@ export default function App() { {activeView === "col-editor" && ( )} + {activeView === "options-editor" && ( + + )} {activeView === "skins" && } {activeView === "screenshots" && ( diff --git a/src/services/OptionsService.ts b/src/services/OptionsService.ts new file mode 100644 index 0000000..84c3b9b --- /dev/null +++ b/src/services/OptionsService.ts @@ -0,0 +1,167 @@ +import { OptionsFile } from "../types/options"; + +export class OptionsService { + public static readOptions(buffer: ArrayBuffer, endianness: "little" | "big" = "little"): OptionsFile { + const rawData = new Uint8Array(buffer).slice(); + const view = new DataView(buffer); + const little = endianness === "little"; + + const opt: Partial = { + endianness, + rawData + }; + + opt.musicVolume = view.getUint8(0x01); + opt.soundVolume = view.getUint8(0x02); + opt.gameSensitivity = view.getUint8(0x03); + opt.gamma = view.getUint8(0x04); + opt.interfaceSensitivity = view.getUint8(0x50); + opt.interfaceOpacity = view.getUint8(0x51); + const val06 = view.getUint16(0x06, little); + opt.difficulty = val06 & 0x3; + opt.viewBobbing = (val06 & (1 << 2)) !== 0; + opt.inGameGamertags = (val06 & (1 << 3)) !== 0; + opt.invertLook = (val06 & (1 << 6)) !== 0; + opt.southpaw = (val06 & (1 << 7)) !== 0; + opt.verticalSplitscreen = (val06 & (1 << 8)) !== 0; + opt.splitscreenGamertags = (val06 & (1 << 9)) !== 0; + opt.hints = (val06 & (1 << 10)) !== 0; + opt.autosaveTimer = (val06 >> 11) & 0xF; + opt.inGameTooltips = (val06 & (1 << 15)) !== 0; + const val54 = view.getUint32(0x54, little); + opt.renderClouds = (val54 & (1 << 0)) !== 0; + opt.displayHud = (val54 & (1 << 7)) !== 0; + opt.displayHand = (val54 & (1 << 8)) !== 0; + opt.customSkinAnimation = (val54 & (1 << 9)) !== 0; + opt.deathMessages = (val54 & (1 << 10)) !== 0; + opt.hudSize = (val54 >> 11) & 0x3; + opt.hudSizeSplitscreen = (val54 >> 13) & 0x3; + opt.animatedCharacter = (val54 & (1 << 15)) !== 0; + opt.classicCrafting = (val54 & (1 << 18)) !== 0; + opt.caveSounds = (val54 & (1 << 19)) !== 0; + opt.gameChat = (val54 & (1 << 20)) !== 0; + opt.minecartSounds = (val54 & (1 << 21)) !== 0; + opt.showGlideGhost = (val54 & (1 << 22)) !== 0; + opt.autoJump = (val54 & (1 << 26)) !== 0; + opt.displayGameMessages = (val54 & (1 << 28)) !== 0; + opt.displaySaveIcon = (val54 & (1 << 29)) !== 0; + opt.flyingViewRolling = (val54 & (1 << 30)) === 0; + opt.showGlideGhostPath = (val54 & (1 << 31)) !== 0; + opt.chosenSkin = view.getUint32(0x4C, little); + opt.playerCape = view.getUint32(0x5C, little); + opt.favoriteSkins = []; + for (let i = 0; i < 10; i++) { + opt.favoriteSkins.push(view.getUint32(0x60 + i * 4, little)); + } + + opt.actions = { + jump: view.getUint8(0xA4), + use: view.getUint8(0xA5), + action: view.getUint8(0xA6), + cycleHeldItemLeft: view.getUint8(0xA7), + cycleHeldItemRight: view.getUint8(0xA8), + inventory: view.getUint8(0xA9), + drop: view.getUint8(0xAA), + sneakDismount: view.getUint8(0xAB), + crafting: view.getUint8(0xAC), + changeCameraMode: view.getUint8(0xAD), + flyLeft: view.getUint8(0xAE), + flyRight: view.getUint8(0xAF), + flyUp: view.getUint8(0xB0), + flyDown: view.getUint8(0xB1), + sprint: view.getUint8(0xB2), + pickBlock: view.getUint8(0xB3), + previousPlayer: view.getUint8(0xB4), + nextPlayer: view.getUint8(0xB5), + spectateNoise: view.getUint8(0xB6), + cancelSpectating: view.getUint8(0xB7), + confirmReady: view.getUint8(0xB8), + vote: view.getUint8(0xB9), + restartSection: view.getUint8(0xBA), + restartRace: view.getUint8(0xBB), + lookBehind: view.getUint8(0xBC) + }; + + return opt as OptionsFile; + } + + public static serializeOptions(opt: OptionsFile): ArrayBuffer { + const minSize = 0xBC + 1; + const buffer = new Uint8Array(Math.max(opt.rawData.length, minSize)); + buffer.set(opt.rawData); + const view = new DataView(buffer.buffer); + const little = opt.endianness === "little"; + view.setUint8(0x01, opt.musicVolume); + view.setUint8(0x02, opt.soundVolume); + view.setUint8(0x03, opt.gameSensitivity); + view.setUint8(0x04, opt.gamma); + view.setUint8(0x50, opt.interfaceSensitivity); + view.setUint8(0x51, opt.interfaceOpacity); + let val06 = view.getUint16(0x06, little); + val06 = (val06 & ~0x3) | (opt.difficulty & 0x3); + val06 = (val06 & ~(1 << 2)) | (opt.viewBobbing ? (1 << 2) : 0); + val06 = (val06 & ~(1 << 3)) | (opt.inGameGamertags ? (1 << 3) : 0); + val06 = (val06 & ~(1 << 6)) | (opt.invertLook ? (1 << 6) : 0); + val06 = (val06 & ~(1 << 7)) | (opt.southpaw ? (1 << 7) : 0); + val06 = (val06 & ~(1 << 8)) | (opt.verticalSplitscreen ? (1 << 8) : 0); + val06 = (val06 & ~(1 << 9)) | (opt.splitscreenGamertags ? (1 << 9) : 0); + val06 = (val06 & ~(1 << 10)) | (opt.hints ? (1 << 10) : 0); + val06 = (val06 & ~(0xF << 11)) | ((opt.autosaveTimer & 0xF) << 11); + val06 = (val06 & ~(1 << 15)) | (opt.inGameTooltips ? (1 << 15) : 0); + view.setUint16(0x06, val06, little); + let val54 = view.getUint32(0x54, little); + val54 = (val54 & ~(1 << 0)) | (opt.renderClouds ? (1 << 0) : 0); + val54 = (val54 & ~(1 << 7)) | (opt.displayHud ? (1 << 7) : 0); + val54 = (val54 & ~(1 << 8)) | (opt.displayHand ? (1 << 8) : 0); + val54 = (val54 & ~(1 << 9)) | (opt.customSkinAnimation ? (1 << 9) : 0); + val54 = (val54 & ~(1 << 10)) | (opt.deathMessages ? (1 << 10) : 0); + val54 = (val54 & ~(0x3 << 11)) | ((opt.hudSize & 0x3) << 11); + val54 = (val54 & ~(0x3 << 13)) | ((opt.hudSizeSplitscreen & 0x3) << 13); + val54 = (val54 & ~(1 << 15)) | (opt.animatedCharacter ? (1 << 15) : 0); + val54 = (val54 & ~(1 << 18)) | (opt.classicCrafting ? (1 << 18) : 0); + val54 = (val54 & ~(1 << 19)) | (opt.caveSounds ? (1 << 19) : 0); + val54 = (val54 & ~(1 << 20)) | (opt.gameChat ? (1 << 20) : 0); + val54 = (val54 & ~(1 << 21)) | (opt.minecartSounds ? (1 << 21) : 0); + val54 = (val54 & ~(1 << 22)) | (opt.showGlideGhost ? (1 << 22) : 0); + val54 = (val54 & ~(1 << 26)) | (opt.autoJump ? (1 << 26) : 0); + val54 = (val54 & ~(1 << 28)) | (opt.displayGameMessages ? (1 << 28) : 0); + val54 = (val54 & ~(1 << 29)) | (opt.displaySaveIcon ? (1 << 29) : 0); + val54 = (val54 & ~(1 << 30)) | (!opt.flyingViewRolling ? (1 << 30) : 0); + val54 = (val54 & ~(1 << 31)) | (opt.showGlideGhostPath ? (1 << 31) : 0); + view.setUint32(0x54, val54, little); + view.setUint32(0x4C, opt.chosenSkin, little); + view.setUint32(0x5C, opt.playerCape, little); + for (let i = 0; i < 10; i++) { + if (opt.favoriteSkins[i] !== undefined) { + view.setUint32(0x60 + i * 4, opt.favoriteSkins[i], little); + } + } + + view.setUint8(0xA4, opt.actions.jump); + view.setUint8(0xA5, opt.actions.use); + view.setUint8(0xA6, opt.actions.action); + view.setUint8(0xA7, opt.actions.cycleHeldItemLeft); + view.setUint8(0xA8, opt.actions.cycleHeldItemRight); + view.setUint8(0xA9, opt.actions.inventory); + view.setUint8(0xAA, opt.actions.drop); + view.setUint8(0xAB, opt.actions.sneakDismount); + view.setUint8(0xAC, opt.actions.crafting); + view.setUint8(0xAD, opt.actions.changeCameraMode); + view.setUint8(0xAE, opt.actions.flyLeft); + view.setUint8(0xAF, opt.actions.flyRight); + view.setUint8(0xB0, opt.actions.flyUp); + view.setUint8(0xB1, opt.actions.flyDown); + view.setUint8(0xB2, opt.actions.sprint); + view.setUint8(0xB3, opt.actions.pickBlock); + view.setUint8(0xB4, opt.actions.previousPlayer); + view.setUint8(0xB5, opt.actions.nextPlayer); + view.setUint8(0xB6, opt.actions.spectateNoise); + view.setUint8(0xB7, opt.actions.cancelSpectating); + view.setUint8(0xB8, opt.actions.confirmReady); + view.setUint8(0xB9, opt.actions.vote); + view.setUint8(0xBA, opt.actions.restartSection); + view.setUint8(0xBB, opt.actions.restartRace); + view.setUint8(0xBC, opt.actions.lookBehind); + return buffer.buffer; + } +} diff --git a/src/types/options.ts b/src/types/options.ts new file mode 100644 index 0000000..a93b0c8 --- /dev/null +++ b/src/types/options.ts @@ -0,0 +1,69 @@ +export interface OptionsFile { + endianness: "little" | "big"; + musicVolume: number; + soundVolume: number; + gamma: number; + gameSensitivity: number; + interfaceSensitivity: number; + interfaceOpacity: number; + difficulty: number; + viewBobbing: boolean; + invertLook: boolean; + southpaw: boolean; + verticalSplitscreen: boolean; + inGameGamertags: boolean; + autosaveTimer: number; + splitscreenGamertags: boolean; + hints: boolean; + inGameTooltips: boolean; + renderClouds: boolean; + displayHud: boolean; + displayHand: boolean; + customSkinAnimation: boolean; + deathMessages: boolean; + hudSize: number; + hudSizeSplitscreen: number; + animatedCharacter: boolean; + classicCrafting: boolean; + caveSounds: boolean; + gameChat: boolean; + minecartSounds: boolean; + displayGameMessages: boolean; + displaySaveIcon: boolean; + flyingViewRolling: boolean; + showGlideGhost: boolean; + showGlideGhostPath: boolean; + autoJump: boolean; + chosenSkin: number; + playerCape: number; + favoriteSkins: number[]; + actions: { + jump: number; + use: number; + action: number; + cycleHeldItemLeft: number; + cycleHeldItemRight: number; + inventory: number; + drop: number; + sneakDismount: number; + crafting: number; + changeCameraMode: number; + flyLeft: number; + flyRight: number; + flyUp: number; + flyDown: number; + sprint: number; + pickBlock: number; + previousPlayer: number; + nextPlayer: number; + spectateNoise: number; + cancelSpectating: number; + confirmReady: number; + vote: number; + restartSection: number; + restartRace: number; + lookBehind: number; + }; + + rawData: Uint8Array; +}