Almost finalised refactoring

This commit is contained in:
Dennis Postma 2025-01-03 14:35:02 +01:00
parent fecdf222d7
commit a40b71140a
35 changed files with 257 additions and 410 deletions

View File

@ -1,6 +1,6 @@
import { Migration } from '@mikro-orm/migrations'; import { Migration } from '@mikro-orm/migrations';
export class Migration20250102162954 extends Migration { export class Migration20250103003053 extends Migration {
override async up(): Promise<void> { override async up(): Promise<void> {
this.addSql(`create table \`map\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`width\` int not null default 10, \`height\` int not null default 10, \`tiles\` json null, \`pvp\` tinyint(1) not null default false, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`create table \`map\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`width\` int not null default 10, \`height\` int not null default 10, \`tiles\` json null, \`pvp\` tinyint(1) not null default false, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
@ -15,7 +15,7 @@ export class Migration20250102162954 extends Migration {
this.addSql(`alter table \`map_event_tile_teleport\` add unique \`map_event_tile_teleport_map_event_tile_id_unique\`(\`map_event_tile_id\`);`); this.addSql(`alter table \`map_event_tile_teleport\` add unique \`map_event_tile_teleport_map_event_tile_id_unique\`(\`map_event_tile_id\`);`);
this.addSql(`alter table \`map_event_tile_teleport\` add index \`map_event_tile_teleport_to_map_id_index\`(\`to_map_id\`);`); this.addSql(`alter table \`map_event_tile_teleport\` add index \`map_event_tile_teleport_to_map_id_index\`(\`to_map_id\`);`);
this.addSql(`create table \`map_object\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`tags\` json null, \`origin_x\` int not null default 0, \`origin_y\` int not null default 0, \`is_animated\` tinyint(1) not null default false, \`frame_rate\` int not null default 0, \`frame_width\` int not null default 0, \`frame_height\` int not null default 0, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`create table \`map_object\` (\`id\` varchar(255) not null, \`name\` varchar(255) not null, \`tags\` json null, \`origin_x\` numeric(10,2) not null default 0, \`origin_y\` numeric(10,2) not null default 0, \`is_animated\` tinyint(1) not null default false, \`frame_rate\` int not null default 0, \`frame_width\` int not null default 0, \`frame_height\` int not null default 0, \`created_at\` datetime not null, \`updated_at\` datetime not null, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`create table \`placed_map_object\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`map_object_id\` varchar(255) not null, \`depth\` int not null default 0, \`is_rotated\` tinyint(1) not null default false, \`position_x\` int not null default 0, \`position_y\` int not null default 0, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`); this.addSql(`create table \`placed_map_object\` (\`id\` varchar(255) not null, \`map_id\` varchar(255) not null, \`map_object_id\` varchar(255) not null, \`depth\` int not null default 0, \`is_rotated\` tinyint(1) not null default false, \`position_x\` int not null default 0, \`position_y\` int not null default 0, primary key (\`id\`)) default character set utf8mb4 engine = InnoDB;`);
this.addSql(`alter table \`placed_map_object\` add index \`placed_map_object_map_id_index\`(\`map_id\`);`); this.addSql(`alter table \`placed_map_object\` add index \`placed_map_object_map_id_index\`(\`map_id\`);`);

View File

@ -17,6 +17,7 @@ export default defineConfig({
password: serverConfig.DB_PASS, password: serverConfig.DB_PASS,
dbName: serverConfig.DB_NAME, dbName: serverConfig.DB_NAME,
debug: serverConfig.ENV !== 'production', debug: serverConfig.ENV !== 'production',
// allowGlobalContext: true,
driverOptions: { driverOptions: {
allowPublicKeyRetrieval: true allowPublicKeyRetrieval: true
}, },

Binary file not shown.

View File

@ -6,13 +6,11 @@ import config from '../../mikro-orm.config'
class Database { class Database {
private static orm: MikroORM private static orm: MikroORM
private static em: EntityManager
private static logger = Logger.type(LoggerType.APP) private static logger = Logger.type(LoggerType.APP)
public static async initialize(): Promise<void> { public static async initialize(): Promise<void> {
try { try {
Database.orm = await MikroORM.init(config) Database.orm = await MikroORM.init(config)
Database.em = Database.orm.em.fork()
this.logger.info('Database connection initialized') this.logger.info('Database connection initialized')
} catch (error) { } catch (error) {
this.logger.error(`MikroORM connection failed: ${error}`) this.logger.error(`MikroORM connection failed: ${error}`)
@ -20,18 +18,8 @@ class Database {
} }
} }
public static getORM(): MikroORM {
if (!Database.orm) {
throw new Error('Database not initialized. Call Database.initialize() first.')
}
return Database.orm
}
public static getEntityManager(): EntityManager { public static getEntityManager(): EntityManager {
if (!Database.em) { return Database.orm.em.fork()
throw new Error('Database not initialized. Call Database.initialize() first.')
}
return Database.em
} }
} }

View File

@ -27,7 +27,7 @@ export default class InitCommand extends BaseCommand {
public async execute(): Promise<void> { public async execute(): Promise<void> {
// Assets // Assets
await this.importTiles() await this.importTiles()
await this.importObjects() await this.importMapObjects()
await this.createCharacterType() await this.createCharacterType()
await this.createCharacterHair() await this.createCharacterHair()
// await this.createCharacterEquipment() // await this.createCharacterEquipment()
@ -51,19 +51,19 @@ export default class InitCommand extends BaseCommand {
} }
} }
private async importObjects(): Promise<void> { private async importMapObjects(): Promise<void> {
for (const object of fs.readdirSync(Storage.getPublicPath('objects'))) { for (const mapObject of fs.readdirSync(Storage.getPublicPath('map_objects'))) {
const newMapObject = new MapObject() const newMapObject = new MapObject()
newMapObject newMapObject
.setId(object.split('.')[0] as UUID) .setId(mapObject.split('.')[0] as UUID)
.setName('New object') .setName('New map object')
.setFrameWidth( .setFrameWidth(
(await sharp(Storage.getPublicPath('objects', object)) (await sharp(Storage.getPublicPath('map_objects', mapObject))
.metadata() .metadata()
.then((metadata) => metadata.height)) ?? 0 .then((metadata) => metadata.height)) ?? 0
) )
.setFrameHeight( .setFrameHeight(
(await sharp(Storage.getPublicPath('objects', object)) (await sharp(Storage.getPublicPath('map_objects', mapObject))
.metadata() .metadata()
.then((metadata) => metadata.width)) ?? 0 .then((metadata) => metadata.width)) ?? 0
) )

View File

@ -26,10 +26,7 @@ export class CharacterType extends BaseEntity {
@Property() @Property()
isSelectable = false isSelectable = false
@OneToMany(() => Character, (character) => character.characterType) @ManyToOne({ nullable: true })
characters = new Collection<Character>(this)
@ManyToOne(() => Sprite, { nullable: true })
sprite?: Sprite sprite?: Sprite
@Property() @Property()
@ -109,13 +106,4 @@ export class CharacterType extends BaseEntity {
getUpdatedAt() { getUpdatedAt() {
return this.updatedAt return this.updatedAt
} }
setCharacters(characters: Collection<Character>) {
this.characters = characters
return this
}
getCharacters() {
return this.characters
}
} }

View File

@ -10,7 +10,7 @@ import { MapEventTileTeleport } from './mapEventTileTeleport'
import { BaseEntity } from '#application/base/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import { placedMapObject } from '#entities/placedMapObject' import { PlacedMapObject } from '#entities/placedMapObject'
@Entity() @Entity()
export class Map extends BaseEntity { export class Map extends BaseEntity {
@ -47,8 +47,8 @@ export class Map extends BaseEntity {
@OneToMany(() => MapEventTileTeleport, (teleport) => teleport.toMap) @OneToMany(() => MapEventTileTeleport, (teleport) => teleport.toMap)
mapEventTileTeleports = new Collection<MapEventTileTeleport>(this) mapEventTileTeleports = new Collection<MapEventTileTeleport>(this)
@OneToMany(() => placedMapObject, (object) => object.map) @OneToMany(() => PlacedMapObject, (object) => object.map)
placedMapObjects = new Collection<placedMapObject>(this) placedMapObjects = new Collection<PlacedMapObject>(this)
@OneToMany(() => Character, (character) => character.map) @OneToMany(() => Character, (character) => character.map)
characters = new Collection<Character>(this) characters = new Collection<Character>(this)
@ -155,7 +155,7 @@ export class Map extends BaseEntity {
return this.mapEventTileTeleports return this.mapEventTileTeleports
} }
setPlacedMapObjects(placedMapObjects: Collection<placedMapObject>) { setPlacedMapObjects(placedMapObjects: Collection<PlacedMapObject>) {
this.placedMapObjects = placedMapObjects this.placedMapObjects = placedMapObjects
return this return this
} }

View File

@ -16,10 +16,10 @@ export class MapObject extends BaseEntity {
@Property({ type: 'json', nullable: true }) @Property({ type: 'json', nullable: true })
tags?: any tags?: any
@Property() @Property({ type: 'decimal', precision: 10, scale: 2 })
originX = 0 originX = 0
@Property() @Property({ type: 'decimal', precision: 10, scale: 2 })
originY = 0 originY = 0
@Property() @Property()

View File

@ -10,7 +10,7 @@ import { MapObject } from '#entities/mapObject'
//@TODO : Rename mapObject //@TODO : Rename mapObject
@Entity() @Entity()
export class placedMapObject extends BaseEntity { export class PlacedMapObject extends BaseEntity {
@PrimaryKey() @PrimaryKey()
id = randomUUID() id = randomUUID()

View File

@ -11,8 +11,13 @@ export default class characterHairListEvent extends BaseEvent {
} }
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
const items: CharacterHair[] = await characterHairRepository.getAllSelectable() try {
await Database.getEntityManager().populate(items, ['sprite']) const items: CharacterHair[] = await characterHairRepository.getAllSelectable()
callback(items) await Database.getEntityManager().populate(items, ['sprite'])
return callback(items)
} catch (error) {
this.logger.error('character:hair:list error', error)
return callback([])
}
} }
} }

View File

@ -15,30 +15,14 @@ export default class CharacterConnectEvent extends BaseEvent {
this.socket.on('character:connect', this.handleEvent.bind(this)) this.socket.on('character:connect', this.handleEvent.bind(this))
} }
/**
* Handle character connect event
* @TODO:
* 1. Check if character is already connected
* 2. Update character hair if provided
* 3. Emit character connect event
* 4. Let other clients know of new character
* @param data
* @param callback
* @private
*/
private async handleEvent(data: CharacterConnectPayload, callback: (response: any) => void): Promise<void> { private async handleEvent(data: CharacterConnectPayload, callback: (response: any) => void): Promise<void> {
if (!this.socket.userId) {
this.emitError('User not authenticated')
return
}
try { try {
if (await this.checkForActiveCharacters()) { if (await this.checkForActiveCharacters()) {
this.emitError('You are already connected to another character') this.emitError('You are already connected to another character')
return return
} }
const character = await CharacterRepository.getByUserAndId(this.socket.userId, data.characterId) const character = await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId)
if (!character) { if (!character) {
this.emitError('Character not found or does not belong to this user') this.emitError('Character not found or does not belong to this user')
@ -57,8 +41,8 @@ export default class CharacterConnectEvent extends BaseEvent {
// Emit character connect event // Emit character connect event
callback({ character }) callback({ character })
// wait 300 ms, @TODO: Find a better way to do this // wait 300 ms, @TODO: Find a better way to do this, race condition
await new Promise((resolve) => setTimeout(resolve, 100)) await new Promise((resolve) => setTimeout(resolve, 500))
await TeleportService.teleportCharacter(character.id, { await TeleportService.teleportCharacter(character.id, {
targetMapId: character.map.id, targetMapId: character.map.id,

View File

@ -9,7 +9,6 @@ type TypePayload = {
} }
type TypeResponse = { type TypeResponse = {
map: Map
characters: Character[] characters: Character[]
} }
@ -20,11 +19,7 @@ export default class CharacterDeleteEvent extends BaseEvent {
private async handleEvent(data: TypePayload, callback: (response: TypeResponse) => void): Promise<any> { private async handleEvent(data: TypePayload, callback: (response: TypeResponse) => void): Promise<any> {
try { try {
const character = await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId) await (await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId))?.delete()
if (character) {
await character.delete()
}
const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!) const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!)
this.socket.emit('character:list', characters) this.socket.emit('character:list', characters)

View File

@ -1,5 +1,4 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import Database from '#application/database'
import { Character } from '#entities/character' import { Character } from '#entities/character'
import CharacterRepository from '#repositories/characterRepository' import CharacterRepository from '#repositories/characterRepository'
@ -10,9 +9,7 @@ export default class CharacterListEvent extends BaseEvent {
private async handleEvent(data: any): Promise<void> { private async handleEvent(data: any): Promise<void> {
try { try {
const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!) let characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!, ['characterType', 'characterHair'])
await Database.getEntityManager().populate(characters, ['characterType', 'characterHair'])
this.socket.emit('character:list', characters) this.socket.emit('character:list', characters)
} catch (error: any) { } catch (error: any) {
this.logger.error('character:list error', error.message) this.logger.error('character:list error', error.message)

View File

@ -1,9 +1,9 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import CharacterHairRepository from '#repositories/characterHairRepository' import CharacterHairRepository from '#repositories/characterHairRepository'
import characterRepository from '#repositories/characterRepository' import { UUID } from '#application/types'
interface IPayload { interface IPayload {
id: number id: UUID
} }
export default class characterHairDeleteEvent extends BaseEvent { export default class characterHairDeleteEvent extends BaseEvent {
@ -12,20 +12,13 @@ export default class characterHairDeleteEvent extends BaseEvent {
} }
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
try { try {
const characterHair = await CharacterHairRepository.getById(data.id) if (!(await this.isCharacterGM())) return
if (characterHair) {
await characterHair.delete()
}
callback(true) const characterHair = await CharacterHairRepository.getById(data.id)
await (await CharacterHairRepository.getById(data.id))?.delete()
return callback(true)
} catch (error) { } catch (error) {
this.logger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`) this.logger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
callback(false) callback(false)

View File

@ -1,7 +1,6 @@
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import { CharacterHair } from '#entities/characterHair' import { CharacterHair } from '#entities/characterHair'
import characterHairRepository from '#repositories/characterHairRepository' import characterHairRepository from '#repositories/characterHairRepository'
import characterRepository from '#repositories/characterRepository'
interface IPayload {} interface IPayload {}
@ -11,19 +10,14 @@ export default class characterHairListEvent extends BaseEvent {
} }
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number) try {
if (!character) { if (!(await this.isCharacterGM())) return
this.logger.error('gm:characterHair:list error', 'Character not found')
const items = await characterHairRepository.getAll()
return callback(items)
} catch (error) {
this.logger.error('gm:characterHair:list error', error)
return callback([]) return callback([])
} }
if (character.role !== 'gm') {
this.logger.info(`User ${character.id} tried to list character hair but is not a game master.`)
return callback([])
}
// get all objects
const items = await characterHairRepository.getAll()
callback(items)
} }
} }

View File

@ -6,7 +6,7 @@ import characterRepository from '#repositories/characterRepository'
import SpriteRepository from '#repositories/spriteRepository' import SpriteRepository from '#repositories/spriteRepository'
type Payload = { type Payload = {
id: number id: UUID
name: string name: string
gender: CharacterGender gender: CharacterGender
isSelectable: boolean isSelectable: boolean
@ -19,21 +19,17 @@ export default class CharacterHairUpdateEvent extends BaseEvent {
} }
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> { private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
try { try {
if (!(await this.isCharacterGM())) return
const sprite = await SpriteRepository.getById(data.spriteId) const sprite = await SpriteRepository.getById(data.spriteId)
const characterHair = await CharacterHairRepository.getById(data.id) const characterHair = await CharacterHairRepository.getById(data.id)
if (characterHair) { if (!characterHair) {
await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite!).update() return callback(false)
} }
await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite!).update()
return callback(true) return callback(true)
} catch (error) { } catch (error) {
this.logger.error(`Error updating character hair: ${error instanceof Error ? error.message : String(error)}`) this.logger.error(`Error updating character hair: ${error instanceof Error ? error.message : String(error)}`)

View File

@ -1,41 +1,22 @@
import { CharacterGender, CharacterRace } from '@prisma/client' import { BaseEvent } from '#application/base/baseEvent'
import { Server } from 'socket.io' import { CharacterType } from '#entities/characterType'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
export default class CharacterTypeCreateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
export default class CharacterTypeCreateEvent extends BaseEvent {
public listen(): void { public listen(): void {
this.socket.on('gm:characterType:create', this.handleEvent.bind(this)) this.socket.on('gm:characterType:create', this.handleEvent.bind(this))
} }
private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> { private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {
try { try {
const character = await characterRepository.getById(this.socket.characterId as number) if (!(await this.isCharacterGM())) return
if (!character) return callback(false)
if (character.role !== 'gm') { const newCharacterType = new CharacterType()
return callback(false) await newCharacterType.setName('New character type').save()
}
const newCharacterType = await prisma.characterType.create({ return callback(true, newCharacterType)
data: {
name: 'New character type',
gender: CharacterGender.MALE,
race: CharacterRace.HUMAN
}
})
callback(true, newCharacterType)
} catch (error) { } catch (error) {
console.error('Error creating character type:', error) console.error('Error creating character type:', error)
callback(false) return callback(false)
} }
} }
} }

View File

@ -1,41 +1,28 @@
import { Server } from 'socket.io' import { UUID } from '#application/types'
import { gameMasterLogger } from '#application/logger'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository' import CharacterTypeRepository from '#repositories/characterTypeRepository'
import { BaseEvent } from '#application/base/baseEvent'
interface IPayload { interface IPayload {
id: number id: UUID
} }
export default class CharacterTypeDeleteEvent { export default class CharacterTypeDeleteEvent extends BaseEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:characterType:remove', this.handleEvent.bind(this)) this.socket.on('gm:characterType:remove', this.handleEvent.bind(this))
} }
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId!)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
try { try {
if (!(await this.isCharacterGM())) return
const characterType = await CharacterTypeRepository.getById(data.id) const characterType = await CharacterTypeRepository.getById(data.id)
if (!characterType) return callback(false) if (!characterType) return callback(false)
await characterType.delete() await characterType.delete()
callback(true) return callback(true)
} catch (error) { } catch (error) {
gameMasterLogger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`) this.logger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`)
callback(false) return callback(false)
} }
} }
} }

View File

@ -1,37 +1,23 @@
import { CharacterType } from '@prisma/client'
import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository' import CharacterTypeRepository from '#repositories/characterTypeRepository'
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterType } from '#entities/characterType'
interface IPayload {} interface IPayload {}
export default class CharacterTypeListEvent { export default class CharacterTypeListEvent extends BaseEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void { public listen(): void {
this.socket.on('gm:characterType:list', this.handleEvent.bind(this)) this.socket.on('gm:characterType:list', this.handleEvent.bind(this))
} }
private async handleEvent(data: IPayload, callback: (response: CharacterType[]) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: CharacterType[]) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number) try {
if (!character) { if (!(await this.isCharacterGM())) return
gameMasterLogger.error('gm:characterType:list error', 'Character not found')
const items = await CharacterTypeRepository.getAll()
return callback(items)
} catch (error) {
this.logger.error('gm:characterType:list error', error)
return callback([]) return callback([])
} }
if (character.role !== 'gm') {
gameMasterLogger.info(`User ${character.id} tried to list character types but is not a game master.`)
return callback([])
}
// get all objects
const items = await CharacterTypeRepository.getAll()
callback(items)
} }
} }

View File

@ -1,14 +1,15 @@
import ObjectRepository from '#repositories/mapObjectRepository' import ObjectRepository from '#repositories/mapObjectRepository'
import { BaseEvent } from '#application/base/baseEvent' import { BaseEvent } from '#application/base/baseEvent'
import { MapObject } from '#entities/mapObject'
interface IPayload {} interface IPayload {}
export default class ObjectListEvent extends BaseEvent{ export default class MapObjectListEvent extends BaseEvent {
public listen(): void { public listen(): void {
this.socket.on('gm:object:list', this.handleEvent.bind(this)) this.socket.on('gm:mapObject:list', this.handleEvent.bind(this))
} }
private async handleEvent(data: IPayload, callback: (response: Object[]) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: MapObject[]) => void): Promise<void> {
if (!(await this.isCharacterGM())) return if (!(await this.isCharacterGM())) return
// get all objects // get all objects

View File

@ -0,0 +1,38 @@
import fs from 'fs'
import Storage from '#application/storage'
import { BaseEvent } from '#application/base/baseEvent'
import MapObjectRepository from '#repositories/mapObjectRepository'
import { UUID } from '#application/types'
interface IPayload {
mapObjectId: UUID
}
export default class MapObjectRemoveEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:mapObject:remove', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
if (!(await this.isCharacterGM())) return
try {
// remove the tile from the disk
const finalFilePath = Storage.getPublicPath('map_objects', data.mapObjectId + '.png')
fs.unlink(finalFilePath, async (err) => {
if (err) {
this.logger.error(`Error deleting object ${data.mapObjectId}: ${err.message}`)
callback(false)
return
}
await (await MapObjectRepository.getById(data.mapObjectId))?.delete()
return callback(true)
})
} catch (error) {
this.logger.error(`Error deleting object ${data.mapObjectId}: ${error instanceof Error ? error.message : String(error)}`)
return callback(false)
}
}
}

View File

@ -0,0 +1,46 @@
import { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
import MapObjectRepository from '#repositories/mapObjectRepository'
type Payload = {
id: UUID
name: string
tags: string[]
originX: number
originY: number
isAnimated: boolean
frameRate: number
frameWidth: number
frameHeight: number
}
export default class MapObjectUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:mapObject:update', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
const mapObject = await MapObjectRepository.getById(data.id)
if (!mapObject) return callback(false)
await mapObject
.setName(data.name)
.setTags(data.tags)
.setOriginX(data.originX)
.setOriginY(data.originY)
.setIsAnimated(data.isAnimated)
.setFrameRate(data.frameRate)
.setFrameWidth(data.frameWidth)
.setFrameHeight(data.frameHeight)
.update()
return callback(true)
} catch (error) {
console.error(error)
return callback(false)
}
}
}

View File

@ -0,0 +1,53 @@
import fs from 'fs/promises'
import { writeFile } from 'node:fs/promises'
import sharp from 'sharp'
import Storage from '#application/storage'
import { BaseEvent } from '#application/base/baseEvent'
import { MapObject } from '#entities/mapObject'
interface IObjectData {
[key: string]: Buffer
}
export default class MapObjectUploadEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:mapObject:upload', this.handleEvent.bind(this))
}
private async handleEvent(data: IObjectData, callback: (response: boolean) => void): Promise<void> {
try {
if (!(await this.isCharacterGM())) return
const public_folder = Storage.getPublicPath('map_objects')
// Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true })
const uploadPromises = Object.entries(data).map(async ([key, objectData]) => {
// Get image dimensions
const metadata = await sharp(objectData).metadata()
const width = metadata.width || 0
const height = metadata.height || 0
// Create new map object and save it to database
const mapObject = new MapObject()
await mapObject.setName(key).setTags([]).setOriginX(0).setOriginY(0).setFrameWidth(width).setFrameHeight(height).save()
// Save image to disk
const uuid = mapObject.getId()
const filename = `${uuid}.png`
const finalFilePath = Storage.getPublicPath('map_objects', filename)
await writeFile(finalFilePath, objectData)
this.logger.info('gm:mapObject:upload', `Object ${key} uploaded with id ${uuid}`)
})
await Promise.all(uploadPromises)
return callback(true)
} catch (error: any) {
this.logger.error('gm:mapObject:upload error', error.message)
return callback(false)
}
}
}

View File

@ -1,59 +0,0 @@
import fs from 'fs'
import { Server } from 'socket.io'
import { gameLogger, gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import Storage from '#application/storage'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
interface IPayload {
object: string
}
export default class ObjectRemoveEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:object:remove', this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
try {
await prisma.object.delete({
where: {
id: data.object
}
})
// get root path
const public_folder = Storage.getPublicPath('objects')
// remove the tile from the disk
const finalFilePath = Storage.getPublicPath('objects', data.object + '.png')
fs.unlink(finalFilePath, (err) => {
if (err) {
gameMasterLogger.error(`Error deleting object ${data.object}: ${err.message}`)
callback(false)
return
}
callback(true)
})
} catch (error) {
gameLogger.error(`Error deleting object ${data.object}: ${error instanceof Error ? error.message : String(error)}`)
callback(false)
}
}
}

View File

@ -1,59 +0,0 @@
import { Server } from 'socket.io'
import prisma from '#application/prisma'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
type Payload = {
id: string
name: string
tags: string[]
originX: number
originY: number
isAnimated: boolean
frameRate: number
frameWidth: number
frameHeight: number
}
export default class ObjectUpdateEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:object:update', this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
try {
const object = await prisma.object.update({
where: {
id: data.id
},
data: {
name: data.name,
tags: data.tags,
originX: data.originX,
originY: data.originY,
isAnimated: data.isAnimated,
frameRate: data.frameRate,
frameWidth: data.frameWidth,
frameHeight: data.frameHeight
}
})
callback(true)
} catch (error) {
console.error(error)
callback(false)
}
}
}

View File

@ -1,73 +0,0 @@
import fs from 'fs/promises'
import { writeFile } from 'node:fs/promises'
import sharp from 'sharp'
import { Server } from 'socket.io'
import { gameMasterLogger } from '#application/logger'
import prisma from '#application/prisma'
import Storage from '#application/storage'
import { TSocket } from '#application/types'
import characterRepository from '#repositories/characterRepository'
interface IObjectData {
[key: string]: Buffer
}
export default class ObjectUploadEvent {
constructor(
private readonly io: Server,
private readonly socket: TSocket
) {}
public listen(): void {
this.socket.on('gm:object:upload', this.handleEvent.bind(this))
}
private async handleEvent(data: IObjectData, callback: (response: boolean) => void): Promise<void> {
try {
const character = await characterRepository.getById(this.socket.characterId as number)
if (!character) return callback(false)
if (character.role !== 'gm') {
return callback(false)
}
const public_folder = Storage.getPublicPath('objects')
// Ensure the folder exists
await fs.mkdir(public_folder, { recursive: true })
const uploadPromises = Object.entries(data).map(async ([key, objectData]) => {
// Get image dimensions
const metadata = await sharp(objectData).metadata()
const width = metadata.width || 0
const height = metadata.height || 0
const object = await prisma.object.create({
data: {
name: key,
tags: [],
originX: 0,
originY: 0,
frameWidth: width,
frameHeight: height
}
})
const uuid = object.id
const filename = `${uuid}.png`
const finalFilePath = Storage.getPublicPath('objects', filename)
await writeFile(finalFilePath, objectData)
gameMasterLogger.info('gm:object:upload', `Object ${key} uploaded with id ${uuid}`)
})
await Promise.all(uploadPromises)
callback(true)
} catch (error: any) {
gameMasterLogger.error('gm:object:upload error', error.message)
callback(false)
}
}
}

View File

@ -10,7 +10,7 @@ type Payload = {
export default class MapCreateEvent extends BaseEvent { export default class MapCreateEvent extends BaseEvent {
public listen(): void { public listen(): void {
this.socket.on('gm:map_editor:map:create', this.handleEvent.bind(this)) this.socket.on('gm:map:create', this.handleEvent.bind(this))
} }
private async handleEvent(data: Payload, callback: (response: Map[]) => void): Promise<void> { private async handleEvent(data: Payload, callback: (response: Map[]) => void): Promise<void> {
@ -30,7 +30,7 @@ export default class MapCreateEvent extends BaseEvent {
const mapList = await MapRepository.getAll() const mapList = await MapRepository.getAll()
return callback(mapList) return callback(mapList)
} catch (error: any) { } catch (error: any) {
this.logger.error('gm:map_editor:map:create error', error.message) this.logger.error('gm:map:create error', error.message)
this.socket.emit('notification', { message: 'Failed to create map.' }) this.socket.emit('notification', { message: 'Failed to create map.' })
return callback([]) return callback([])
} }

View File

@ -8,7 +8,7 @@ type Payload = {
export default class MapDeleteEvent extends BaseEvent { export default class MapDeleteEvent extends BaseEvent {
public listen(): void { public listen(): void {
this.socket.on('gm:map_editor:map:delete', this.handleEvent.bind(this)) this.socket.on('gm:map:delete', this.handleEvent.bind(this))
} }
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> { private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {
@ -22,7 +22,7 @@ export default class MapDeleteEvent extends BaseEvent {
this.logger.info(`Map ${data.mapId} deleted successfully.`) this.logger.info(`Map ${data.mapId} deleted successfully.`)
return callback(true) return callback(true)
} catch (error: unknown) { } catch (error: unknown) {
this.logger.error('gm:map_editor:map:delete error', error) this.logger.error('gm:map:delete error', error)
return callback(false) return callback(false)
} }
} }

View File

@ -6,7 +6,7 @@ interface IPayload {}
export default class MapListEvent extends BaseEvent { export default class MapListEvent extends BaseEvent {
public listen(): void { public listen(): void {
this.socket.on('gm:map_editor:map:list', this.handleEvent.bind(this)) this.socket.on('gm:map:list', this.handleEvent.bind(this))
} }
private async handleEvent(data: IPayload, callback: (response: Map[]) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: Map[]) => void): Promise<void> {
@ -18,7 +18,7 @@ export default class MapListEvent extends BaseEvent {
const maps = await MapRepository.getAll() const maps = await MapRepository.getAll()
return callback(maps) return callback(maps)
} catch (error: any) { } catch (error: any) {
this.logger.error('gm:map_editor:map:list error', error.message) this.logger.error('gm:map:list error', error.message)
return callback([]) return callback([])
} }
} }

View File

@ -2,6 +2,7 @@ import { BaseEvent } from '#application/base/baseEvent'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import { Map } from '#entities/map' import { Map } from '#entities/map'
import MapRepository from '#repositories/mapRepository' import MapRepository from '#repositories/mapRepository'
import Database from '#application/database'
interface IPayload { interface IPayload {
mapId: UUID mapId: UUID
@ -9,7 +10,7 @@ interface IPayload {
export default class MapRequestEvent extends BaseEvent { export default class MapRequestEvent extends BaseEvent {
public listen(): void { public listen(): void {
this.socket.on('gm:map_editor:map:request', this.handleEvent.bind(this)) this.socket.on('gm:map:request', this.handleEvent.bind(this))
} }
private async handleEvent(data: IPayload, callback: (response: Map | null) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: Map | null) => void): Promise<void> {
@ -25,14 +26,18 @@ export default class MapRequestEvent extends BaseEvent {
const map = await MapRepository.getById(data.mapId) const map = await MapRepository.getById(data.mapId)
if (!map) { if (!map) {
this.logger.info(`User ${(await this.getCharacter())!.getId()} tried to request map ${data.mapId} but it does not exist.`) this.logger.info(`User ${(await this.getCharacter())!.getId()} tried to request map ${data.mapId} but it does not exist.`)
return callback(null) return callback(null)
} }
console.log(map)
await Database.getEntityManager().populate(map, ['mapEventTiles', 'placedMapObjects'])
return callback(map) return callback(map)
} catch (error: any) { } catch (error: any) {
this.logger.error('gm:map_editor:map:request error', error.message) this.logger.error('gm:map:request error', error.message)
return callback(null) return callback(null)
} }
} }

View File

@ -5,9 +5,9 @@ import { Map } from '#entities/map'
import { MapEffect } from '#entities/mapEffect' import { MapEffect } from '#entities/mapEffect'
import { MapEventTile } from '#entities/mapEventTile' import { MapEventTile } from '#entities/mapEventTile'
import { MapEventTileTeleport } from '#entities/mapEventTileTeleport' import { MapEventTileTeleport } from '#entities/mapEventTileTeleport'
import { MapObject } from '#entities/mapObject'
import mapManager from '#managers/mapManager' import mapManager from '#managers/mapManager'
import MapRepository from '#repositories/mapRepository' import MapRepository from '#repositories/mapRepository'
import { PlacedMapObject } from '#entities/placedMapObject'
interface IPayload { interface IPayload {
mapId: UUID mapId: UUID
@ -31,12 +31,12 @@ interface IPayload {
effect: string effect: string
strength: number strength: number
}[] }[]
mapObjects: MapObject[] placedMapObjects: PlacedMapObject[]
} }
export default class MapUpdateEvent extends BaseEvent { export default class MapUpdateEvent extends BaseEvent {
public listen(): void { public listen(): void {
this.socket.on('gm:map_editor:map:update', this.handleEvent.bind(this)) this.socket.on('gm:map:update', this.handleEvent.bind(this))
} }
private async handleEvent(data: IPayload, callback: (response: Map | null) => void): Promise<void> { private async handleEvent(data: IPayload, callback: (response: Map | null) => void): Promise<void> {
@ -70,11 +70,11 @@ export default class MapUpdateEvent extends BaseEvent {
data.mapEventTiles = data.mapEventTiles.filter((tile) => tile.positionX >= 0 && tile.positionX < data.width && tile.positionY >= 0 && tile.positionY < data.height) data.mapEventTiles = data.mapEventTiles.filter((tile) => tile.positionX >= 0 && tile.positionX < data.width && tile.positionY >= 0 && tile.positionY < data.height)
data.mapObjects = data.mapObjects.filter((obj) => obj.positionX >= 0 && obj.positionX < data.width && obj.positionY >= 0 && obj.positionY < data.height) data.placedMapObjects = data.placedMapObjects.filter((obj) => obj.positionX >= 0 && obj.positionX < data.width && obj.positionY >= 0 && obj.positionY < data.height)
// Clear existing collections // Clear existing collections
map.mapEventTiles.removeAll() map.mapEventTiles.removeAll()
map.mapObjects.removeAll() map.placedMapObjects.removeAll()
map.mapEffects.removeAll() map.mapEffects.removeAll()
// Create and add new map event tiles // Create and add new map event tiles
@ -95,16 +95,14 @@ export default class MapUpdateEvent extends BaseEvent {
} }
// Create and add new map objects // Create and add new map objects
for (const object of data.mapObjects) { for (const object of data.placedMapObjects) {
const mapObject = new MapObject().setMapObject(object.mapObject).setDepth(object.depth).setIsRotated(object.isRotated).setPositionX(object.positionX).setPositionY(object.positionY).setMap(map) const mapObject = new PlacedMapObject().setMapObject(object.mapObject).setDepth(object.depth).setIsRotated(object.isRotated).setPositionX(object.positionX).setPositionY(object.positionY).setMap(map)
map.placedMapObjects.add(mapObject)
map.mapObjects.add(mapObject)
} }
// Create and add new map effects // Create and add new map effects
for (const effect of data.mapEffects) { for (const effect of data.mapEffects) {
const mapEffect = new MapEffect().setEffect(effect.effect).setStrength(effect.strength).setMap(map) const mapEffect = new MapEffect().setEffect(effect.effect).setStrength(effect.strength).setMap(map)
map.mapEffects.add(mapEffect) map.mapEffects.add(mapEffect)
} }
@ -125,7 +123,7 @@ export default class MapUpdateEvent extends BaseEvent {
return callback(map) return callback(map)
} catch (error: any) { } catch (error: any) {
this.logger.error(`gm:map_editor:map:update error: ${error instanceof Error ? error.message : String(error)}`) this.logger.error(`gm:mapObject:update error: ${error instanceof Error ? error.message : String(error)}`)
return callback(null) return callback(null)
} }
} }

View File

@ -74,17 +74,17 @@ export class AssetsController extends BaseController {
await Database.getEntityManager().populate(sprite, ['spriteActions']) await Database.getEntityManager().populate(sprite, ['spriteActions'])
const assets: AssetData[] = sprite.spriteActions.getItems().map((spriteAction) => ({ const assets: AssetData[] = sprite.spriteActions.getItems().map((spriteAction) => ({
key: sprite.id + '-' + spriteAction.action, key: sprite.getId() + '-' + spriteAction.getAction(),
data: '/assets/sprites/' + sprite.getId() + '/' + spriteAction.getAction() + '.png', data: '/assets/sprites/' + sprite.getId() + '/' + spriteAction.getAction() + '.png',
group: spriteAction.isAnimated ? 'sprite_animations' : 'sprites', group: spriteAction.getIsAnimated() ? 'sprite_animations' : 'sprites',
updatedAt: sprite.getUpdatedAt(), updatedAt: sprite.getUpdatedAt(),
originX: Number(spriteAction.originX.toString()), originX: Number(spriteAction.getOriginX().toString()),
originY: Number(spriteAction.originY.toString()), originY: Number(spriteAction.getOriginY().toString()),
isAnimated: spriteAction.getIsAnimated(), isAnimated: spriteAction.getIsAnimated(),
frameRate: spriteAction.getFrameRate(), frameRate: spriteAction.getFrameRate(),
frameWidth: spriteAction.getFrameWidth(), frameWidth: spriteAction.getFrameWidth(),
frameHeight: spriteAction.getFrameHeight(), frameHeight: spriteAction.getFrameHeight(),
frameCount: JSON.parse(JSON.stringify(spriteAction.getSprites())).length frameCount: spriteAction.getSprites()?.length
})) }))
return this.sendSuccess(res, assets) return this.sendSuccess(res, assets)

View File

@ -1,12 +1,13 @@
import { BaseRepository } from '#application/base/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import { Character } from '#entities/character' import { Character } from '#entities/character'
import { LoadHint, Populate } from '@mikro-orm/core'
class CharacterRepository extends BaseRepository { class CharacterRepository extends BaseRepository {
async getByUserId(userId: UUID): Promise<Character[]> { async getByUserId(userId: UUID, populate?: LoadHint<Character, '*'>): Promise<Character[]> {
try { try {
const repository = this.em.getRepository(Character) const repository = this.em.getRepository(Character)
return await repository.find({ user: userId }) return await repository.find({ user: userId }, { populate: populate as Populate<Character> })
} catch (error: any) { } catch (error: any) {
this.logger.error(`Failed to get character by user ID: ${error instanceof Error ? error.message : String(error)}`) this.logger.error(`Failed to get character by user ID: ${error instanceof Error ? error.message : String(error)}`)
return [] return []

View File

@ -19,7 +19,7 @@ class CharacterTypeRepository extends BaseRepository {
return await repository.findAll() return await repository.findAll()
} catch (error: any) { } catch (error: any) {
this.logger.error(`Failed to get all character types: ${error instanceof Error ? error.message : String(error)}`) this.logger.error(`Failed to get all character types: ${error instanceof Error ? error.message : String(error)}`)
return null return []
} }
} }

View File

@ -1,22 +1,23 @@
import { BaseRepository } from '#application/base/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
import { UUID } from '#application/types' import { UUID } from '#application/types'
import { MapObject } from '#entities/mapObject'
class MapObjectRepository extends BaseRepository { class MapObjectRepository extends BaseRepository {
async getById(id: UUID): Promise<any> { async getById(id: UUID): Promise<MapObject | null> {
try { try {
const repository = this.em.getRepository(Object) const repository = this.em.getRepository(MapObject)
return await repository.findOne({ id }) return await repository.findOne({ id })
} catch (error: any) { } catch (error: any) {
return null return null
} }
} }
async getAll(): Promise<any> { async getAll(): Promise<MapObject[]> {
try { try {
const repository = this.em.getRepository(Object) const repository = this.em.getRepository(MapObject)
return await repository.findAll() return await repository.findAll()
} catch (error: any) { } catch (error: any) {
return null return []
} }
} }
} }