import fs from 'fs' import { BaseEvent } from '@/application/base/baseEvent' import { SocketEvent } from '@/application/enums' import type { UUID } from '@/application/types' import { SpriteAction } from '@/entities/spriteAction' import SpriteRepository from '@/repositories/spriteRepository' import sharp from 'sharp' interface SpriteImage { url: string offset: { x: number y: number } } interface SpriteActionData { action: string sprites: SpriteImage[] originX: number originY: number frameRate: number } interface UpdateSpritePayload { id: UUID name: string spriteActions: SpriteActionData[] } export default class SpriteUpdateEvent extends BaseEvent { private readonly spriteRepository: SpriteRepository = new SpriteRepository() public listen(): void { this.socket.on(SocketEvent.GM_SPRITE_UPDATE, this.handleEvent.bind(this)) } private async handleEvent(data: UpdateSpritePayload, callback: (success: boolean) => void): Promise { try { // Validate request and permissions if (!(await this.isCharacterGM())) { callback(false) return } // Get and validate sprite const sprite = await this.spriteRepository.getById(data.id) if (!sprite) { callback(false) return } await this.spriteRepository.getEntityManager().populate(sprite, ['spriteActions']) // Update sprite name await sprite.setName(data.name).setUpdatedAt(new Date()).save() // Process all sprite actions const success = await this.processAllSpriteActions(data.spriteActions, sprite) callback(success) } catch (error) { console.error(`Error updating sprite ${data.id}:`, error) callback(false) } } private async processAllSpriteActions(actionsData: SpriteActionData[], sprite: any): Promise { try { // Remove existing actions const existingActions = sprite.getSpriteActions() for (const existingAction of existingActions) { await this.spriteRepository.getEntityManager().removeAndFlush(existingAction) } // Process each action for (const actionData of actionsData) { if (actionData.sprites.length === 0) continue // Generate and save the sprite sheet const frameSize = await this.generateAndSaveSpriteSheet( actionData.sprites, sprite.getId(), actionData.action ) if (!frameSize) return false // Create and save sprite action const spriteAction = new SpriteAction() spriteAction.setSprite(sprite) sprite.getSpriteActions().add(spriteAction) spriteAction .setAction(actionData.action) .setSprites(actionData.sprites) .setOriginX(actionData.originX) .setOriginY(actionData.originY) .setFrameWidth(frameSize.width) .setFrameHeight(frameSize.height) .setFrameRate(actionData.frameRate) await this.spriteRepository.getEntityManager().persistAndFlush(spriteAction) } return true } catch (error) { console.error('Error processing sprite actions:', error) return false } } private async generateAndSaveSpriteSheet( sprites: SpriteImage[], spriteId: string, action: string ): Promise<{ width: number, height: number } | null> { try { if (sprites.length === 0) return { width: 0, height: 0 } // Extract image data from sprites const imageBuffers = await Promise.all( sprites.map(sprite => { const base64Data = sprite.url.split(';base64,').pop() if (!base64Data) throw new Error('Invalid base64 image') return Buffer.from(base64Data, 'base64') }) ) // Get metadata for all images to find the maximum dimensions const metadataList = await Promise.all( imageBuffers.map(buffer => sharp(buffer).metadata()) ) // Calculate the maximum width and height across all frames const maxWidth = Math.max(...metadataList.map(meta => meta.width || 0)) const maxHeight = Math.max(...metadataList.map(meta => meta.height || 0)) // Skip creation if we couldn't determine dimensions if (maxWidth === 0 || maxHeight === 0) { console.error('Could not determine sprite dimensions') return null } // Resize all frames to the same dimensions const resizedFrames = await Promise.all( imageBuffers.map(async (buffer) => { return sharp(buffer) .resize({ width: maxWidth, height: maxHeight, fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }) .toBuffer() }) ) // Create sprite sheet with uniformly sized frames const spriteSheet = await sharp({ create: { width: maxWidth * resizedFrames.length, height: maxHeight, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } } }) .composite( resizedFrames.map((buffer, index) => ({ input: buffer, left: index * maxWidth, top: 0 })) ) .png() .toBuffer() // Save sprite sheet const dir = `public/sprites/${spriteId}` await fs.promises.mkdir(dir, { recursive: true }) await fs.promises.writeFile(`${dir}/${action}.png`, spriteSheet) return { width: maxWidth, height: maxHeight } } catch (error) { console.error('Error generating sprite sheet:', error) return null } } }