import fs from 'fs' import sharp from 'sharp' import type { UUID } from '@/application/types' import { BaseEvent } from '@/application/base/baseEvent' import { SocketEvent } from '@/application/enums' import { SpriteAction } from '@/entities/spriteAction' import SpriteRepository from '@/repositories/spriteRepository' interface SpriteImage { url: string offset: { x: number y: number } } interface ImageDimensions { width: number height: number offsetX: number offsetY: number } interface EffectiveDimensions { width: number height: number top: number bottom: number } type Payload = { id: UUID name: string spriteActions: Array<{ action: string sprites: SpriteImage[] originX: number originY: number frameRate: number }> } export default class SpriteUpdateEvent extends BaseEvent { public listen(): void { this.socket.on(SocketEvent.GM_SPRITE_UPDATE, this.handleEvent.bind(this)) } private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise { try { if (!(await this.isCharacterGM())) return const spriteRepository = new SpriteRepository() const sprite = await spriteRepository.getById(data.id) if (!sprite) return callback(false) await spriteRepository.getEntityManager().populate(sprite, ['spriteActions']) // Update sprite in database await sprite.setName(data.name).save() // First verify all sprite sheets can be generated for (const actionData of data.spriteActions) { if (!(await this.generateSpriteSheet(actionData.sprites, sprite.getId(), actionData.action))) { return callback(false) } } const existingActions = sprite.getSpriteActions() // Remove existing actions only after confirming sprite sheets generated successfully for (const existingAction of existingActions) { await spriteRepository.getEntityManager().removeAndFlush(existingAction) } // Create new actions for (const actionData of data.spriteActions) { // Process images and calculate dimensions const imageData = await Promise.all(actionData.sprites.map((sprite) => this.processImage(sprite))) const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions)) // Calculate total height needed for the sprite sheet const maxHeight = Math.max(...effectiveDimensions.map((d) => d.height)) const maxTop = Math.max(...effectiveDimensions.map((d) => d.top)) const maxBottom = Math.max(...effectiveDimensions.map((d) => d.bottom)) const totalHeight = maxHeight + maxTop + maxBottom 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(await this.calculateMaxWidth(actionData.sprites)) .setFrameHeight(totalHeight) .setFrameRate(actionData.frameRate) await spriteRepository.getEntityManager().persistAndFlush(spriteAction) } return callback(true) } catch (error) { console.error(`Error updating sprite ${data.id}:`, error) return callback(false) } } private async generateSpriteSheet(sprites: SpriteImage[], spriteId: string, action: string): Promise { try { if (!sprites.length) return true // Process all images and get their dimensions const imageData = await Promise.all(sprites.map((sprite) => this.processImage(sprite))) const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions)) // Calculate maximum dimensions const maxWidth = Math.max(...effectiveDimensions.map((d) => d.width)) const maxHeight = Math.max(...effectiveDimensions.map((d) => d.height)) const maxTop = Math.max(...effectiveDimensions.map((d) => d.top)) const maxBottom = Math.max(...effectiveDimensions.map((d) => d.bottom)) // Calculate total height needed const totalHeight = maxHeight + maxTop + maxBottom // Process images and create sprite sheet const processedImages = await Promise.all( sprites.map(async (sprite, index) => { const { width, height, offsetX, offsetY } = await this.processImage(sprite) const uri = sprite.url.split(';base64,').pop() if (!uri) throw new Error('Invalid base64 image') const buffer = Buffer.from(uri, 'base64') // Create individual frame const left = offsetX >= 0 ? offsetX : 0 const verticalOffset = totalHeight - height - (offsetY >= 0 ? offsetY : 0) return sharp({ create: { width: maxWidth, height: totalHeight, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } } }) .composite([{ input: buffer, left, top: verticalOffset }]) .png() .toBuffer() }) ) // Combine frames into sprite sheet const spriteSheet = await sharp({ create: { width: maxWidth * sprites.length, height: totalHeight, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } } }) .composite( processedImages.map((buffer, index) => ({ input: buffer, left: index * maxWidth, top: 0 })) ) .png() .toBuffer() // Ensure directory exists const dir = `public/sprites/${spriteId}` await fs.promises.mkdir(dir, { recursive: true }) // Save the sprite sheet await fs.promises.writeFile(`${dir}/${action}.png`, spriteSheet) return true } catch (error) { console.error('Error generating sprite sheet:', error) return false } } private async processImage(sprite: SpriteImage): Promise { const uri = sprite.url.split(';base64,').pop() if (!uri) throw new Error('Invalid base64 image') const buffer = Buffer.from(uri, 'base64') const metadata = await sharp(buffer).metadata() return { width: metadata.width ?? 0, height: metadata.height ?? 0, offsetX: sprite.offset?.x ?? 0, offsetY: sprite.offset?.y ?? 0 } } private calculateEffectiveDimensions(imageDimensions: ImageDimensions): EffectiveDimensions { return { width: imageDimensions.width + Math.abs(imageDimensions.offsetX), height: imageDimensions.height + Math.abs(imageDimensions.offsetY), top: imageDimensions.offsetY >= 0 ? imageDimensions.offsetY : 0, bottom: imageDimensions.offsetY < 0 ? Math.abs(imageDimensions.offsetY) : 0 } } private async calculateMaxWidth(sprites: SpriteImage[]): Promise { if (!sprites.length) return 0 // Process all images and get their dimensions const imageData = await Promise.all(sprites.map((sprite) => this.processImage(sprite))) const effectiveDimensions = imageData.map((dimensions) => this.calculateEffectiveDimensions(dimensions)) // Calculate maximum width needed return Math.max(...effectiveDimensions.map((d) => d.width)) } }