From 972a5958c66039303eb20f64b723ad9b349e5e92 Mon Sep 17 00:00:00 2001 From: neoapps-dev Date: Tue, 28 Apr 2026 12:28:32 +0300 Subject: [PATCH] feat(devtools/grf): support for editing and creating new GRF/GRH files --- src/components/views/GrfEditorView.tsx | 132 +++++++++++++++++++------ src/services/GrfService.ts | 17 ++++ 2 files changed, 121 insertions(+), 28 deletions(-) diff --git a/src/components/views/GrfEditorView.tsx b/src/components/views/GrfEditorView.tsx index 225d38f..0ad374f 100644 --- a/src/components/views/GrfEditorView.tsx +++ b/src/components/views/GrfEditorView.tsx @@ -2,8 +2,7 @@ import { useState, useRef } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { useUI, useAudio, useConfig } from "../../context/LauncherContext"; import { GrfService } from "../../services/GrfService"; -import { GrfFile, GrfNode } from "../../types/grf"; - +import { GrfFile, GrfNode, GrfFileEntry } from "../../types/grf"; export default function GrfEditorView() { const { setActiveView } = useUI(); const { playPressSound, playBackSound } = useAudio(); @@ -12,9 +11,8 @@ export default function GrfEditorView() { const [filename, setFilename] = useState("game_rules.grf"); const [notification, setNotification] = useState<{ message: string, type: "success" | "error" } | null>(null); const fileInputRef = useRef(null); - + const addFileInputRef = useRef(null); const [activeTab, setActiveTab] = useState<"rules" | "files">("rules"); - const showNotification = (message: string, type: "success" | "error" = "success") => { setNotification({ message, type }); setTimeout(() => setNotification(null), 3000); @@ -37,6 +35,51 @@ export default function GrfEditorView() { e.target.value = ""; }; + const handleNewGrf = () => { + playPressSound(); + setGrf(GrfService.createDefaultGRF()); + setFilename("new_rules.grf"); + showNotification("New GRF Created"); + }; + + const handleAddFile = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file || !grf) return; + playPressSound(); + const buffer = await file.arrayBuffer(); + const newFile: GrfFileEntry = { + filename: file.name, + data: new Uint8Array(buffer) + }; + setGrf({ + ...grf, + files: [...grf.files, newFile] + }); + showNotification(`Added ${file.name}`); + e.target.value = ""; + }; + + const handleDeleteFile = (index: number) => { + if (!grf) return; + playPressSound(); + const newFiles = [...grf.files]; + const removed = newFiles.splice(index, 1)[0]; + setGrf({ ...grf, files: newFiles }); + showNotification(`Removed ${removed.filename}`); + }; + + const handleUpdateParameter = (nodePath: string[], paramIndex: number, value: string) => { + if (!grf) return; + const newGrf = { ...grf }; + let current = newGrf.root; + for (const part of nodePath) { + const found = current.children.find(c => c.name === part); + if (found) current = found; + } + current.parameters[paramIndex].value = value; + setGrf(newGrf); + }; + const handleSaveGrf = () => { if (!grf) return; playPressSound(); @@ -65,12 +108,20 @@ export default function GrfEditorView() { className="flex flex-col w-full h-[85vh] max-w-7xl relative" > +

GRF Editor

{grf && editing: {filename}}
+ + + + ))} + + +
)}
@@ -174,11 +243,13 @@ export default function GrfEditorView() { ); } -function GrfNodeView({ node, level }: { node: GrfNode, level: number }) { +function GrfNodeView({ node, level, path, onUpdate }: { node: GrfNode, level: number, path: string[], onUpdate: (path: string[], paramIdx: number, val: string) => void }) { const [expanded, setExpanded] = useState(level < 1); + const currentPath = [...path, node.name]; + return (
-
setExpanded(!expanded)} @@ -206,15 +277,20 @@ function GrfNodeView({ node, level }: { node: GrfNode, level: number }) { {node.parameters.length > 0 && (
{node.parameters.map((p, i) => ( -
+
{p.name} - {p.value} + onUpdate(path, i, e.target.value)} + className="flex-1 bg-white/5 border border-white/10 px-2 py-1 text-white outline-none focus:border-[#FFFF55]/50 font-mono transition-colors" + />
))}
)} {node.children.map((child, i) => ( - + ))}
)} diff --git a/src/services/GrfService.ts b/src/services/GrfService.ts index 634d7af..5ec044d 100644 --- a/src/services/GrfService.ts +++ b/src/services/GrfService.ts @@ -302,4 +302,21 @@ export class GrfService { return finalBuffer.buffer; } + + public static createDefaultGRF(): GrfFile { + return { + header: { + compressionLevel: GrfCompressionLevel.CompressedRle, + crc: 0, + compressionType: GrfCompressionType.Zlib, + unknownData: new Uint8Array([0, 0, 0, 0]) + }, + files: [], + root: { + name: "__ROOT__", + parameters: [], + children: [] + } + }; + } }