156 lines
4.8 KiB
TypeScript
156 lines
4.8 KiB
TypeScript
import { Server } from 'socket.io'
|
|
import { TSocket } from '../../../../utilities/types'
|
|
import prisma from '../../../../utilities/prisma'
|
|
import type { Prisma, SpriteAction } from '@prisma/client'
|
|
import { writeFile, mkdir } from 'node:fs/promises'
|
|
import sharp from 'sharp'
|
|
import CharacterManager from '../../../../managers/characterManager'
|
|
import { getPublicPath } from '../../../../utilities/storage'
|
|
|
|
type SpriteActionInput = Omit<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> & {
|
|
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<void> {
|
|
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<ProcessedSpriteAction[]> {
|
|
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 = getPublicPath('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 = getPublicPath('sprites', id, `${action}.png`)
|
|
await writeFile(filename, combinedImage)
|
|
})
|
|
)
|
|
}
|
|
}
|
|
}
|