forked from noxious/server
Loading sprites and animations now works
This commit is contained in:
parent
9273d877f8
commit
e3d556dce2
@ -39,7 +39,6 @@ export default function (socket: TSocket, io: Server) {
|
||||
})
|
||||
|
||||
callback(true)
|
||||
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
callback(false)
|
||||
|
@ -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)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user