diff --git a/.gitignore b/.gitignore index 8f6d3e2..eeac31a 100644 --- a/.gitignore +++ b/.gitignore @@ -309,7 +309,4 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk -# End of https://www.toptal.com/developers/gitignore/api/node,jetbrains+all,visualstudiocode,macos,windows - -prisma/dev.db -prisma/dev.db-journal \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/node,jetbrains+all,visualstudiocode,macos,windows \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c173541..08412ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,9 +44,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.0.tgz", - "integrity": "sha512-XMBySMuNZs3DM96xcJmLW4EfGnf+uGmFNjzpehMjuX5PLB5j87ar2Zc4e3PVeZ3I5g3tYtAqskB28manlF69Zw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", "license": "MIT", "optional": true, "dependencies": { @@ -719,9 +719,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.16.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz", - "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==", + "version": "20.16.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", + "integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -982,9 +982,9 @@ "license": "BSD-3-Clause" }, "node_modules/bullmq": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.15.0.tgz", - "integrity": "sha512-h53shVjx8s6wxYGtUfzAfENpSP7N5T0D4PMTvbZncozLjb8yUKhopfpa7PmcpQfq7SSO9dm/OZ9XQuGOCSGNug==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.20.0.tgz", + "integrity": "sha512-eCJyYJqNUl9swC39x2fVm1BUv5BuO/nv2eAcAsz58znue0ZCYgSG+yWXZeauRG98Jl0UIBcPgJtbF+c9Wd+Odg==", "license": "MIT", "dependencies": { "cron-parser": "^4.6.0", @@ -1128,9 +1128,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -1282,9 +1282,9 @@ } }, "node_modules/engine.io": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.1.tgz", - "integrity": "sha512-NEpDCw9hrvBW+hVEOK4T7v0jFJ++KgtPl4jKFwsZVfG1XhS0dCrSb3VMb9gPAd7VAdW52VT1EnaNiU2vM8C0og==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "license": "MIT", "dependencies": { "@types/cookie": "^0.4.1", @@ -1292,7 +1292,7 @@ "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", @@ -1312,9 +1312,9 @@ } }, "node_modules/engine.io/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -1398,9 +1398,9 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -1408,7 +1408,7 @@ "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -2867,9 +2867,9 @@ } }, "node_modules/typescript": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", - "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/prisma/schema/zone.prisma b/prisma/schema/zone.prisma index f980b0a..3772e1c 100644 --- a/prisma/schema/zone.prisma +++ b/prisma/schema/zone.prisma @@ -38,6 +38,7 @@ model Zone { height Int @default(10) tiles Json? pvp Boolean @default(false) + effects ZoneEffect[] zoneEventTiles ZoneEventTile[] zoneEventTileTeleports ZoneEventTileTeleport[] zoneObjects ZoneObject[] @@ -47,6 +48,14 @@ model Zone { updatedAt DateTime @updatedAt } +model ZoneEffect { + id String @id @default(uuid()) + zoneId Int + zone Zone @relation(fields: [zoneId], references: [id], onDelete: Cascade) + effect String + strength Int +} + model ZoneObject { id String @id @default(uuid()) zoneId Int diff --git a/src/commands/tiles.ts b/src/commands/tiles.ts index 0a59de9..02f2d39 100644 --- a/src/commands/tiles.ts +++ b/src/commands/tiles.ts @@ -2,7 +2,7 @@ import fs from 'fs' import sharp from 'sharp' import { commandLogger } from '../utilities/logger' import { Server } from 'socket.io' -import { getPublicPath } from '../utilities/utilities' +import { getPublicPath } from '../utilities/files' import path from 'path' export default class TilesCommand { diff --git a/src/managers/commandManager.ts b/src/managers/commandManager.ts index e56d194..4c8a1a0 100644 --- a/src/managers/commandManager.ts +++ b/src/managers/commandManager.ts @@ -3,7 +3,7 @@ import * as fs from 'fs' import * as path from 'path' import { Server } from 'socket.io' import { commandLogger } from '../utilities/logger' -import { getAppPath } from '../utilities/utilities' +import { getAppPath } from '../utilities/files' class CommandManager { private commands: Map = new Map() diff --git a/src/managers/datetimeManager.ts b/src/managers/datetimeManager.ts new file mode 100644 index 0000000..7dc2c0e --- /dev/null +++ b/src/managers/datetimeManager.ts @@ -0,0 +1,84 @@ +// src/managers/datetimeManager.ts + +import fs from 'fs/promises' +import { Server } from 'socket.io' +import { appLogger } from '../utilities/logger' +import { createDir, doesPathExist, getRootPath } from '../utilities/files' + +class DatetimeManager { + private static readonly GAME_SPEED = 24 / 3 // 24 hours / 3 hours = 8x speed + private static readonly UPDATE_INTERVAL = 1000 // Update every second for smooth second transitions + + private io: Server | null = null + private intervalId: NodeJS.Timeout | null = null + + public async boot(io: Server): Promise { + this.io = io + this.startDateTimeLoop() + appLogger.info('Datetime manager loaded') + } + + public stop(): void { + if (this.intervalId) { + clearInterval(this.intervalId) + this.intervalId = null + } + } + + public async loadDateTime(): Promise { + try { + const datetimeFilePath = this.getDatetimeFilePath() + const content = await fs.readFile(datetimeFilePath, 'utf-8') + return new Date(content.trim()) + } catch (error) { + appLogger.error(`Failed to load datetime: ${error instanceof Error ? error.message : String(error)}`) + return new Date() // Use current date as fallback + } + } + + private startDateTimeLoop(): void { + this.intervalId = setInterval(async () => { + const currentDateTime = await this.loadDateTime() + this.advanceGameTime(currentDateTime) + this.emitDateTime(currentDateTime) + this.saveDateTimeIfNeeded(currentDateTime) + }, DatetimeManager.UPDATE_INTERVAL) + } + + private advanceGameTime(currentDateTime: Date): void { + const advanceTime = (DatetimeManager.GAME_SPEED * DatetimeManager.UPDATE_INTERVAL) / 1000 * 1000 + currentDateTime.setTime(currentDateTime.getTime() + advanceTime) + } + + private emitDateTime(currentDateTime: Date): void { + this.io?.emit('datetime', this.formatDateTime(currentDateTime)) + } + + private formatDateTime(date: Date): string { + return date.toISOString().slice(0, 19).replace('T', ' ') + } + + private saveDateTimeIfNeeded(currentDateTime: Date): void { + if (currentDateTime.getMilliseconds() < DatetimeManager.UPDATE_INTERVAL) { + this.saveDateTime(currentDateTime) + } + } + + private async saveDateTime(currentDateTime: Date): Promise { + try { + const datetimeFilePath = this.getDatetimeFilePath() + await fs.writeFile(datetimeFilePath, this.formatDateTime(currentDateTime)) + } catch (error) { + appLogger.error(`Failed to save datetime: ${error instanceof Error ? error.message : String(error)}`) + } + } + + private getDatetimeFilePath(): string { + if (!doesPathExist(getRootPath('data'))) { + createDir(getRootPath('data')) + } + return getRootPath('data', 'datetime.txt') + } +} + +export default new DatetimeManager() \ No newline at end of file diff --git a/src/managers/queueManager.ts b/src/managers/queueManager.ts index 9856619..d05fe7d 100644 --- a/src/managers/queueManager.ts +++ b/src/managers/queueManager.ts @@ -5,7 +5,7 @@ import { Server as SocketServer } from 'socket.io' import { TSocket } from '../utilities/types' import { queueLogger } from '../utilities/logger' import fs from 'fs' -import { getAppPath } from '../utilities/utilities' +import { getAppPath } from '../utilities/files' class QueueManager { private connection!: IORedis diff --git a/src/managers/weatherManager.ts b/src/managers/weatherManager.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/server.ts b/src/server.ts index 959bc7e..d9b1bf5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,6 +1,7 @@ import fs from 'fs' import express, { Application } from 'express' import config from './utilities/config' +import { getAppPath } from './utilities/files' import { createServer as httpServer, Server as HTTPServer } from 'http' import { addHttpRoutes } from './utilities/http' import cors from 'cors' @@ -14,7 +15,7 @@ import UserManager from './managers/userManager' import CommandManager from './managers/commandManager' import CharacterManager from './managers/characterManager' import QueueManager from './managers/queueManager' -import { getAppPath } from './utilities/utilities' +import DatetimeManager from './managers/datetimeManager' export class Server { private readonly app: Application @@ -66,6 +67,9 @@ export class Server { // Load user manager await UserManager.boot() + // Load datetime manager + await DatetimeManager.boot(this.io) + // Load zoneEditor manager await ZoneManager.boot() diff --git a/src/socketEvents/disconnect.ts b/src/socketEvents/disconnect.ts index 19f882e..4bd5acf 100644 --- a/src/socketEvents/disconnect.ts +++ b/src/socketEvents/disconnect.ts @@ -29,6 +29,8 @@ export default class DisconnectEvent { return } + character.resetMovement = true + gameLogger.info('User disconnected along with their character') await CharacterManager.removeCharacter(character) diff --git a/src/socketEvents/gameMaster/assetManager/object/remove.ts b/src/socketEvents/gameMaster/assetManager/object/remove.ts index b94ce60..17d0ce9 100644 --- a/src/socketEvents/gameMaster/assetManager/object/remove.ts +++ b/src/socketEvents/gameMaster/assetManager/object/remove.ts @@ -3,7 +3,7 @@ import { Server } from 'socket.io' import { TSocket } from '../../../../utilities/types' import prisma from '../../../../utilities/prisma' import characterRepository from '../../../../repositories/characterRepository' -import { getPublicPath } from '../../../../utilities/utilities' +import { getPublicPath } from '../../../../utilities/files' interface IPayload { object: string diff --git a/src/socketEvents/gameMaster/assetManager/object/upload.ts b/src/socketEvents/gameMaster/assetManager/object/upload.ts index a7bfb0e..c42ffae 100644 --- a/src/socketEvents/gameMaster/assetManager/object/upload.ts +++ b/src/socketEvents/gameMaster/assetManager/object/upload.ts @@ -6,7 +6,7 @@ import prisma from '../../../../utilities/prisma' import sharp from 'sharp' import characterRepository from '../../../../repositories/characterRepository' import { gameMasterLogger } from '../../../../utilities/logger' -import { getPublicPath } from '../../../../utilities/utilities' +import { getPublicPath } from '../../../../utilities/files' interface IObjectData { [key: string]: Buffer diff --git a/src/socketEvents/gameMaster/assetManager/sprite/create.ts b/src/socketEvents/gameMaster/assetManager/sprite/create.ts index b9b3bf0..68ba746 100644 --- a/src/socketEvents/gameMaster/assetManager/sprite/create.ts +++ b/src/socketEvents/gameMaster/assetManager/sprite/create.ts @@ -3,7 +3,7 @@ import { TSocket } from '../../../../utilities/types' import fs from 'fs/promises' import prisma from '../../../../utilities/prisma' import characterRepository from '../../../../repositories/characterRepository' -import { getPublicPath } from '../../../../utilities/utilities' +import { getPublicPath } from '../../../../utilities/files' export default class SpriteCreateEvent { constructor( diff --git a/src/socketEvents/gameMaster/assetManager/sprite/delete.ts b/src/socketEvents/gameMaster/assetManager/sprite/delete.ts index 024336b..f346a8d 100644 --- a/src/socketEvents/gameMaster/assetManager/sprite/delete.ts +++ b/src/socketEvents/gameMaster/assetManager/sprite/delete.ts @@ -4,7 +4,7 @@ import fs from 'fs' import prisma from '../../../../utilities/prisma' import CharacterManager from '../../../../managers/characterManager' import { gameMasterLogger } from '../../../../utilities/logger' -import { getPublicPath } from '../../../../utilities/utilities' +import { getPublicPath } from '../../../../utilities/files' type Payload = { id: string diff --git a/src/socketEvents/gameMaster/assetManager/sprite/update.ts b/src/socketEvents/gameMaster/assetManager/sprite/update.ts index d23734a..5d15424 100644 --- a/src/socketEvents/gameMaster/assetManager/sprite/update.ts +++ b/src/socketEvents/gameMaster/assetManager/sprite/update.ts @@ -5,7 +5,7 @@ import type { Prisma, SpriteAction } from '@prisma/client' import { writeFile, mkdir } from 'node:fs/promises' import sharp from 'sharp' import CharacterManager from '../../../../managers/characterManager' -import { getPublicPath } from '../../../../utilities/utilities' +import { getPublicPath } from '../../../../utilities/files' type SpriteActionInput = Omit & { sprites: string[] diff --git a/src/socketEvents/gameMaster/assetManager/tile/delete.ts b/src/socketEvents/gameMaster/assetManager/tile/delete.ts index f2be8de..32becbe 100644 --- a/src/socketEvents/gameMaster/assetManager/tile/delete.ts +++ b/src/socketEvents/gameMaster/assetManager/tile/delete.ts @@ -4,7 +4,7 @@ import { TSocket } from '../../../../utilities/types' import prisma from '../../../../utilities/prisma' import characterRepository from '../../../../repositories/characterRepository' import { gameMasterLogger } from '../../../../utilities/logger' -import { getPublicPath } from '../../../../utilities/utilities' +import { getPublicPath } from '../../../../utilities/files' type Payload = { id: string diff --git a/src/socketEvents/gameMaster/assetManager/tile/upload.ts b/src/socketEvents/gameMaster/assetManager/tile/upload.ts index 8c680f0..beb8ea6 100644 --- a/src/socketEvents/gameMaster/assetManager/tile/upload.ts +++ b/src/socketEvents/gameMaster/assetManager/tile/upload.ts @@ -5,7 +5,7 @@ import fs from 'fs/promises' import prisma from '../../../../utilities/prisma' import characterRepository from '../../../../repositories/characterRepository' import { gameMasterLogger } from '../../../../utilities/logger' -import { getPublicPath } from '../../../../utilities/utilities' +import { getPublicPath } from '../../../../utilities/files' interface ITileData { [key: string]: Buffer diff --git a/src/utilities/utilities.ts b/src/utilities/files.ts similarity index 64% rename from src/utilities/utilities.ts rename to src/utilities/files.ts index 416816b..bcca0bf 100644 --- a/src/utilities/utilities.ts +++ b/src/utilities/files.ts @@ -1,5 +1,6 @@ import config from './config' import path from 'path' +import fs from 'fs' export function getRootPath(folder: string, ...additionalSegments: string[]) { return path.join(process.cwd(), folder, ...additionalSegments) @@ -13,3 +14,20 @@ export function getAppPath(folder: string, ...additionalSegments: string[]) { export function getPublicPath(folder: string, ...additionalSegments: string[]) { return path.join(process.cwd(), 'public', folder, ...additionalSegments) } + +export function doesPathExist(path: string) { + try { + fs.accessSync(path, fs.constants.F_OK); + return true; + } catch (e) { + return false; + } +} + +export function createDir(path: string) { + try { + fs.mkdirSync(path, { recursive: true }); + } catch (e) { + console.error(e); + } +} \ No newline at end of file diff --git a/src/utilities/http.ts b/src/utilities/http.ts index e0f6433..1910678 100644 --- a/src/utilities/http.ts +++ b/src/utilities/http.ts @@ -12,7 +12,7 @@ import fs from 'fs' import zoneRepository from '../repositories/zoneRepository' import zoneManager from '../managers/zoneManager' import { httpLogger } from './logger' -import { getPublicPath } from './utilities' +import { getPublicPath } from './files' async function addHttpRoutes(app: Application) { /** diff --git a/src/utilities/logger.ts b/src/utilities/logger.ts index 24140be..7e5d55c 100644 --- a/src/utilities/logger.ts +++ b/src/utilities/logger.ts @@ -1,6 +1,6 @@ import pino from 'pino' import fs from 'fs' -import { getRootPath } from './utilities' +import { getRootPath } from './files' // Array of log types const LOG_TYPES = ['http', 'game', 'gameMaster', 'app', 'queue', 'command'] as const