Skip to content

Support Fields Having Pre Select Scoring Zones [AARD-1894] #1211

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 102 additions & 1 deletion fission/src/mirabuf/MirabufLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,49 @@ class MirabufCachingService {
}
}

/**
* Caches and gets local Mirabuf file with cache info
*
* @param {ArrayBuffer} buffer ArrayBuffer of Mirabuf file.
* @param {MiraType} miraType Type of Mirabuf Assembly.
*
* @returns {Promise<{assembly: mirabuf.Assembly, cacheInfo: MirabufCacheInfo} | undefined>} Promise with the result of the promise. Assembly and cache info of the mirabuf file if successful, undefined if not.
*/
public static async cacheAndGetLocalWithInfo(
buffer: ArrayBuffer,
miraType: MiraType
): Promise<{ assembly: mirabuf.Assembly; cacheInfo: MirabufCacheInfo } | undefined> {
const key = await this.hashBuffer(buffer)
const map = MirabufCachingService.getCacheMap(miraType)
const target = map[key]
const assembly = this.assemblyFromBuffer(buffer)

// Check if assembly has devtool data and update name accordingly
let displayName = assembly.info?.name ?? undefined
if (assembly.data?.parts?.userData?.data) {
const devtoolKeys = Object.keys(assembly.data.parts.userData.data).filter(k => k.startsWith("devtool:"))
if (devtoolKeys.length > 0) {
displayName = displayName ? `Edited ${displayName}` : "Edited Field"
}
}

if (!target) {
const cacheInfo = await MirabufCachingService.storeInCache(key, buffer, miraType, displayName)
if (cacheInfo) {
return { assembly, cacheInfo }
}
} else {
// Update existing cache info with new name if it has devtool data
if (displayName && displayName !== target.name) {
await MirabufCachingService.cacheInfo(key, miraType, displayName)
target.name = displayName
}
return { assembly, cacheInfo: target }
}

return undefined
}

/**
* Caches and gets local Mirabuf file
*
Expand All @@ -258,8 +301,17 @@ class MirabufCachingService {
const target = map[key]
const assembly = this.assemblyFromBuffer(buffer)

// Check if assembly has devtool data and update name accordingly
let displayName = assembly.info?.name ?? undefined
if (assembly.data?.parts?.userData?.data) {
const devtoolKeys = Object.keys(assembly.data.parts.userData.data).filter(k => k.startsWith("devtool:"))
if (devtoolKeys.length > 0) {
displayName = displayName ? `Edited ${displayName}` : "Edited Field"
}
}

if (!target) {
await MirabufCachingService.storeInCache(key, buffer, miraType, assembly.info?.name ?? undefined)
await MirabufCachingService.storeInCache(key, buffer, miraType, displayName)
}

return assembly
Expand Down Expand Up @@ -369,6 +421,55 @@ class MirabufCachingService {
backUpFields = {}
}

/**
* Persists devtool changes back to the cache by re-encoding the assembly
*
* @param {MirabufCacheID} id ID of the cached mirabuf file
* @param {MiraType} miraType Type of Mirabuf Assembly
* @param {mirabuf.Assembly} assembly The updated assembly with devtool changes
*
* @returns {Promise<boolean>} Promise with the result. True if successful, false if not.
*/
public static async persistDevtoolChanges(
id: MirabufCacheID,
miraType: MiraType,
assembly: mirabuf.Assembly
): Promise<boolean> {
try {
// Re-encode the assembly with devtool changes
const updatedBuffer = mirabuf.Assembly.encode(assembly).finish()

// Update the cached buffer
const cache = miraType == MiraType.ROBOT ? backUpRobots : backUpFields
if (cache[id]) {
cache[id].buffer = updatedBuffer
}

// Update OPFS if available
if (canOPFS) {
const fileHandle = await (
miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle
).getFileHandle(id, { create: false })
const writable = await fileHandle.createWritable()
await writable.write(updatedBuffer)
await writable.close()
}

World.analyticsSystem?.event("Devtool Cache Persist", {
key: id,
type: miraType == MiraType.ROBOT ? "robot" : "field",
assemblyName: assembly.info?.name ?? "unknown",
fileSize: updatedBuffer.byteLength,
})

return true
} catch (e) {
console.error("Failed to persist devtool changes", e)
World.analyticsSystem?.exception("Failed to persist devtool changes to cache")
return false
}
Comment on lines +438 to +470
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should try to put the try block around as small a section of fallible code as possible. It seems like the fallible function here is the event method call (obviously any file I/O is subject to failure as well, but since we're creating the directory, it shouldn't regularly fail), unless I'm missing something. So maybe we could more the try block to be just around the event method class.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would argue that most of the lines in the current try block is fallible. For example, const updatedBuffer = mirabuf.Assembly.encode(assembly).finish() if the assembly is malformed.

}

// Optional name for when assembly is being decoded anyway like in CacheAndGetLocal()
private static async storeInCache(
key: string,
Expand Down
53 changes: 34 additions & 19 deletions fission/src/mirabuf/MirabufSceneObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { SimConfigData } from "@/ui/panels/simulation/SimConfigShared"
import WPILibBrain from "@/systems/simulation/wpilib_brain/WPILibBrain"
import { Alliance } from "@/systems/preferences/PreferenceTypes"
import { OnContactAddedEvent } from "@/systems/physics/ContactEvents"
import FieldMiraEditor from "./FieldMiraEditor"

const DEBUG_BODIES = false

Expand Down Expand Up @@ -90,14 +91,14 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {

private _nameTag: SceneOverlayTag | undefined
private _centerOfMassIndicator: THREE.Mesh | undefined
private _centerOfMassListenerUnsubscribe: (() => void) | undefined
private _intakeActive = false
private _ejectorActive = false

private _lastEjectableToastTime = 0
private static readonly EJECTABLE_TOAST_COOLDOWN_MS = 500

private _collision?: (event: OnContactAddedEvent) => void
private _cacheId?: string

public get intakeActive() {
return this._intakeActive
Expand Down Expand Up @@ -145,7 +146,7 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
}

public get activeEjectables(): Jolt.BodyID[] {
return this._ejectables.map(e => e.gamePieceBodyId!)
return this._ejectables.map(e => e.gamePieceBodyId!).filter(x => x !== undefined)
}

public get miraType(): MiraType {
Expand Down Expand Up @@ -174,11 +175,21 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
this._alliance = alliance
}

public constructor(mirabufInstance: MirabufInstance, assemblyName: string, progressHandle?: ProgressHandle) {
public get cacheId() {
return this._cacheId
}

public constructor(
mirabufInstance: MirabufInstance,
assemblyName: string,
progressHandle?: ProgressHandle,
cacheId?: string
) {
super()

this._mirabufInstance = mirabufInstance
this._assemblyName = assemblyName
this._cacheId = cacheId

progressHandle?.update("Creating mechanism...", 0.9)

Expand Down Expand Up @@ -221,18 +232,8 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
})
material.depthTest = false
this._centerOfMassIndicator = new THREE.Mesh(new THREE.SphereGeometry(0.02), material)
this._centerOfMassIndicator.visible = PreferencesSystem.getGlobalPreference("ShowCenterOfMassIndicators")

this._centerOfMassIndicator.visible = false
World.sceneRenderer.scene.add(this._centerOfMassIndicator)

this._centerOfMassListenerUnsubscribe = PreferencesSystem.addPreferenceEventListener(
"ShowCenterOfMassIndicators",
e => {
if (this._centerOfMassIndicator) {
this._centerOfMassIndicator.visible = e.prefValue
}
}
)
}
}

Expand Down Expand Up @@ -357,9 +358,6 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
if (this._brain && this._brain instanceof SynthesisBrain) {
this._brain.clearControls()
}
if (this._centerOfMassListenerUnsubscribe) {
this._centerOfMassListenerUnsubscribe()
}
}

public eject() {
Expand Down Expand Up @@ -453,6 +451,7 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
if (this._centerOfMassIndicator) {
const netCoM = totalMass > 0 ? weightedCOM.Div(totalMass) : weightedCOM
this._centerOfMassIndicator.position.set(netCoM.GetX(), netCoM.GetY(), netCoM.GetZ())
this._centerOfMassIndicator.visible = PreferencesSystem.getGlobalPreference("ShowCenterOfMassIndicators")
}
}

Expand Down Expand Up @@ -660,6 +659,21 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
}

this._fieldPreferences = PreferencesSystem.getFieldPreferences(this.assemblyName)

// For fields, sync devtool data with field preferences
if (this.miraType === MiraType.FIELD) {
const parts = this._mirabufInstance.parser.assembly.data?.parts
if (parts) {
const editor = new FieldMiraEditor(parts)
const devtoolScoringZones = editor.getUserData("devtool:scoring_zones")

if (devtoolScoringZones && Array.isArray(devtoolScoringZones)) {
this._fieldPreferences.scoringZones = devtoolScoringZones
PreferencesSystem.setFieldPreferences(this.assemblyName, this._fieldPreferences)
PreferencesSystem.savePreferences()
}
}
}
}

public updateSimConfig(config: SimConfigData | undefined) {
Expand Down Expand Up @@ -792,15 +806,16 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {

export async function createMirabuf(
assembly: mirabuf.Assembly,
progressHandle?: ProgressHandle
progressHandle?: ProgressHandle,
cacheId?: string
): Promise<MirabufSceneObject | null | undefined> {
const parser = new MirabufParser(assembly, progressHandle)
if (parser.maxErrorSeverity >= ParseErrorSeverity.UNIMPORTABLE) {
console.error(`Assembly Parser produced significant errors for '${assembly.info!.name!}'`)
return
}

return new MirabufSceneObject(new MirabufInstance(parser), assembly.info!.name!, progressHandle)
return new MirabufSceneObject(new MirabufInstance(parser), assembly.info!.name!, progressHandle, cacheId)
}

/**
Expand Down
49 changes: 49 additions & 0 deletions fission/src/test/FieldMiraEditor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ function mockParts(): mirabuf.IParts {
return { userData: { data: {} } }
}

const scoringZonePayload = [
{
name: "Red Zone",
alliance: "red",
parentNode: "root",
points: 5,
destroyGamepiece: false,
persistentPoints: true,
deltaTransformation: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
},
]

describe("Basic Field Mira Editor Tests", () => {
test("writes and reads devtool data", () => {
const parts = mockParts()
Expand Down Expand Up @@ -75,3 +87,40 @@ describe("Basic Field Mira Editor Tests", () => {
expect(editor.getAllDevtoolKeys()).toEqual(["devtool:keep"])
})
})

describe("Devtool Scoring Zones Caching Tests", () => {
test("add scoring zones and read back", () => {
const parts = mockParts()
const editor = new FieldMiraEditor(parts)
editor.setUserData("devtool:scoring_zones", scoringZonePayload)
expect(editor.getUserData("devtool:scoring_zones")).toEqual(scoringZonePayload)
expect(editor.getAllDevtoolKeys()).toContain("devtool:scoring_zones")
})

test("overwrite and remove scoring zones", () => {
const parts = mockParts()
const editor = new FieldMiraEditor(parts)
editor.setUserData("devtool:scoring_zones", scoringZonePayload)

const newPayload = [{ ...scoringZonePayload[0], name: "Blue Zone", alliance: "blue" }]
editor.setUserData("devtool:scoring_zones", newPayload)
expect(editor.getUserData("devtool:scoring_zones")).toEqual(newPayload)

editor.removeUserData("devtool:scoring_zones")
expect(editor.getUserData("devtool:scoring_zones")).toBeUndefined()
expect(editor.getAllDevtoolKeys()).not.toContain("devtool:scoring_zones")
})
})

describe("Caching tests", () => {
test("cache round-trip preserves devtool scoring zones", () => {
const parts = mockParts()
const editor = new FieldMiraEditor(parts)
editor.setUserData("devtool:scoring_zones", scoringZonePayload)

const encoded = mirabuf.Parts.encode(parts).finish()
const decoded = mirabuf.Parts.decode(encoded)
const roundTripEditor = new FieldMiraEditor(decoded)
expect(roundTripEditor.getUserData("devtool:scoring_zones")).toEqual(scoringZonePayload)
})
})
9 changes: 7 additions & 2 deletions fission/src/ui/modals/mirabuf/ImportLocalMirabufModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,13 @@ const ImportLocalMirabufModal: React.FC<ModalPropsImpl> = ({ modalId }) => {

const hashBuffer = await selectedFile.arrayBuffer()
World.physicsSystem.holdPause(PAUSE_REF_ASSEMBLY_SPAWNING)
await MirabufCachingService.cacheAndGetLocal(hashBuffer, miraType)
.then(x => createMirabuf(x!))
await MirabufCachingService.cacheAndGetLocalWithInfo(hashBuffer, miraType)
.then(result => {
if (result) {
return createMirabuf(result.assembly, undefined, result.cacheInfo.id)
}
return undefined
})
.then(x => {
if (x) {
World.sceneRenderer.registerSceneObject(x)
Expand Down
Loading
Loading