diff --git a/migrations/Migration20250102162954.ts b/migrations/Migration20250103003053.ts similarity index 96% rename from migrations/Migration20250102162954.ts rename to migrations/Migration20250103003053.ts index 99e3bf0..b555b9e 100644 --- a/migrations/Migration20250102162954.ts +++ b/migrations/Migration20250103003053.ts @@ -1,6 +1,6 @@ import { Migration } from '@mikro-orm/migrations'; -export class Migration20250102162954 extends Migration { +export class Migration20250103003053 extends Migration { override async up(): Promise { 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 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(`alter table \`placed_map_object\` add index \`placed_map_object_map_id_index\`(\`map_id\`);`); diff --git a/mikro-orm.config.ts b/mikro-orm.config.ts index 9f5ded4..5bc1e12 100644 --- a/mikro-orm.config.ts +++ b/mikro-orm.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ password: serverConfig.DB_PASS, dbName: serverConfig.DB_NAME, debug: serverConfig.ENV !== 'production', + // allowGlobalContext: true, driverOptions: { allowPublicKeyRetrieval: true }, diff --git a/public/assets.zip b/public/assets.zip index 0cdeddf..c6c406b 100644 Binary files a/public/assets.zip and b/public/assets.zip differ diff --git a/src/application/database.ts b/src/application/database.ts index 77994c2..536cbdf 100644 --- a/src/application/database.ts +++ b/src/application/database.ts @@ -6,13 +6,11 @@ import config from '../../mikro-orm.config' class Database { private static orm: MikroORM - private static em: EntityManager private static logger = Logger.type(LoggerType.APP) public static async initialize(): Promise { try { Database.orm = await MikroORM.init(config) - Database.em = Database.orm.em.fork() this.logger.info('Database connection initialized') } catch (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 { - if (!Database.em) { - throw new Error('Database not initialized. Call Database.initialize() first.') - } - return Database.em + return Database.orm.em.fork() } } diff --git a/src/commands/init.ts b/src/commands/init.ts index 0944155..1f414c7 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -27,7 +27,7 @@ export default class InitCommand extends BaseCommand { public async execute(): Promise { // Assets await this.importTiles() - await this.importObjects() + await this.importMapObjects() await this.createCharacterType() await this.createCharacterHair() // await this.createCharacterEquipment() @@ -51,19 +51,19 @@ export default class InitCommand extends BaseCommand { } } - private async importObjects(): Promise { - for (const object of fs.readdirSync(Storage.getPublicPath('objects'))) { + private async importMapObjects(): Promise { + for (const mapObject of fs.readdirSync(Storage.getPublicPath('map_objects'))) { const newMapObject = new MapObject() newMapObject - .setId(object.split('.')[0] as UUID) - .setName('New object') + .setId(mapObject.split('.')[0] as UUID) + .setName('New map object') .setFrameWidth( - (await sharp(Storage.getPublicPath('objects', object)) + (await sharp(Storage.getPublicPath('map_objects', mapObject)) .metadata() .then((metadata) => metadata.height)) ?? 0 ) .setFrameHeight( - (await sharp(Storage.getPublicPath('objects', object)) + (await sharp(Storage.getPublicPath('map_objects', mapObject)) .metadata() .then((metadata) => metadata.width)) ?? 0 ) diff --git a/src/entities/characterType.ts b/src/entities/characterType.ts index 160d02a..b260730 100644 --- a/src/entities/characterType.ts +++ b/src/entities/characterType.ts @@ -26,10 +26,7 @@ export class CharacterType extends BaseEntity { @Property() isSelectable = false - @OneToMany(() => Character, (character) => character.characterType) - characters = new Collection(this) - - @ManyToOne(() => Sprite, { nullable: true }) + @ManyToOne({ nullable: true }) sprite?: Sprite @Property() @@ -109,13 +106,4 @@ export class CharacterType extends BaseEntity { getUpdatedAt() { return this.updatedAt } - - setCharacters(characters: Collection) { - this.characters = characters - return this - } - - getCharacters() { - return this.characters - } } diff --git a/src/entities/map.ts b/src/entities/map.ts index acb92f9..657711d 100644 --- a/src/entities/map.ts +++ b/src/entities/map.ts @@ -10,7 +10,7 @@ import { MapEventTileTeleport } from './mapEventTileTeleport' import { BaseEntity } from '#application/base/baseEntity' import { UUID } from '#application/types' -import { placedMapObject } from '#entities/placedMapObject' +import { PlacedMapObject } from '#entities/placedMapObject' @Entity() export class Map extends BaseEntity { @@ -47,8 +47,8 @@ export class Map extends BaseEntity { @OneToMany(() => MapEventTileTeleport, (teleport) => teleport.toMap) mapEventTileTeleports = new Collection(this) - @OneToMany(() => placedMapObject, (object) => object.map) - placedMapObjects = new Collection(this) + @OneToMany(() => PlacedMapObject, (object) => object.map) + placedMapObjects = new Collection(this) @OneToMany(() => Character, (character) => character.map) characters = new Collection(this) @@ -155,7 +155,7 @@ export class Map extends BaseEntity { return this.mapEventTileTeleports } - setPlacedMapObjects(placedMapObjects: Collection) { + setPlacedMapObjects(placedMapObjects: Collection) { this.placedMapObjects = placedMapObjects return this } diff --git a/src/entities/mapObject.ts b/src/entities/mapObject.ts index a59d8e6..3debba2 100644 --- a/src/entities/mapObject.ts +++ b/src/entities/mapObject.ts @@ -16,10 +16,10 @@ export class MapObject extends BaseEntity { @Property({ type: 'json', nullable: true }) tags?: any - @Property() + @Property({ type: 'decimal', precision: 10, scale: 2 }) originX = 0 - @Property() + @Property({ type: 'decimal', precision: 10, scale: 2 }) originY = 0 @Property() diff --git a/src/entities/placedMapObject.ts b/src/entities/placedMapObject.ts index 2c929f4..72e7ed8 100644 --- a/src/entities/placedMapObject.ts +++ b/src/entities/placedMapObject.ts @@ -10,7 +10,7 @@ import { MapObject } from '#entities/mapObject' //@TODO : Rename mapObject @Entity() -export class placedMapObject extends BaseEntity { +export class PlacedMapObject extends BaseEntity { @PrimaryKey() id = randomUUID() diff --git a/src/events/character/charactersScreen/characterHairList.ts b/src/events/character/charactersScreen/characterHairList.ts index 47e7e49..c1dacc5 100644 --- a/src/events/character/charactersScreen/characterHairList.ts +++ b/src/events/character/charactersScreen/characterHairList.ts @@ -11,8 +11,13 @@ export default class characterHairListEvent extends BaseEvent { } private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise { - const items: CharacterHair[] = await characterHairRepository.getAllSelectable() - await Database.getEntityManager().populate(items, ['sprite']) - callback(items) + try { + const items: CharacterHair[] = await characterHairRepository.getAllSelectable() + await Database.getEntityManager().populate(items, ['sprite']) + return callback(items) + } catch (error) { + this.logger.error('character:hair:list error', error) + return callback([]) + } } } diff --git a/src/events/character/connect.ts b/src/events/character/connect.ts index d4e421e..bff78d1 100644 --- a/src/events/character/connect.ts +++ b/src/events/character/connect.ts @@ -15,30 +15,14 @@ export default class CharacterConnectEvent extends BaseEvent { 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 { - if (!this.socket.userId) { - this.emitError('User not authenticated') - return - } - try { if (await this.checkForActiveCharacters()) { this.emitError('You are already connected to another character') return } - const character = await CharacterRepository.getByUserAndId(this.socket.userId, data.characterId) + const character = await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId) if (!character) { 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 callback({ character }) - // wait 300 ms, @TODO: Find a better way to do this - await new Promise((resolve) => setTimeout(resolve, 100)) + // wait 300 ms, @TODO: Find a better way to do this, race condition + await new Promise((resolve) => setTimeout(resolve, 500)) await TeleportService.teleportCharacter(character.id, { targetMapId: character.map.id, diff --git a/src/events/character/delete.ts b/src/events/character/delete.ts index ccb0ac0..a077b5a 100644 --- a/src/events/character/delete.ts +++ b/src/events/character/delete.ts @@ -9,7 +9,6 @@ type TypePayload = { } type TypeResponse = { - map: Map characters: Character[] } @@ -20,11 +19,7 @@ export default class CharacterDeleteEvent extends BaseEvent { private async handleEvent(data: TypePayload, callback: (response: TypeResponse) => void): Promise { try { - const character = await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId) - if (character) { - await character.delete() - } - + await (await CharacterRepository.getByUserAndId(this.socket.userId!, data.characterId))?.delete() const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!) this.socket.emit('character:list', characters) diff --git a/src/events/character/list.ts b/src/events/character/list.ts index d7836be..01e4a6c 100644 --- a/src/events/character/list.ts +++ b/src/events/character/list.ts @@ -1,5 +1,4 @@ import { BaseEvent } from '#application/base/baseEvent' -import Database from '#application/database' import { Character } from '#entities/character' import CharacterRepository from '#repositories/characterRepository' @@ -10,9 +9,7 @@ export default class CharacterListEvent extends BaseEvent { private async handleEvent(data: any): Promise { try { - const characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!) - await Database.getEntityManager().populate(characters, ['characterType', 'characterHair']) - + let characters: Character[] = await CharacterRepository.getByUserId(this.socket.userId!, ['characterType', 'characterHair']) this.socket.emit('character:list', characters) } catch (error: any) { this.logger.error('character:list error', error.message) diff --git a/src/events/gameMaster/assetManager/characterHair/delete.ts b/src/events/gameMaster/assetManager/characterHair/delete.ts index 0f80778..f4b91c8 100644 --- a/src/events/gameMaster/assetManager/characterHair/delete.ts +++ b/src/events/gameMaster/assetManager/characterHair/delete.ts @@ -1,9 +1,9 @@ import { BaseEvent } from '#application/base/baseEvent' import CharacterHairRepository from '#repositories/characterHairRepository' -import characterRepository from '#repositories/characterRepository' +import { UUID } from '#application/types' interface IPayload { - id: number + id: UUID } 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 { - const character = await characterRepository.getById(this.socket.characterId as number) - if (!character) return callback(false) - - if (character.role !== 'gm') { - return callback(false) - } - try { - const characterHair = await CharacterHairRepository.getById(data.id) - if (characterHair) { - await characterHair.delete() - } + if (!(await this.isCharacterGM())) return - callback(true) + const characterHair = await CharacterHairRepository.getById(data.id) + await (await CharacterHairRepository.getById(data.id))?.delete() + + return callback(true) } catch (error) { this.logger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`) callback(false) diff --git a/src/events/gameMaster/assetManager/characterHair/list.ts b/src/events/gameMaster/assetManager/characterHair/list.ts index d6ea78d..9b111f8 100644 --- a/src/events/gameMaster/assetManager/characterHair/list.ts +++ b/src/events/gameMaster/assetManager/characterHair/list.ts @@ -1,7 +1,6 @@ import { BaseEvent } from '#application/base/baseEvent' import { CharacterHair } from '#entities/characterHair' import characterHairRepository from '#repositories/characterHairRepository' -import characterRepository from '#repositories/characterRepository' interface IPayload {} @@ -11,19 +10,14 @@ export default class characterHairListEvent extends BaseEvent { } private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise { - const character = await characterRepository.getById(this.socket.characterId as number) - if (!character) { - this.logger.error('gm:characterHair:list error', 'Character not found') + try { + if (!(await this.isCharacterGM())) return + + const items = await characterHairRepository.getAll() + return callback(items) + } catch (error) { + this.logger.error('gm:characterHair:list error', error) 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) } } diff --git a/src/events/gameMaster/assetManager/characterHair/update.ts b/src/events/gameMaster/assetManager/characterHair/update.ts index 87b1667..a882ce1 100644 --- a/src/events/gameMaster/assetManager/characterHair/update.ts +++ b/src/events/gameMaster/assetManager/characterHair/update.ts @@ -6,7 +6,7 @@ import characterRepository from '#repositories/characterRepository' import SpriteRepository from '#repositories/spriteRepository' type Payload = { - id: number + id: UUID name: string gender: CharacterGender isSelectable: boolean @@ -19,21 +19,17 @@ export default class CharacterHairUpdateEvent extends BaseEvent { } private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise { - const character = await characterRepository.getById(this.socket.characterId as number) - if (!character) return callback(false) - - if (character.role !== 'gm') { - return callback(false) - } - try { + if (!(await this.isCharacterGM())) return + const sprite = await SpriteRepository.getById(data.spriteId) const characterHair = await CharacterHairRepository.getById(data.id) - if (characterHair) { - await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite!).update() + if (!characterHair) { + return callback(false) } + await characterHair.setName(data.name).setGender(data.gender).setIsSelectable(data.isSelectable).setSprite(sprite!).update() return callback(true) } catch (error) { this.logger.error(`Error updating character hair: ${error instanceof Error ? error.message : String(error)}`) diff --git a/src/events/gameMaster/assetManager/characterType/create.ts b/src/events/gameMaster/assetManager/characterType/create.ts index 4bbe104..f47170f 100644 --- a/src/events/gameMaster/assetManager/characterType/create.ts +++ b/src/events/gameMaster/assetManager/characterType/create.ts @@ -1,41 +1,22 @@ -import { CharacterGender, CharacterRace } from '@prisma/client' -import { Server } from 'socket.io' - -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 - ) {} +import { BaseEvent } from '#application/base/baseEvent' +import { CharacterType } from '#entities/characterType' +export default class CharacterTypeCreateEvent extends BaseEvent { public listen(): void { this.socket.on('gm:characterType:create', this.handleEvent.bind(this)) } private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise { try { - const character = await characterRepository.getById(this.socket.characterId as number) - if (!character) return callback(false) + if (!(await this.isCharacterGM())) return - if (character.role !== 'gm') { - return callback(false) - } + const newCharacterType = new CharacterType() + await newCharacterType.setName('New character type').save() - const newCharacterType = await prisma.characterType.create({ - data: { - name: 'New character type', - gender: CharacterGender.MALE, - race: CharacterRace.HUMAN - } - }) - - callback(true, newCharacterType) + return callback(true, newCharacterType) } catch (error) { console.error('Error creating character type:', error) - callback(false) + return callback(false) } } } diff --git a/src/events/gameMaster/assetManager/characterType/delete.ts b/src/events/gameMaster/assetManager/characterType/delete.ts index 321077c..3d8039b 100644 --- a/src/events/gameMaster/assetManager/characterType/delete.ts +++ b/src/events/gameMaster/assetManager/characterType/delete.ts @@ -1,41 +1,28 @@ -import { Server } from 'socket.io' - -import { gameMasterLogger } from '#application/logger' -import { TSocket } from '#application/types' -import characterRepository from '#repositories/characterRepository' +import { UUID } from '#application/types' import CharacterTypeRepository from '#repositories/characterTypeRepository' +import { BaseEvent } from '#application/base/baseEvent' interface IPayload { - id: number + id: UUID } -export default class CharacterTypeDeleteEvent { - constructor( - private readonly io: Server, - private readonly socket: TSocket - ) {} - +export default class CharacterTypeDeleteEvent extends BaseEvent { public listen(): void { this.socket.on('gm:characterType:remove', this.handleEvent.bind(this)) } private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise { - const character = await characterRepository.getById(this.socket.characterId!) - if (!character) return callback(false) - - if (character.role !== 'gm') { - return callback(false) - } - try { + if (!(await this.isCharacterGM())) return + const characterType = await CharacterTypeRepository.getById(data.id) if (!characterType) return callback(false) await characterType.delete() - callback(true) + return callback(true) } catch (error) { - gameMasterLogger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`) - callback(false) + this.logger.error(`Error deleting character type ${data.id}: ${error instanceof Error ? error.message : String(error)}`) + return callback(false) } } } diff --git a/src/events/gameMaster/assetManager/characterType/list.ts b/src/events/gameMaster/assetManager/characterType/list.ts index 15ff718..7436888 100644 --- a/src/events/gameMaster/assetManager/characterType/list.ts +++ b/src/events/gameMaster/assetManager/characterType/list.ts @@ -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 { BaseEvent } from '#application/base/baseEvent' +import { CharacterType } from '#entities/characterType' interface IPayload {} -export default class CharacterTypeListEvent { - constructor( - private readonly io: Server, - private readonly socket: TSocket - ) {} - +export default class CharacterTypeListEvent extends BaseEvent { public listen(): void { this.socket.on('gm:characterType:list', this.handleEvent.bind(this)) } private async handleEvent(data: IPayload, callback: (response: CharacterType[]) => void): Promise { - const character = await characterRepository.getById(this.socket.characterId as number) - if (!character) { - gameMasterLogger.error('gm:characterType:list error', 'Character not found') + try { + if (!(await this.isCharacterGM())) return + + const items = await CharacterTypeRepository.getAll() + return callback(items) + } catch (error) { + this.logger.error('gm:characterType:list error', error) 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) } } diff --git a/src/events/gameMaster/assetManager/object/list.ts b/src/events/gameMaster/assetManager/mapObject/list.ts similarity index 64% rename from src/events/gameMaster/assetManager/object/list.ts rename to src/events/gameMaster/assetManager/mapObject/list.ts index e375a11..477e336 100644 --- a/src/events/gameMaster/assetManager/object/list.ts +++ b/src/events/gameMaster/assetManager/mapObject/list.ts @@ -1,14 +1,15 @@ import ObjectRepository from '#repositories/mapObjectRepository' import { BaseEvent } from '#application/base/baseEvent' +import { MapObject } from '#entities/mapObject' interface IPayload {} -export default class ObjectListEvent extends BaseEvent{ +export default class MapObjectListEvent extends BaseEvent { 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 { + private async handleEvent(data: IPayload, callback: (response: MapObject[]) => void): Promise { if (!(await this.isCharacterGM())) return // get all objects diff --git a/src/events/gameMaster/assetManager/mapObject/remove.ts b/src/events/gameMaster/assetManager/mapObject/remove.ts new file mode 100644 index 0000000..2eda1b3 --- /dev/null +++ b/src/events/gameMaster/assetManager/mapObject/remove.ts @@ -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 { + 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) + } + } +} diff --git a/src/events/gameMaster/assetManager/mapObject/update.ts b/src/events/gameMaster/assetManager/mapObject/update.ts new file mode 100644 index 0000000..2e288e0 --- /dev/null +++ b/src/events/gameMaster/assetManager/mapObject/update.ts @@ -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 { + 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) + } + } +} diff --git a/src/events/gameMaster/assetManager/mapObject/upload.ts b/src/events/gameMaster/assetManager/mapObject/upload.ts new file mode 100644 index 0000000..6015659 --- /dev/null +++ b/src/events/gameMaster/assetManager/mapObject/upload.ts @@ -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 { + 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) + } + } +} diff --git a/src/events/gameMaster/assetManager/object/remove.ts b/src/events/gameMaster/assetManager/object/remove.ts deleted file mode 100644 index 6f63eef..0000000 --- a/src/events/gameMaster/assetManager/object/remove.ts +++ /dev/null @@ -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 { - 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) - } - } -} diff --git a/src/events/gameMaster/assetManager/object/update.ts b/src/events/gameMaster/assetManager/object/update.ts deleted file mode 100644 index 305a927..0000000 --- a/src/events/gameMaster/assetManager/object/update.ts +++ /dev/null @@ -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 { - 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) - } - } -} diff --git a/src/events/gameMaster/assetManager/object/upload.ts b/src/events/gameMaster/assetManager/object/upload.ts deleted file mode 100644 index 840ee66..0000000 --- a/src/events/gameMaster/assetManager/object/upload.ts +++ /dev/null @@ -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 { - 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) - } - } -} diff --git a/src/events/gameMaster/mapEditor/create.ts b/src/events/gameMaster/mapEditor/create.ts index 8b8b94a..60c168f 100644 --- a/src/events/gameMaster/mapEditor/create.ts +++ b/src/events/gameMaster/mapEditor/create.ts @@ -10,7 +10,7 @@ type Payload = { export default class MapCreateEvent extends BaseEvent { 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 { @@ -30,7 +30,7 @@ export default class MapCreateEvent extends BaseEvent { const mapList = await MapRepository.getAll() return callback(mapList) } 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.' }) return callback([]) } diff --git a/src/events/gameMaster/mapEditor/delete.ts b/src/events/gameMaster/mapEditor/delete.ts index d4fb700..80d92ea 100644 --- a/src/events/gameMaster/mapEditor/delete.ts +++ b/src/events/gameMaster/mapEditor/delete.ts @@ -8,7 +8,7 @@ type Payload = { export default class MapDeleteEvent extends BaseEvent { 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 { @@ -22,7 +22,7 @@ export default class MapDeleteEvent extends BaseEvent { this.logger.info(`Map ${data.mapId} deleted successfully.`) return callback(true) } catch (error: unknown) { - this.logger.error('gm:map_editor:map:delete error', error) + this.logger.error('gm:map:delete error', error) return callback(false) } } diff --git a/src/events/gameMaster/mapEditor/list.ts b/src/events/gameMaster/mapEditor/list.ts index 8130643..4161a1b 100644 --- a/src/events/gameMaster/mapEditor/list.ts +++ b/src/events/gameMaster/mapEditor/list.ts @@ -6,7 +6,7 @@ interface IPayload {} export default class MapListEvent extends BaseEvent { 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 { @@ -18,7 +18,7 @@ export default class MapListEvent extends BaseEvent { const maps = await MapRepository.getAll() return callback(maps) } 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([]) } } diff --git a/src/events/gameMaster/mapEditor/request.ts b/src/events/gameMaster/mapEditor/request.ts index 9809e0a..f5e95c7 100644 --- a/src/events/gameMaster/mapEditor/request.ts +++ b/src/events/gameMaster/mapEditor/request.ts @@ -2,6 +2,7 @@ import { BaseEvent } from '#application/base/baseEvent' import { UUID } from '#application/types' import { Map } from '#entities/map' import MapRepository from '#repositories/mapRepository' +import Database from '#application/database' interface IPayload { mapId: UUID @@ -9,7 +10,7 @@ interface IPayload { export default class MapRequestEvent extends BaseEvent { 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 { @@ -25,14 +26,18 @@ export default class MapRequestEvent extends BaseEvent { const map = await MapRepository.getById(data.mapId) + if (!map) { this.logger.info(`User ${(await this.getCharacter())!.getId()} tried to request map ${data.mapId} but it does not exist.`) return callback(null) } + console.log(map) + await Database.getEntityManager().populate(map, ['mapEventTiles', 'placedMapObjects']) + return callback(map) } 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) } } diff --git a/src/events/gameMaster/mapEditor/update.ts b/src/events/gameMaster/mapEditor/update.ts index 6d95a2d..4b11a62 100644 --- a/src/events/gameMaster/mapEditor/update.ts +++ b/src/events/gameMaster/mapEditor/update.ts @@ -5,9 +5,9 @@ import { Map } from '#entities/map' import { MapEffect } from '#entities/mapEffect' import { MapEventTile } from '#entities/mapEventTile' import { MapEventTileTeleport } from '#entities/mapEventTileTeleport' -import { MapObject } from '#entities/mapObject' import mapManager from '#managers/mapManager' import MapRepository from '#repositories/mapRepository' +import { PlacedMapObject } from '#entities/placedMapObject' interface IPayload { mapId: UUID @@ -31,12 +31,12 @@ interface IPayload { effect: string strength: number }[] - mapObjects: MapObject[] + placedMapObjects: PlacedMapObject[] } export default class MapUpdateEvent extends BaseEvent { 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 { @@ -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.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 map.mapEventTiles.removeAll() - map.mapObjects.removeAll() + map.placedMapObjects.removeAll() map.mapEffects.removeAll() // Create and add new map event tiles @@ -95,16 +95,14 @@ export default class MapUpdateEvent extends BaseEvent { } // Create and add new map objects - for (const object of data.mapObjects) { - const mapObject = new MapObject().setMapObject(object.mapObject).setDepth(object.depth).setIsRotated(object.isRotated).setPositionX(object.positionX).setPositionY(object.positionY).setMap(map) - - map.mapObjects.add(mapObject) + for (const object of data.placedMapObjects) { + 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) } // Create and add new map effects for (const effect of data.mapEffects) { const mapEffect = new MapEffect().setEffect(effect.effect).setStrength(effect.strength).setMap(map) - map.mapEffects.add(mapEffect) } @@ -125,7 +123,7 @@ export default class MapUpdateEvent extends BaseEvent { return callback(map) } 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) } } diff --git a/src/http/controllers/assets.ts b/src/http/controllers/assets.ts index 4be8bf9..8c1bfd9 100644 --- a/src/http/controllers/assets.ts +++ b/src/http/controllers/assets.ts @@ -74,17 +74,17 @@ export class AssetsController extends BaseController { await Database.getEntityManager().populate(sprite, ['spriteActions']) 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', - group: spriteAction.isAnimated ? 'sprite_animations' : 'sprites', + group: spriteAction.getIsAnimated() ? 'sprite_animations' : 'sprites', updatedAt: sprite.getUpdatedAt(), - originX: Number(spriteAction.originX.toString()), - originY: Number(spriteAction.originY.toString()), + originX: Number(spriteAction.getOriginX().toString()), + originY: Number(spriteAction.getOriginY().toString()), isAnimated: spriteAction.getIsAnimated(), frameRate: spriteAction.getFrameRate(), frameWidth: spriteAction.getFrameWidth(), frameHeight: spriteAction.getFrameHeight(), - frameCount: JSON.parse(JSON.stringify(spriteAction.getSprites())).length + frameCount: spriteAction.getSprites()?.length })) return this.sendSuccess(res, assets) diff --git a/src/repositories/characterRepository.ts b/src/repositories/characterRepository.ts index 87e31c5..ddacc3f 100644 --- a/src/repositories/characterRepository.ts +++ b/src/repositories/characterRepository.ts @@ -1,12 +1,13 @@ import { BaseRepository } from '#application/base/baseRepository' import { UUID } from '#application/types' import { Character } from '#entities/character' +import { LoadHint, Populate } from '@mikro-orm/core' class CharacterRepository extends BaseRepository { - async getByUserId(userId: UUID): Promise { + async getByUserId(userId: UUID, populate?: LoadHint): Promise { try { const repository = this.em.getRepository(Character) - return await repository.find({ user: userId }) + return await repository.find({ user: userId }, { populate: populate as Populate }) } catch (error: any) { this.logger.error(`Failed to get character by user ID: ${error instanceof Error ? error.message : String(error)}`) return [] diff --git a/src/repositories/characterTypeRepository.ts b/src/repositories/characterTypeRepository.ts index dd763f3..78051f6 100644 --- a/src/repositories/characterTypeRepository.ts +++ b/src/repositories/characterTypeRepository.ts @@ -19,7 +19,7 @@ class CharacterTypeRepository extends BaseRepository { return await repository.findAll() } catch (error: any) { this.logger.error(`Failed to get all character types: ${error instanceof Error ? error.message : String(error)}`) - return null + return [] } } diff --git a/src/repositories/mapObjectRepository.ts b/src/repositories/mapObjectRepository.ts index 1496387..13290ae 100644 --- a/src/repositories/mapObjectRepository.ts +++ b/src/repositories/mapObjectRepository.ts @@ -1,22 +1,23 @@ import { BaseRepository } from '#application/base/baseRepository' import { UUID } from '#application/types' +import { MapObject } from '#entities/mapObject' class MapObjectRepository extends BaseRepository { - async getById(id: UUID): Promise { + async getById(id: UUID): Promise { try { - const repository = this.em.getRepository(Object) + const repository = this.em.getRepository(MapObject) return await repository.findOne({ id }) } catch (error: any) { return null } } - async getAll(): Promise { + async getAll(): Promise { try { - const repository = this.em.getRepository(Object) + const repository = this.em.getRepository(MapObject) return await repository.findAll() } catch (error: any) { - return null + return [] } } }