From e3d556dce22f58ab5a40b6262d3505b17f830780 Mon Sep 17 00:00:00 2001
From: Dennis Postma <dennis@directonline.io>
Date: Fri, 26 Jul 2024 20:20:25 +0200
Subject: [PATCH] Loading sprites and animations now works

---
 src/events/gm/sprite/Remove.ts       |   1 -
 src/events/gm/sprite/Update.ts       | 210 ++++++++++++++-------------
 src/repositories/SpriteRepository.ts |   4 +-
 src/utilities/Http.ts                |  21 ++-
 src/utilities/Types.ts               |   4 +-
 5 files changed, 131 insertions(+), 109 deletions(-)

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<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> & {
@@ -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<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, 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<Sprite | null> {
+  async getById(id: string) {
     return prisma.sprite.findUnique({
       where: { id },
       include: {
@@ -11,7 +11,7 @@ class SpriteRepository {
     })
   }
 
-  async getAll(): Promise<Sprite[]> {
+  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
 }