1
0
forked from noxious/server

Loading sprites and animations now works

This commit is contained in:
Dennis Postma 2024-07-26 20:20:25 +02:00
parent 9273d877f8
commit e3d556dce2
5 changed files with 131 additions and 109 deletions

View File

@ -39,7 +39,6 @@ export default function (socket: TSocket, io: Server) {
})
callback(true)
} catch (e) {
console.log(e)
callback(false)

View File

@ -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)
})
)
}

View File

@ -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

View File

@ -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'
})
})
})

View File

@ -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
}