diff --git a/fission/src/mirabuf/FieldMiraEditor.ts b/fission/src/mirabuf/FieldMiraEditor.ts index 4955fc42a2..ec659c27b9 100644 --- a/fission/src/mirabuf/FieldMiraEditor.ts +++ b/fission/src/mirabuf/FieldMiraEditor.ts @@ -3,11 +3,13 @@ import { mirabuf } from "@/proto/mirabuf" import { defaultFieldPreferences, type FieldPreferences, + type ProtectedZonePreferences, type ScoringZonePreferences, } from "@/systems/preferences/PreferenceTypes" export interface DevtoolMiraData { "devtool:scoring_zones": ScoringZonePreferences[] + "devtool:protected_zones": ProtectedZonePreferences[] "devtool:camera_locations": unknown "devtool:spawn_locations": FieldPreferences["spawnLocations"] "devtool:a": unknown diff --git a/fission/src/systems/preferences/PreferenceTypes.ts b/fission/src/systems/preferences/PreferenceTypes.ts index 72c0b44260..e0fbb7772e 100644 --- a/fission/src/systems/preferences/PreferenceTypes.ts +++ b/fission/src/systems/preferences/PreferenceTypes.ts @@ -147,26 +147,26 @@ export type Alliance = "red" | "blue" export type Station = 1 | 2 | 3 -export type ScoringZonePreferences = { +/** + * Base properties shared by all zone types + */ +export type BaseZonePreferences = { name: string alliance: Alliance parentNode: string | undefined + deltaTransformation: number[] +} + +export type ScoringZonePreferences = BaseZonePreferences & { points: number destroyGamepiece: boolean persistentPoints: boolean - - deltaTransformation: number[] } -export type ProtectedZonePreferences = { - name: string - alliance: Alliance +export type ProtectedZonePreferences = BaseZonePreferences & { penaltyPoints: number - parentNode: string | undefined contactType: ContactType activeDuring: MatchModeType[] - - deltaTransformation: number[] } export type SpawnLocation = Readonly<{ diff --git a/fission/src/ui/modals/DevtoolZoneModificationModal.tsx b/fission/src/ui/modals/DevtoolZoneModificationModal.tsx new file mode 100644 index 0000000000..a8d96debe0 --- /dev/null +++ b/fission/src/ui/modals/DevtoolZoneModificationModal.tsx @@ -0,0 +1,91 @@ +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, Typography } from "@mui/material" +import type React from "react" +import { useState } from "react" +import { globalAddToast } from "../components/GlobalUIControls" + +interface DevtoolZoneModificationModalProps { + isOpen: boolean + onClose: () => void + zoneType: "scoring" | "protected" + zoneName: string + onTemporaryModification: () => void + onPermanentModification: () => void +} + +const DevtoolZoneModificationModal: React.FC = ({ + isOpen, + onClose, + zoneType, + zoneName, + onTemporaryModification, + onPermanentModification, +}) => { + const [isModifying, setIsModifying] = useState(false) + + const handleTemporaryModification = () => { + onTemporaryModification() + onClose() + } + + const handlePermanentModification = async () => { + setIsModifying(true) + try { + await onPermanentModification() + globalAddToast?.("info", "Zone Modified", `${zoneName} has been permanently modified in the field file.`) + } catch (error) { + globalAddToast?.( + "error", + "Modification Failed", + "Failed to permanently modify zone in the field file cache." + ) + console.error("Failed to modify zone in field file:", error) + } finally { + setIsModifying(false) + onClose() + } + } + + return ( + + Modify {zoneType === "scoring" ? "Scoring" : "Protected"} Zone + + + + The {zoneType} zone "{zoneName}" was defined in the field file and is cached. + + + Choose how you'd like to save your modifications: + + + + Temporary modification: Save changes until next field reload. Original zone + will reappear when you refresh the page. + + + Permanent modification: Save changes to the local asset file. This will + persist your modifications until you remove it from the cache. + + + + + + + + + + + ) +} + +export default DevtoolZoneModificationModal diff --git a/fission/src/ui/panels/DeveloperToolPanel.tsx b/fission/src/ui/panels/DeveloperToolPanel.tsx index fb8a34cbfa..df032d3905 100644 --- a/fission/src/ui/panels/DeveloperToolPanel.tsx +++ b/fission/src/ui/panels/DeveloperToolPanel.tsx @@ -34,7 +34,11 @@ const DeveloperToolPanel: React.FC> = ({ panel }) => if (parts) { const newEditor = new FieldMiraEditor(parts) setEditor(newEditor) - setKeys(newEditor.getAllDevtoolKeys()) + setKeys( + newEditor + .getAllDevtoolKeys() + .filter(k => Object.prototype.hasOwnProperty.call(devtoolHandlers, k)) + ) setFieldLoaded(true) } else { setEditor(undefined) @@ -50,7 +54,9 @@ const DeveloperToolPanel: React.FC> = ({ panel }) => setJsonValue("") setError("") } else if (currentField && editor) { - setKeys(editor.getAllDevtoolKeys()) + setKeys( + editor.getAllDevtoolKeys().filter(k => Object.prototype.hasOwnProperty.call(devtoolHandlers, k)) + ) } } const allKeys = editor?.getAllDevtoolKeys() @@ -64,7 +70,8 @@ const DeveloperToolPanel: React.FC> = ({ panel }) => // Load value when key changes useEffect(() => { const field = World.sceneRenderer.mirabufSceneObjects.getField() - if (!editor || !field || !selectedKey) return + if (!editor || !field || !selectedKey || !Object.prototype.hasOwnProperty.call(devtoolHandlers, selectedKey)) + return const val = devtoolHandlers[selectedKey].get(field) editor.setUserData(selectedKey, val) @@ -83,7 +90,7 @@ const DeveloperToolPanel: React.FC> = ({ panel }) => } editor.setUserData(selectedKey, parsed) - setKeys(editor.getAllDevtoolKeys()) + setKeys(editor.getAllDevtoolKeys().filter(k => Object.prototype.hasOwnProperty.call(devtoolHandlers, k))) // Persist changes to cache const field = World.sceneRenderer.mirabufSceneObjects.getField() @@ -127,7 +134,7 @@ const DeveloperToolPanel: React.FC> = ({ panel }) => const handleRemove = () => { if (!editor || !selectedKey) return editor.removeUserData(selectedKey) - setKeys(editor.getAllDevtoolKeys()) + setKeys(editor.getAllDevtoolKeys().filter(k => Object.prototype.hasOwnProperty.call(devtoolHandlers, k))) setSelectedKey(undefined) setJsonValue("") setError("") diff --git a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx index 7b378e1c62..ca26817500 100644 --- a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx @@ -14,7 +14,7 @@ import World from "@/systems/World" import Label from "@/ui/components/Label" import type { PanelImplProps } from "@/ui/components/Panel" import TransformGizmoControl from "@/ui/components/TransformGizmoControl" -import { CloseType, type UIScreen, useUIContext } from "@/ui/helpers/UIProviderHelpers" +import { CloseType, type Panel, type UIScreen, useUIContext } from "@/ui/helpers/UIProviderHelpers" import ChooseInputSchemePanel from "../ChooseInputSchemePanel" import { CONFIG_OPTS, ConfigMode, type ConfigurationType } from "./ConfigTypes" import AssemblySelection, { type AssemblySelectionOption } from "./configure/AssemblySelection" @@ -79,7 +79,13 @@ const ConfigInterface: React.FCERROR: Field does not contain scoring zone configuration! } - return + return ( + } + selectedField={assembly} + initialZones={zones} + /> + ) } case ConfigMode.PROTECTED_ZONES: { const zones = assembly.fieldPreferences?.protectedZones ?? [] diff --git a/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ConfigureProtectedZonesInterface.tsx b/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ConfigureProtectedZonesInterface.tsx index 550cbb4c7b..6ff216e348 100644 --- a/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ConfigureProtectedZonesInterface.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ConfigureProtectedZonesInterface.tsx @@ -5,6 +5,7 @@ import type MirabufSceneObject from "@/mirabuf/MirabufSceneObject" import PreferencesSystem from "@/systems/preferences/PreferencesSystem" import type { ProtectedZonePreferences } from "@/systems/preferences/PreferenceTypes" import Label from "@/ui/components/Label" +import type { Panel } from "@/ui/helpers/UIProviderHelpers" import ManageProtectedZonesInterface from "./ManageProtectedZonesInterface" import ZoneConfigInterface from "./ProtectedZoneConfigInterface" @@ -21,9 +22,10 @@ const protectedZones = (zones: ProtectedZonePreferences[] | undefined, field: Mi interface ConfigureZonesProps { selectedField: MirabufSceneObject initialZones: ProtectedZonePreferences[] + panel?: Panel } -const ConfigureProtectedZonesInterface: React.FC = ({ selectedField, initialZones }) => { +const ConfigureProtectedZonesInterface: React.FC = ({ selectedField, initialZones, panel }) => { const [selectedZone, setSelectedZone] = useState(undefined) return ( @@ -33,6 +35,7 @@ const ConfigureProtectedZonesInterface: React.FC = ({ selec selectedField={selectedField} initialZones={initialZones} selectZone={setSelectedZone} + panel={panel} /> ) : ( <> diff --git a/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ConfigureScoringZonesInterface.tsx b/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ConfigureScoringZonesInterface.tsx index 9b5ea175d2..b9b508748c 100644 --- a/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ConfigureScoringZonesInterface.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ConfigureScoringZonesInterface.tsx @@ -21,12 +21,15 @@ const saveZones = (zones: ScoringZonePreferences[] | undefined, field: MirabufSc field.updateScoringZones() } +import type { Panel } from "@/ui/helpers/UIProviderHelpers" + interface ConfigureZonesProps { selectedField: MirabufSceneObject initialZones: ScoringZonePreferences[] + panel?: Panel } -const ConfigureScoringZonesInterface: React.FC = ({ selectedField, initialZones }) => { +const ConfigureScoringZonesInterface: React.FC = ({ selectedField, initialZones, panel }) => { const [selectedZone, setSelectedZone] = useState(undefined) return ( @@ -36,6 +39,7 @@ const ConfigureScoringZonesInterface: React.FC = ({ selecte selectedField={selectedField} initialZones={initialZones} selectZone={setSelectedZone} + panel={panel} /> ) : ( <> @@ -66,6 +70,7 @@ const ConfigureScoringZonesInterface: React.FC = ({ selecte saveAllZones={() => { saveZones(selectedField.fieldPreferences?.scoringZones, selectedField) }} + panel={panel} /> )} diff --git a/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ManageProtectedZonesInterface.tsx b/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ManageProtectedZonesInterface.tsx index 47151ecaf8..78d9fc2c40 100644 --- a/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ManageProtectedZonesInterface.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ManageProtectedZonesInterface.tsx @@ -11,6 +11,8 @@ import World from "@/systems/World" import Label from "@/ui/components/Label" import ScrollView from "@/ui/components/ScrollView" import { AddButton, DeleteButton, EditButton } from "@/ui/components/StyledComponents" +import type { Panel } from "@/ui/helpers/UIProviderHelpers" +import { useUIContext } from "@/ui/helpers/UIProviderHelpers" const saveZones = (zones: ProtectedZonePreferences[] | undefined, field: MirabufSceneObject | undefined) => { if (!zones || !field) return @@ -59,11 +61,21 @@ interface ProtectedZonesProps { selectedField: MirabufSceneObject initialZones: ProtectedZonePreferences[] selectZone: (zone: ProtectedZonePreferences) => void + panel?: Panel } -const ManageZonesInterface: React.FC = ({ selectedField, initialZones, selectZone }) => { +const ManageZonesInterface: React.FC = ({ selectedField, initialZones, selectZone, panel }) => { const [zones, setZones] = useState(initialZones) + const { configureScreen } = useUIContext() + + // Show the panel's default footer buttons when this interface is active + useEffect(() => { + if (panel) { + configureScreen(panel, { hideAccept: false, hideCancel: false }, {}) + } + }, [panel, configureScreen]) + const saveEvent = useCallback(() => { saveZones(zones, selectedField) }, [zones, selectedField]) diff --git a/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ManageScoringZonesInterface.tsx b/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ManageScoringZonesInterface.tsx index e6c22c6933..6d9085b81e 100644 --- a/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ManageScoringZonesInterface.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ManageScoringZonesInterface.tsx @@ -9,6 +9,10 @@ import World from "@/systems/World" import Label from "@/ui/components/Label" import ScrollView from "@/ui/components/ScrollView" import { AddButton, DeleteButton, EditButton } from "@/ui/components/StyledComponents" +import DevtoolZoneModificationModal from "@/ui/modals/DevtoolZoneModificationModal" +import { isZoneFromDevtools, removeZoneFromDevtools } from "@/util/DevtoolZoneUtils" +import type { Panel } from "@/ui/helpers/UIProviderHelpers" +import { useUIContext } from "@/ui/helpers/UIProviderHelpers" const saveZones = (zones: ScoringZonePreferences[] | undefined, field: MirabufSceneObject | undefined) => { if (!zones || !field) return @@ -25,9 +29,18 @@ type ScoringZoneRowProps = { save: () => void deleteZone: () => void selectZone: (zone: ScoringZonePreferences) => void + onShowConfirmation: (zone: ScoringZonePreferences) => void } -const ScoringZoneRow: React.FC = ({ zone, save, deleteZone, selectZone }) => { +const ScoringZoneRow: React.FC = ({ zone, save, deleteZone, selectZone, onShowConfirmation }) => { + const handleDeleteClick = () => { + if (isZoneFromDevtools(zone, "scoring")) { + onShowConfirmation(zone) + } else { + deleteZone() + } + } + return ( @@ -51,7 +64,7 @@ const ScoringZoneRow: React.FC = ({ zone, save, deleteZone, })} {DeleteButton(() => { - deleteZone() + handleDeleteClick() })} @@ -62,10 +75,25 @@ interface ScoringZonesProps { selectedField: MirabufSceneObject initialZones: ScoringZonePreferences[] selectZone: (zone: ScoringZonePreferences) => void + panel?: Panel } -const ManageZonesInterface: React.FC = ({ selectedField, initialZones, selectZone }) => { +const ManageZonesInterface: React.FC = ({ selectedField, initialZones, selectZone, panel }) => { const [zones, setZones] = useState(initialZones) + const [confirmationModal, setConfirmationModal] = useState<{ + isOpen: boolean + zone: ScoringZonePreferences | null + zoneIndex: number + }>({ isOpen: false, zone: null, zoneIndex: -1 }) + + const { configureScreen } = useUIContext() + + // Show the panel's default footer buttons when this interface is active + useEffect(() => { + if (panel) { + configureScreen(panel, { hideAccept: false, hideCancel: false }, {}) + } + }, [panel, configureScreen]) const saveEvent = useCallback(() => { saveZones(zones, selectedField) @@ -89,6 +117,31 @@ const ManageZonesInterface: React.FC = ({ selectedField, init } }, [selectedField, zones]) + const handleShowConfirmation = (zone: ScoringZonePreferences) => { + const zoneIndex = zones.indexOf(zone) + setConfirmationModal({ isOpen: true, zone, zoneIndex }) + } + + const handleTemporaryRemoval = () => { + if (confirmationModal.zoneIndex >= 0) { + const newZones = zones.filter((_, idx) => idx !== confirmationModal.zoneIndex) + setZones(newZones) + saveZones(newZones, selectedField) + } + } + + const handlePermanentRemoval = async () => { + if (confirmationModal.zone) { + await removeZoneFromDevtools(confirmationModal.zone, "scoring") + const updatedZones = selectedField.fieldPreferences?.scoringZones ?? [] + setZones(updatedZones) + } + } + + const handleCloseConfirmation = () => { + setConfirmationModal({ isOpen: false, zone: null, zoneIndex: -1 }) + } + return ( <> {zones?.length > 0 ? ( @@ -109,6 +162,7 @@ const ManageZonesInterface: React.FC = ({ selectedField, init ) }} selectZone={selectZone} + onShowConfirmation={handleShowConfirmation} /> ))} @@ -133,6 +187,15 @@ const ManageZonesInterface: React.FC = ({ selectedField, init selectZone(newZone) })} + + ) } diff --git a/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ScoringZoneConfigInterface.tsx b/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ScoringZoneConfigInterface.tsx index 065dc3a5b5..eb6b21d3b8 100644 --- a/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ScoringZoneConfigInterface.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/interfaces/scoring/ScoringZoneConfigInterface.tsx @@ -1,9 +1,7 @@ import type Jolt from "@azaleacolburn/jolt-physics" -import { TextField } from "@mui/material" -import { Button } from "@/ui/components/StyledComponents" +import { Button, Stack, TextField } from "@mui/material" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import * as THREE from "three" -import { ConfigurationSavedEvent } from "@/events/ConfigurationSavedEvent" import type { RigidNodeId } from "@/mirabuf/MirabufParser" import type MirabufSceneObject from "@/mirabuf/MirabufSceneObject" import type { RigidNodeAssociate } from "@/mirabuf/MirabufSceneObject" @@ -12,9 +10,14 @@ import PreferencesSystem from "@/systems/preferences/PreferencesSystem" import type { Alliance, ScoringZonePreferences } from "@/systems/preferences/PreferenceTypes" import type GizmoSceneObject from "@/systems/scene/GizmoSceneObject" import World from "@/systems/World" + import Checkbox from "@/ui/components/Checkbox" import SelectButton from "@/ui/components/SelectButton" import TransformGizmoControl from "@/ui/components/TransformGizmoControl" +import type { Panel } from "@/ui/helpers/UIProviderHelpers" +import { CloseType, useUIContext } from "@/ui/helpers/UIProviderHelpers" +import DevtoolZoneModificationModal from "@/ui/modals/DevtoolZoneModificationModal" +import { addUserZoneToDevtools, isZoneFromDevtools, modifyZoneInDevtools, zonesEqual } from "@/util/DevtoolZoneUtils" import { convertArrayToThreeMatrix4, convertJoltMat44ToThreeMatrix4, @@ -102,9 +105,10 @@ interface ZoneConfigProps { selectedField: MirabufSceneObject selectedZone: ScoringZonePreferences saveAllZones: () => void + panel?: Panel } -const ZoneConfigInterface: React.FC = ({ selectedField, selectedZone, saveAllZones }) => { +const ZoneConfigInterface: React.FC = ({ selectedField, selectedZone, saveAllZones, panel }) => { //Official FIRST hex // TODO: Do we want to eventually make these editable? const redMaterial = useMemo(() => { @@ -131,10 +135,23 @@ const ZoneConfigInterface: React.FC = ({ selectedField, selecte const [points, setPoints] = useState(selectedZone.points) const [destroy] = useState(selectedZone.destroyGamepiece) const [persistent, setPersistent] = useState(selectedZone.persistentPoints) + const [confirmationModal, setConfirmationModal] = useState<{ + isOpen: boolean + pendingSave: boolean + }>({ isOpen: false, pendingSave: false }) const gizmoRef = useRef(undefined) + const originalZoneRef = useRef(structuredClone(selectedZone)) + const { configureScreen, closePanel } = useUIContext() + + // Hide the panel's default footer buttons when this interface is active + useEffect(() => { + if (panel) { + configureScreen(panel, { hideAccept: true, hideCancel: true }, {}) + } + }, [panel, configureScreen]) - const saveEvent = useCallback(() => { + const handleSave = useCallback(async () => { if (gizmoRef.current && selectedField) { save( selectedField, @@ -147,17 +164,27 @@ const ZoneConfigInterface: React.FC = ({ selectedField, selecte gizmoRef.current, selectedNode ) - saveAllZones() - } - }, [selectedField, selectedZone, name, alliance, points, destroy, persistent, selectedNode, saveAllZones]) - useEffect(() => { - ConfigurationSavedEvent.listen(saveEvent) + // Auto-cache user-created zones for persistence + const isExistingZone = selectedField.fieldPreferences?.scoringZones.some( + z => z === originalZoneRef.current || zonesEqual(z, originalZoneRef.current) + ) - return () => { - ConfigurationSavedEvent.removeListener(saveEvent) + if (!isZoneFromDevtools(originalZoneRef.current, "scoring")) { + try { + await addUserZoneToDevtools( + selectedZone, + isExistingZone ? originalZoneRef.current : undefined, + "scoring" + ) + } catch (error) { + console.warn("Failed to auto-cache user zone:", error) + } + } + + saveAllZones() } - }, [saveEvent]) + }, [selectedField, selectedZone, name, alliance, points, destroy, persistent, selectedNode, saveAllZones]) /** Holds a pause for the duration of the interface component */ useEffect(() => { @@ -246,6 +273,81 @@ const ZoneConfigInterface: React.FC = ({ selectedField, selecte [selectedField] ) + const handleTemporaryModification = () => { + if (gizmoRef.current && selectedField) { + save( + selectedField, + selectedZone, + name, + alliance, + points, + destroy, + persistent, + gizmoRef.current, + selectedNode + ) + + const fieldZones = selectedField.fieldPreferences?.scoringZones + if (fieldZones) { + const zoneIndex = fieldZones.findIndex( + z => + z === selectedZone || + (z.name === originalZoneRef.current.name && + z.alliance === originalZoneRef.current.alliance && + z.parentNode === originalZoneRef.current.parentNode && + JSON.stringify(z.deltaTransformation) === + JSON.stringify(originalZoneRef.current.deltaTransformation)) + ) + + if (zoneIndex >= 0) { + fieldZones[zoneIndex] = { + name, + alliance, + parentNode: selectedNode, + points, + destroyGamepiece: destroy, + persistentPoints: persistent, + deltaTransformation: selectedZone.deltaTransformation, + } + } + } + + PreferencesSystem.savePreferences() + selectedField.updateScoringZones() + } + + setConfirmationModal({ isOpen: false, pendingSave: false }) + if (panel) closePanel(panel.id, CloseType.Accept) + } + + const handlePermanentModification = async () => { + try { + const modifiedZone: ScoringZonePreferences = { + name, + alliance, + parentNode: selectedNode, + points, + destroyGamepiece: destroy, + persistentPoints: persistent, + deltaTransformation: selectedZone.deltaTransformation, + } + + handleSave() + modifiedZone.deltaTransformation = selectedZone.deltaTransformation + + await modifyZoneInDevtools(originalZoneRef.current, modifiedZone, "scoring") + } catch (error) { + console.error("Failed to permanently modify zone:", error) + } finally { + setConfirmationModal({ isOpen: false, pendingSave: false }) + if (panel) closePanel(panel.id, CloseType.Accept) + } + } + + const handleCloseConfirmation = () => { + setConfirmationModal({ isOpen: false, pendingSave: false }) + } + return (
{/** Set the zone name */} @@ -295,6 +397,42 @@ const ZoneConfigInterface: React.FC = ({ selectedField, selecte {/** Switch between transform control modes */} {gizmoComponent} + + + + {/** Custom Save/Cancel buttons that replace the panel's default buttons */} + + + +
) } diff --git a/fission/src/util/DevtoolZoneUtils.ts b/fission/src/util/DevtoolZoneUtils.ts new file mode 100644 index 0000000000..4753f63d1e --- /dev/null +++ b/fission/src/util/DevtoolZoneUtils.ts @@ -0,0 +1,240 @@ +import FieldMiraEditor from "@/mirabuf/FieldMiraEditor" +import MirabufCachingService, { MiraType } from "@/mirabuf/MirabufLoader" +import PreferencesSystem from "@/systems/preferences/PreferencesSystem" +import type { + BaseZonePreferences, + ProtectedZonePreferences, + ScoringZonePreferences, +} from "@/systems/preferences/PreferenceTypes" +import World from "@/systems/World" + +export type ZoneType = "scoring" | "protected" + +/** + * Checks if two zones are equal by comparing their common base properties + */ +export function zonesEqual(zone1: BaseZonePreferences, zone2: BaseZonePreferences): boolean { + return ( + zone1.name === zone2.name && + zone1.alliance === zone2.alliance && + zone1.parentNode === zone2.parentNode && + JSON.stringify(zone1.deltaTransformation) === JSON.stringify(zone2.deltaTransformation) + ) +} + +/** + * Checks if a zone was originally defined in the field file by comparing it with the cached field data. + */ +export function isZoneFromDevtools(zone: BaseZonePreferences, zoneType: ZoneType): boolean { + const field = World.sceneRenderer.mirabufSceneObjects.getField() + if (!field) return false + + const parts = field.mirabufInstance.parser.assembly.data?.parts + if (!parts) return false + + const editor = new FieldMiraEditor(parts) + + if (zoneType === "protected") { + return false + } + + const devtoolZones = editor.getUserData("devtool:scoring_zones") + if (!devtoolZones) return false + + return devtoolZones.some(devZone => zonesEqual(devZone, zone)) +} + +/** + * Removes a zone from the field file cache permanently. + */ +export async function removeZoneFromDevtools(zone: ScoringZonePreferences, zoneType: "scoring"): Promise +export async function removeZoneFromDevtools(zone: ProtectedZonePreferences, zoneType: "protected"): Promise +export async function removeZoneFromDevtools( + zone: ScoringZonePreferences | ProtectedZonePreferences, + zoneType: ZoneType +): Promise { + const field = World.sceneRenderer.mirabufSceneObjects.getField() + if (!field) throw new Error("No field loaded") + + const parts = field.mirabufInstance.parser.assembly.data?.parts + if (!parts) throw new Error("No field parts found") + + const editor = new FieldMiraEditor(parts) + + if (zoneType === "protected") { + throw new Error("Protected zone field file removal not yet implemented") + } + + const devtoolZones = editor.getUserData("devtool:scoring_zones") + if (!devtoolZones) return + + const filteredZones = devtoolZones.filter(devZone => !zonesEqual(devZone, zone)) + if (filteredZones.length === 0) { + editor.removeUserData("devtool:scoring_zones") + } else { + editor.setUserData("devtool:scoring_zones", filteredZones) + } + + if (field.fieldPreferences) { + field.fieldPreferences.scoringZones = filteredZones + PreferencesSystem.savePreferences?.() + field.updateScoringZones() + } + + const assembly = field.mirabufInstance.parser.assembly + const cacheId = field.cacheId + if (cacheId) { + const success = await MirabufCachingService.persistDevtoolChanges(cacheId, MiraType.FIELD, assembly) + if (!success) { + throw new Error("Failed to persist changes to cache") + } + } +} + +/** + * Modifies a zone in the field file cache permanently by replacing it with updated data. + */ +export async function modifyZoneInDevtools( + originalZone: ScoringZonePreferences, + modifiedZone: ScoringZonePreferences, + zoneType: "scoring" +): Promise +export async function modifyZoneInDevtools( + originalZone: ProtectedZonePreferences, + modifiedZone: ProtectedZonePreferences, + zoneType: "protected" +): Promise +export async function modifyZoneInDevtools( + originalZone: ScoringZonePreferences | ProtectedZonePreferences, + modifiedZone: ScoringZonePreferences | ProtectedZonePreferences, + zoneType: ZoneType +): Promise { + const field = World.sceneRenderer.mirabufSceneObjects.getField() + if (!field) throw new Error("No field loaded") + + const parts = field.mirabufInstance.parser.assembly.data?.parts + if (!parts) throw new Error("No field parts found") + + const editor = new FieldMiraEditor(parts) + + if (zoneType === "protected") { + throw new Error("Protected zone field file modification not yet implemented") + } + + const devtoolZones = editor.getUserData("devtool:scoring_zones") + if (!devtoolZones) return + + // Find and replace the zone in field file data + // Since we're in the scoring branch, we know modifiedZone is ScoringZonePreferences + const updatedZones = devtoolZones.map(devZone => { + if (zonesEqual(devZone, originalZone)) { + return modifiedZone as ScoringZonePreferences + } + return devZone + }) + + editor.setUserData("devtool:scoring_zones", updatedZones) + + if (field.fieldPreferences) { + field.fieldPreferences.scoringZones = updatedZones + PreferencesSystem.savePreferences?.() + field.updateScoringZones() + } + + const assembly = field.mirabufInstance.parser.assembly + const cacheId = field.cacheId + if (cacheId) { + const success = await MirabufCachingService.persistDevtoolChanges(cacheId, MiraType.FIELD, assembly) + if (!success) { + throw new Error("Failed to persist changes to cache") + } + } +} + +/** + * Automatically caches user-created or modified zones to the field file for persistence across reloads. + */ +export async function addUserZoneToDevtools( + zone: ScoringZonePreferences, + originalZone: ScoringZonePreferences | undefined, + zoneType: "scoring" +): Promise +export async function addUserZoneToDevtools( + zone: ProtectedZonePreferences, + originalZone: ProtectedZonePreferences | undefined, + zoneType: "protected" +): Promise +export async function addUserZoneToDevtools( + zone: ScoringZonePreferences | ProtectedZonePreferences, + originalZone: ScoringZonePreferences | ProtectedZonePreferences | undefined, + zoneType: ZoneType +): Promise { + const field = World.sceneRenderer.mirabufSceneObjects.getField() + if (!field) throw new Error("No field loaded") + + const parts = field.mirabufInstance.parser.assembly.data?.parts + if (!parts) throw new Error("No field parts found") + + const editor = new FieldMiraEditor(parts) + + if (zoneType === "protected") { + throw new Error("Protected zone field file addition not yet implemented") + } + + const devtoolZones = editor.getUserData("devtool:scoring_zones") || [] + + let updated = false + + if (originalZone) { + const existingIndex = devtoolZones.findIndex(devZone => zonesEqual(devZone, originalZone)) + if (existingIndex >= 0) { + devtoolZones[existingIndex] = zone as ScoringZonePreferences + updated = true + } + } + + if (!updated) { + const zoneExists = devtoolZones.some(devZone => zonesEqual(devZone, zone)) + if (!zoneExists) { + devtoolZones.push(zone as ScoringZonePreferences) + } + } + + editor.setUserData("devtool:scoring_zones", devtoolZones) + + if (field.fieldPreferences) { + field.fieldPreferences.scoringZones = devtoolZones + PreferencesSystem.savePreferences?.() + field.updateScoringZones() + } + + const assembly = field.mirabufInstance.parser.assembly + const cacheId = field.cacheId + if (cacheId) { + const success = await MirabufCachingService.persistDevtoolChanges(cacheId, MiraType.FIELD, assembly) + if (!success) { + throw new Error("Failed to persist changes to cache") + } + } +} + +/** + * Gets all zones that exist in the field file for a given type. + */ +export function getDevtoolZones(zoneType: "scoring"): ScoringZonePreferences[] | undefined +export function getDevtoolZones(zoneType: "protected"): ProtectedZonePreferences[] | undefined +export function getDevtoolZones(zoneType: ZoneType): ScoringZonePreferences[] | ProtectedZonePreferences[] | undefined { + const field = World.sceneRenderer.mirabufSceneObjects.getField() + if (!field) return undefined + + const parts = field.mirabufInstance.parser.assembly.data?.parts + if (!parts) return undefined + + const editor = new FieldMiraEditor(parts) + + if (zoneType === "protected") { + return undefined + } + + return editor.getUserData("devtool:scoring_zones") +}