diff --git a/src/events/gm/sprite/Remove.ts b/src/events/gm/sprite/Remove.ts index 31926f0..b4f1d5f 100644 --- a/src/events/gm/sprite/Remove.ts +++ b/src/events/gm/sprite/Remove.ts @@ -39,7 +39,6 @@ export default function (socket: TSocket, io: Server) { }) callback(true) - } catch (e) { console.log(e) callback(false) diff --git a/src/events/gm/sprite/Update.ts b/src/events/gm/sprite/Update.ts index e3f1d2d..410aaf1 100644 --- a/src/events/gm/sprite/Update.ts +++ b/src/events/gm/sprite/Update.ts @@ -3,8 +3,7 @@ import { TSocket } from '../../../utilities/Types' import prisma from '../../../utilities/Prisma' import type { Prisma, SpriteAction } from '@prisma/client' import path from 'path' -import { writeFile } from 'node:fs/promises' -import fs from 'fs/promises' +import { writeFile, mkdir } from 'node:fs/promises' import sharp from 'sharp' type SpriteActionInput = Omit & { @@ -17,6 +16,16 @@ type Payload = { spriteActions: Prisma.JsonValue } +interface ProcessedSpriteAction extends SpriteActionInput { + frameWidth: number + frameHeight: number + buffersWithDimensions: Array<{ + buffer: Buffer + width: number | undefined + height: number | undefined + }> +} + export default function (socket: TSocket, io: Server) { socket.on('gm:sprite:update', async (data: Payload, callback: (success: boolean) => void) => { if (socket.character?.role !== 'gm') { @@ -25,104 +34,11 @@ export default function (socket: TSocket, io: Server) { } try { - // Parse and validate spriteActions - let parsedSpriteActions: SpriteActionInput[] - try { - parsedSpriteActions = JSON.parse(JSON.stringify(data.spriteActions)) as SpriteActionInput[] - if (!Array.isArray(parsedSpriteActions)) { - throw new Error('spriteActions is not an array') - } - } catch (error) { - console.error('Error parsing spriteActions:', error) - callback(false) - return - } + const parsedSpriteActions = validateSpriteActions(data.spriteActions) + const processedActions = await processSprites(parsedSpriteActions) - // Process the sprites to determine the largest dimensions - const processedActions = await Promise.all( - parsedSpriteActions.map(async (spriteAction) => { - const { action, sprites } = spriteAction - - if (!Array.isArray(sprites) || sprites.length === 0) { - throw new Error(`Invalid sprites array for action: ${action}`) - } - - // Convert base64 strings to buffers and get dimensions - 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 } - }) - ) - - // Find the largest width and 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 - } - }) - ) - - // Update the database with the new sprite actions (including calculated frame sizes) - await prisma.sprite.update({ - where: { id: data.id }, - data: { - name: data.name, - spriteActions: { - deleteMany: { spriteId: data.id }, - create: processedActions.map((spriteAction) => ({ - action: spriteAction.action, - sprites: spriteAction.sprites, - origin_x: spriteAction.origin_x, - origin_y: spriteAction.origin_y, - isAnimated: spriteAction.isAnimated, - isLooping: spriteAction.isLooping, - frameWidth: spriteAction.frameWidth, - frameHeight: spriteAction.frameHeight, - frameSpeed: spriteAction.frameSpeed - })) - } - } - }) - - const public_folder = path.join(process.cwd(), 'public', 'sprites', data.id) - await fs.mkdir(public_folder, { recursive: true }) - - // Process and save each spriteAction - await Promise.all( - processedActions.map(async (spriteAction) => { - const { action, buffersWithDimensions, frameWidth, frameHeight } = spriteAction - - // Combine all sprites into a single image - 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() - - // Save the combined image - const filename = path.join(public_folder, `${action}.png`) - await writeFile(filename, combinedImage) - }) - ) + await updateDatabase(data.id, data.name, processedActions) + await saveSpritesToDisk(data.id, processedActions) callback(true) } catch (error) { @@ -131,3 +47,99 @@ export default function (socket: TSocket, io: Server) { } }) } + +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, origin_x, origin_y, isAnimated, isLooping, frameWidth, frameHeight, frameSpeed }) => ({ + action, + sprites, + origin_x, + origin_y, + 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) + }) + ) +} diff --git a/src/repositories/SpriteRepository.ts b/src/repositories/SpriteRepository.ts index a618320..02f1368 100644 --- a/src/repositories/SpriteRepository.ts +++ b/src/repositories/SpriteRepository.ts @@ -2,7 +2,7 @@ import prisma from '../utilities/Prisma' // Import the global Prisma instance import { Sprite, SpriteAction } from '@prisma/client' class SpriteRepository { - async getById(id: string): Promise { + async getById(id: string) { return prisma.sprite.findUnique({ where: { id }, include: { @@ -11,7 +11,7 @@ class SpriteRepository { }) } - async getAll(): Promise { + async getAll() { return prisma.sprite.findMany({ include: { spriteActions: true diff --git a/src/utilities/Http.ts b/src/utilities/Http.ts index e1070f3..eae4900 100644 --- a/src/utilities/Http.ts +++ b/src/utilities/Http.ts @@ -1,7 +1,3 @@ -/** - * Resources: - * https://stackoverflow.com/questions/76131891/what-is-the-best-method-for-socket-io-authentication - */ import { Application, Request, Response } from 'express' import UserService from '../services/UserService' import jwt from 'jsonwebtoken' @@ -11,6 +7,7 @@ import path from 'path' import { TAsset } from './Types' import tileRepository from '../repositories/TileRepository' import objectRepository from '../repositories/ObjectRepository' +import spriteRepository from '../repositories/SpriteRepository' async function addHttpRoutes(app: Application) { app.get('/assets', async (req: Request, res: Response) => { @@ -20,7 +17,7 @@ async function addHttpRoutes(app: Application) { assets.push({ key: tile.id, url: '/assets/tiles/' + tile.id + '.png', - group: 'tiles', + group: 'tiles' }) }) @@ -29,7 +26,19 @@ async function addHttpRoutes(app: Application) { assets.push({ key: object.id, url: '/assets/objects/' + object.id + '.png', - group: 'objects', + group: 'objects' + }) + }) + + const sprites = await spriteRepository.getAll() + // sprites all contain spriteActions, loop through these + sprites.forEach((sprite) => { + sprite.spriteActions.forEach((spriteAction) => { + assets.push({ + key: spriteAction.id, + url: '/assets/sprites/' + sprite.id + '/' + spriteAction.action + '.png', + group: spriteAction.isAnimated ? 'sprite_animations' : 'sprites' + }) }) }) diff --git a/src/utilities/Types.ts b/src/utilities/Types.ts index ae4db9d..92d2534 100644 --- a/src/utilities/Types.ts +++ b/src/utilities/Types.ts @@ -26,5 +26,7 @@ export type TZoneCharacter = Character & {} export type TAsset = { key: string url: string - group: 'tiles' | 'objects' | 'sound' | 'music' | 'ui' | 'font' | 'other' | 'sprite' + group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other' + frameWidth?: number + frameHeight?: number }