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, GrfFileEntry } from "../../types/grf"; export default function GrfEditorView() { const { setActiveView } = useUI(); const { playPressSound, playBackSound } = useAudio(); const { animationsEnabled } = useConfig(); const [grf, setGrf] = useState(null); 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); }; const handleFileLoad = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; playPressSound(); const buffer = await file.arrayBuffer(); try { const parsedGrf = GrfService.readGRF(buffer); setGrf(parsedGrf); setFilename(file.name); showNotification(`Loaded ${file.name}`); } catch (err: unknown) { console.error("Failed to parse GRF", err); showNotification(err instanceof Error ? err.message : "Failed to parse GRF", "error"); } 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 updateNode = (node: GrfNode, path: string[]): GrfNode => { if (path.length === 0) { if (!node.parameters[paramIndex]) return node; const newParams = [...node.parameters]; newParams[paramIndex] = { ...newParams[paramIndex], value }; return { ...node, parameters: newParams }; } const [next, ...rest] = path; return { ...node, children: node.children.map(child => child.name === next ? updateNode(child, rest) : child) }; }; setGrf({ ...grf, root: updateNode(grf.root, nodePath) }); }; const handleSaveGrf = () => { if (!grf) return; playPressSound(); try { const buffer = GrfService.serializeGRF(grf); const blob = new Blob([buffer]); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); showNotification("GRF Saved Successfully"); } catch (err: unknown) { console.error("Failed to save GRF", err); showNotification(err instanceof Error ? err.message : "Failed to save GRF", "error"); } }; return (

GRF Editor

{grf && editing: {filename}}
{!grf ? (

Open a GRF file to begin editing

) : (
{activeTab === "rules" && (
{grf.root.children.map((node, i) => ( ))} {grf.root.children.length === 0 && No rules found}
)} {activeTab === "files" && (
{grf.files.length === 0 && ( )} {grf.files.map((f, i) => ( ))}
Filename Size Actions
No files in GRF
{f.filename} {(f.data.length / 1024).toFixed(2)} KB
)}
)}
{notification && ( {notification.message} )}
); } 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)} > {node.children.length > 0 ? ( ) : (
)} 0 ? "/images/Folder_Icon.png" : "/images/tools/grf.png"} className="w-4 h-4 object-contain grayscale opacity-60" style={{ imageRendering: "pixelated" }} onError={(e) => (e.currentTarget.src = "/images/tools/pck.png")} /> {node.name} {node.parameters.length > 0 && [{node.parameters.length} props]}
{expanded && (
{node.parameters.length > 0 && (
{node.parameters.map((p, i) => (
{p.name} onUpdate(currentPath, 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) => ( ))}
)}
); }