diff --git a/src/main/ipc/sidekick-pet-bundle.test.ts b/src/main/ipc/sidekick-pet-bundle.test.ts deleted file mode 100644 index 0cafc2896..000000000 --- a/src/main/ipc/sidekick-pet-bundle.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - CODEX_PET_ANIMATIONS, - CODEX_PET_FRAME, - CODEX_PET_SPRITESHEET_PATH, - applyCodexPetDefaults, - readWebpDimensionsFromBuffer -} from './sidekick-pet-bundle' - -function u32(value: number): Buffer { - const buffer = Buffer.alloc(4) - buffer.writeUInt32LE(value, 0) - return buffer -} - -function u24(value: number): Buffer { - return Buffer.from([value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff]) -} - -function webpVp8x(width: number, height: number): Buffer { - const payload = Buffer.concat([Buffer.from([0, 0, 0, 0]), u24(width - 1), u24(height - 1)]) - return Buffer.concat([ - Buffer.from('RIFF'), - u32(4 + 8 + payload.byteLength), - Buffer.from('WEBP'), - Buffer.from('VP8X'), - u32(payload.byteLength), - payload - ]) -} - -describe('applyCodexPetDefaults', () => { - it('fills Codex pet manifests that omit Orca sprite metadata', () => { - const manifest = applyCodexPetDefaults({ id: 'apupepe', displayName: 'Pepe' }) - - expect(manifest.spritesheetPath).toBe(CODEX_PET_SPRITESHEET_PATH) - expect(manifest.frame).toEqual(CODEX_PET_FRAME) - expect(manifest.defaultAnimation).toBe('idle') - expect(manifest.animations).toEqual(CODEX_PET_ANIMATIONS) - expect(Object.keys(manifest.animations ?? [])).toEqual([ - 'idle', - 'running-right', - 'running-left', - 'waving', - 'jumping', - 'failed', - 'waiting', - 'running', - 'review' - ]) - }) - - it('fills Codex pet manifests that declare only spritesheetPath', () => { - const manifest = applyCodexPetDefaults({ - id: 'itachi', - displayName: 'Itachi', - spritesheetPath: 'spritesheet.webp' - }) - - expect(manifest.spritesheetPath).toBe(CODEX_PET_SPRITESHEET_PATH) - expect(manifest.frame).toEqual(CODEX_PET_FRAME) - expect(manifest.defaultAnimation).toBe('idle') - expect(manifest.animations).toEqual(CODEX_PET_ANIMATIONS) - }) - - it('does not override explicit Orca bundle sprite metadata', () => { - const manifest = applyCodexPetDefaults({ - spritesheetPath: 'custom.png', - frame: { width: 64, height: 64 }, - fps: 12, - defaultAnimation: 'blink', - animations: { blink: { row: 0, frames: 2 } } - }) - - expect(manifest).toEqual({ - spritesheetPath: 'custom.png', - frame: { width: 64, height: 64 }, - fps: 12, - defaultAnimation: 'blink', - animations: { blink: { row: 0, frames: 2 } } - }) - }) - - it('defaults only spritesheetPath when explicit sprite metadata is present', () => { - const manifest = applyCodexPetDefaults({ - frame: { width: 64, height: 64 }, - animations: { blink: { row: 0, frames: 2 } } - }) - - expect(manifest.spritesheetPath).toBe(CODEX_PET_SPRITESHEET_PATH) - expect(manifest.frame).toEqual({ width: 64, height: 64 }) - expect(manifest.animations).toEqual({ blink: { row: 0, frames: 2 } }) - expect(manifest.defaultAnimation).toBeUndefined() - }) -}) - -describe('readWebpDimensionsFromBuffer', () => { - it('reads VP8X WebP canvas dimensions without decoding pixels', () => { - expect(readWebpDimensionsFromBuffer(webpVp8x(1536, 1872))).toEqual({ - width: 1536, - height: 1872 - }) - }) - - it('returns null for non-WebP data', () => { - expect(readWebpDimensionsFromBuffer(Buffer.from('not an image'))).toBeNull() - }) -}) diff --git a/src/main/ipc/sidekick-pet-bundle.ts b/src/main/ipc/sidekick-pet-bundle.ts deleted file mode 100644 index 1d231ebaa..000000000 --- a/src/main/ipc/sidekick-pet-bundle.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { SpriteAnimation } from '../../shared/types' - -export type PetManifestLike = { - id?: string - displayName?: string - description?: string - spritesheetPath?: string - frame?: { - width: number - height: number - } - fps?: number - defaultAnimation?: string - animations?: Record -} - -export type ResolvedPetManifest = T & - PetManifestLike & { - spritesheetPath: string - } - -export const CODEX_PET_SPRITESHEET_PATH = 'spritesheet.webp' -export const CODEX_PET_FRAME = { width: 192, height: 208 } as const -export const CODEX_PET_DEFAULT_ANIMATION = 'idle' -export const CODEX_PET_DEFAULT_FPS = 8 - -export const CODEX_PET_ANIMATIONS: Record = { - idle: { row: 0, frames: 6 }, - 'running-right': { row: 1, frames: 8 }, - 'running-left': { row: 2, frames: 8 }, - waving: { row: 3, frames: 4 }, - jumping: { row: 4, frames: 5 }, - failed: { row: 5, frames: 8 }, - waiting: { row: 6, frames: 6 }, - running: { row: 7, frames: 6 }, - review: { row: 8, frames: 6 } -} - -function isCodexPetSpritePath(spritesheetPath: string | undefined): boolean { - return spritesheetPath === undefined || /(^|[/\\])spritesheet\.webp$/i.test(spritesheetPath) -} - -export function applyCodexPetDefaults( - manifest: T -): ResolvedPetManifest { - const shouldApplyCodexLayout = - isCodexPetSpritePath(manifest.spritesheetPath) && - manifest.frame === undefined && - manifest.animations === undefined - - if (!shouldApplyCodexLayout) { - return { - ...manifest, - spritesheetPath: manifest.spritesheetPath ?? CODEX_PET_SPRITESHEET_PATH - } as ResolvedPetManifest - } - - return { - ...manifest, - spritesheetPath: manifest.spritesheetPath ?? CODEX_PET_SPRITESHEET_PATH, - frame: manifest.frame ?? CODEX_PET_FRAME, - fps: manifest.fps ?? CODEX_PET_DEFAULT_FPS, - defaultAnimation: manifest.defaultAnimation ?? CODEX_PET_DEFAULT_ANIMATION, - animations: manifest.animations ?? CODEX_PET_ANIMATIONS - } -} - -function readUInt24LE(buffer: Buffer, offset: number): number { - return buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) -} - -export function readWebpDimensionsFromBuffer( - buffer: Buffer -): { width: number; height: number } | null { - if ( - buffer.byteLength < 20 || - buffer.toString('ascii', 0, 4) !== 'RIFF' || - buffer.toString('ascii', 8, 12) !== 'WEBP' - ) { - return null - } - - let offset = 12 - while (offset + 8 <= buffer.byteLength) { - const chunkType = buffer.toString('ascii', offset, offset + 4) - const chunkSize = buffer.readUInt32LE(offset + 4) - const dataOffset = offset + 8 - const dataEnd = dataOffset + chunkSize - if (dataEnd > buffer.byteLength) { - return null - } - - if (chunkType === 'VP8X' && chunkSize >= 10) { - return { - width: readUInt24LE(buffer, dataOffset + 4) + 1, - height: readUInt24LE(buffer, dataOffset + 7) + 1 - } - } - - if (chunkType === 'VP8L' && chunkSize >= 5 && buffer[dataOffset] === 0x2f) { - const b0 = buffer[dataOffset + 1] - const b1 = buffer[dataOffset + 2] - const b2 = buffer[dataOffset + 3] - const b3 = buffer[dataOffset + 4] - return { - width: 1 + (((b1 & 0x3f) << 8) | b0), - height: 1 + (((b3 & 0x0f) << 10) | (b2 << 2) | ((b1 & 0xc0) >> 6)) - } - } - - if ( - chunkType === 'VP8 ' && - chunkSize >= 10 && - buffer[dataOffset + 3] === 0x9d && - buffer[dataOffset + 4] === 0x01 && - buffer[dataOffset + 5] === 0x2a - ) { - const width = buffer.readUInt16LE(dataOffset + 6) & 0x3fff - const height = buffer.readUInt16LE(dataOffset + 8) & 0x3fff - return width > 0 && height > 0 ? { width, height } : null - } - - offset = dataEnd + (chunkSize % 2) - } - - return null -} diff --git a/src/renderer/src/components/pet/PetOverlay.tsx b/src/renderer/src/components/pet/PetOverlay.tsx index 06812a4a5..d434410d9 100644 --- a/src/renderer/src/components/pet/PetOverlay.tsx +++ b/src/renderer/src/components/pet/PetOverlay.tsx @@ -199,6 +199,7 @@ function usePrefersReducedMotion(): boolean { // from the store so the user can resize from the status-bar menu. const SIZE = 180 const POSITION_STORAGE_KEY = 'pet-overlay-position' +const LEGACY_POSITION_STORAGE_KEY = 'sidekick-overlay-position' type Position = { x: number; y: number } @@ -219,14 +220,26 @@ function loadStoredPosition(size: number = SIZE): Position | null { return null } try { - const raw = window.localStorage.getItem(POSITION_STORAGE_KEY) + let raw = window.localStorage.getItem(POSITION_STORAGE_KEY) + let migratedFromLegacy = false if (!raw) { - return null + raw = window.localStorage.getItem(LEGACY_POSITION_STORAGE_KEY) + if (!raw) { + return null + } + migratedFromLegacy = true } const parsed = JSON.parse(raw) as Partial if (typeof parsed.x !== 'number' || typeof parsed.y !== 'number') { return null } + if (migratedFromLegacy) { + try { + window.localStorage.setItem(POSITION_STORAGE_KEY, raw) + } catch { + // ignore storage failures + } + } // Why: clamp using the live overlay size so a persisted position from a // larger overlay doesn't slip off the bottom/right edge after a shrink. return clampToViewport({ x: parsed.x, y: parsed.y }, size)