import { Server } from 'socket.io' import { TSocket } from '../../../../utilities/types' import prisma from '../../../../utilities/prisma' import type { Prisma, SpriteAction } from '@prisma/client' import path from 'path' import { writeFile, mkdir } from 'node:fs/promises' import sharp from 'sharp' import CharacterManager from '../../../../managers/characterManager' type SpriteActionInput = Omit & { sprites: string[] } type Payload = { id: string name: string spriteActions: Prisma.JsonValue } interface ProcessedSpriteAction extends SpriteActionInput { frameWidth: number frameHeight: number buffersWithDimensions: Array<{ buffer: Buffer width: number | undefined height: number | undefined }> } export default class SpriteUpdateEvent { constructor( private readonly io: Server, private readonly socket: TSocket ) {} public listen(): void { this.socket.on('gm:sprite:update', this.handleSpriteUpdate.bind(this)) } private async handleSpriteUpdate(data: Payload, callback: (success: boolean) => void): Promise { const character = CharacterManager.getCharacterFromSocket(this.socket) if (character?.role !== 'gm') { return callback(false) } try { const parsedSpriteActions = validateSpriteActions(data.spriteActions) const processedActions = await processSprites(parsedSpriteActions) await updateDatabase(data.id, data.name, processedActions) await saveSpritesToDisk(data.id, processedActions) callback(true) } catch (error) { console.error('Error updating sprite:', error) callback(false) } function validateSpriteActions(spriteActions: Prisma.JsonValue): SpriteActionInput[] { try { const parsed = JSON.parse(JSON.stringify(spriteActions)) as SpriteActionInput[] if (!Array.isArray(parsed)) { throw new Error('spriteActions is not an array') } return parsed } catch (error) { console.error('Error parsing spriteActions:', error) throw error } } async function processSprites(spriteActions: SpriteActionInput[]): Promise { return Promise.all( spriteActions.map(async (spriteAction) => { const { action, sprites } = spriteAction if (!Array.isArray(sprites) || sprites.length === 0) { throw new Error(`Invalid sprites array for action: ${action}`) } const buffersWithDimensions = await Promise.all( sprites.map(async (sprite: string) => { const buffer = Buffer.from(sprite.split(',')[1], 'base64') const { width, height } = await sharp(buffer).metadata() return { buffer, width, height } }) ) const frameWidth = Math.max(...buffersWithDimensions.map((b) => b.width || 0)) const frameHeight = Math.max(...buffersWithDimensions.map((b) => b.height || 0)) return { ...spriteAction, frameWidth, frameHeight, buffersWithDimensions } }) ) } async function updateDatabase(id: string, name: string, processedActions: ProcessedSpriteAction[]) { await prisma.sprite.update({ where: { id }, data: { name, spriteActions: { deleteMany: { spriteId: id }, create: processedActions.map(({ action, sprites, originX, originY, isAnimated, isLooping, frameWidth, frameHeight, frameSpeed }) => ({ action, sprites, originX, originY, isAnimated, isLooping, frameWidth, frameHeight, frameSpeed })) } } }) } async function saveSpritesToDisk(id: string, processedActions: ProcessedSpriteAction[]) { const publicFolder = path.join(process.cwd(), 'public', 'sprites', id) await mkdir(publicFolder, { recursive: true }) await Promise.all( processedActions.map(async ({ action, buffersWithDimensions, frameWidth, frameHeight }) => { const combinedImage = await sharp({ create: { width: frameWidth * buffersWithDimensions.length, height: frameHeight, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } } }) .composite( buffersWithDimensions.map(({ buffer }, index) => ({ input: buffer, left: index * frameWidth, top: 0 })) ) .png() .toBuffer() const filename = path.join(publicFolder, `${action}.png`) await writeFile(filename, combinedImage) }) ) } } }