From bb9f62a9c84a8cc66d71f135406a83cd9daa436f Mon Sep 17 00:00:00 2001
From: Dennis Postma <dennis@directonline.io>
Date: Mon, 14 Oct 2024 19:47:52 +0200
Subject: [PATCH] Renamed files to storage, re-worked datetimeManager, added
 json help utilities

---
 package.json                                  |  2 +-
 src/commands/tiles.ts                         |  2 +-
 src/managers/commandManager.ts                |  2 +-
 src/managers/datetimeManager.ts               | 56 ++++++++-----------
 src/managers/queueManager.ts                  |  2 +-
 src/server.ts                                 |  2 +-
 .../gameMaster/assetManager/object/remove.ts  |  2 +-
 .../gameMaster/assetManager/object/upload.ts  |  2 +-
 .../gameMaster/assetManager/sprite/create.ts  |  2 +-
 .../gameMaster/assetManager/sprite/delete.ts  |  2 +-
 .../gameMaster/assetManager/sprite/update.ts  |  2 +-
 .../gameMaster/assetManager/tile/delete.ts    |  2 +-
 .../gameMaster/assetManager/tile/upload.ts    |  2 +-
 src/utilities/http.ts                         |  2 +-
 src/utilities/json.ts                         | 49 ++++++++++++++++
 src/utilities/logger.ts                       |  2 +-
 src/utilities/{files.ts => storage.ts}        |  0
 17 files changed, 86 insertions(+), 47 deletions(-)
 create mode 100644 src/utilities/json.ts
 rename src/utilities/{files.ts => storage.ts} (100%)

diff --git a/package.json b/package.json
index 3e38dd8..1a3bbef 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "scripts": {
     "start": "npx prisma migrate deploy && node dist/server.js",
-    "dev": "nodemon --exec ts-node src/server.ts",
+    "dev": "nodemon --ignore 'data/*' --exec ts-node src/server.ts",
     "build": "tsc",
     "format": "prettier --write src/"
   },
diff --git a/src/commands/tiles.ts b/src/commands/tiles.ts
index 02f2d39..8f9ba4f 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/files'
+import { getPublicPath } from '../utilities/storage'
 import path from 'path'
 
 export default class TilesCommand {
diff --git a/src/managers/commandManager.ts b/src/managers/commandManager.ts
index 4c8a1a0..5d6f386 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/files'
+import { getAppPath } from '../utilities/storage'
 
 class CommandManager {
   private commands: Map<string, any> = new Map()
diff --git a/src/managers/datetimeManager.ts b/src/managers/datetimeManager.ts
index 7dc2c0e..cd511c7 100644
--- a/src/managers/datetimeManager.ts
+++ b/src/managers/datetimeManager.ts
@@ -1,19 +1,21 @@
 // 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'
+import { getRootPath } from '../utilities/storage'
+import { readJsonValue, setJsonValue } from '../utilities/json'
 
 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 static readonly GAME_SPEED = 8 // 24 game hours / 3 real hours
+  private static readonly UPDATE_INTERVAL = 1000 // 1 second
 
   private io: Server | null = null
   private intervalId: NodeJS.Timeout | null = null
+  private currentDateTime: Date = new Date()
 
   public async boot(io: Server): Promise<void> {
     this.io = io
+    await this.loadDateTime()
     this.startDateTimeLoop()
     appLogger.info('Datetime manager loaded')
   }
@@ -25,59 +27,47 @@ class DatetimeManager {
     }
   }
 
-  public async loadDateTime(): Promise<Date> {
+  private async loadDateTime(): Promise<void> {
     try {
-      const datetimeFilePath = this.getDatetimeFilePath()
-      const content = await fs.readFile(datetimeFilePath, 'utf-8')
-      return new Date(content.trim())
+      const datetimeString = await readJsonValue<string>(this.getWorldFilePath(), 'datetime')
+      this.currentDateTime = new Date(datetimeString)
     } catch (error) {
       appLogger.error(`Failed to load datetime: ${error instanceof Error ? error.message : String(error)}`)
-      return new Date() // Use current date as fallback
+      this.currentDateTime = 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)
+    this.intervalId = setInterval(() => {
+      this.advanceGameTime()
+      this.emitDateTime()
+      this.saveDateTime()
     }, DatetimeManager.UPDATE_INTERVAL)
   }
 
-  private advanceGameTime(currentDateTime: Date): void {
-    const advanceTime = (DatetimeManager.GAME_SPEED * DatetimeManager.UPDATE_INTERVAL) / 1000 * 1000
-    currentDateTime.setTime(currentDateTime.getTime() + advanceTime)
+  private advanceGameTime(): void {
+    const advanceMilliseconds = DatetimeManager.GAME_SPEED * DatetimeManager.UPDATE_INTERVAL
+    this.currentDateTime = new Date(this.currentDateTime.getTime() + advanceMilliseconds)
   }
 
-  private emitDateTime(currentDateTime: Date): void {
-    this.io?.emit('datetime', this.formatDateTime(currentDateTime))
+  private emitDateTime(): void {
+    this.io?.emit('datetime', this.formatDateTime(this.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<void> {
+  private async saveDateTime(): Promise<void> {
     try {
-      const datetimeFilePath = this.getDatetimeFilePath()
-      await fs.writeFile(datetimeFilePath, this.formatDateTime(currentDateTime))
+      await setJsonValue(this.getWorldFilePath(), 'datetime', this.formatDateTime(this.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')
+  private getWorldFilePath(): string {
+    return getRootPath('data', 'world.json')
   }
 }
 
diff --git a/src/managers/queueManager.ts b/src/managers/queueManager.ts
index d05fe7d..27e226b 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/files'
+import { getAppPath } from '../utilities/storage'
 
 class QueueManager {
   private connection!: IORedis
diff --git a/src/server.ts b/src/server.ts
index d9b1bf5..5a3a271 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,7 +1,7 @@
 import fs from 'fs'
 import express, { Application } from 'express'
 import config from './utilities/config'
-import { getAppPath } from './utilities/files'
+import { getAppPath } from './utilities/storage'
 import { createServer as httpServer, Server as HTTPServer } from 'http'
 import { addHttpRoutes } from './utilities/http'
 import cors from 'cors'
diff --git a/src/socketEvents/gameMaster/assetManager/object/remove.ts b/src/socketEvents/gameMaster/assetManager/object/remove.ts
index 17d0ce9..883e577 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/files'
+import { getPublicPath } from '../../../../utilities/storage'
 
 interface IPayload {
   object: string
diff --git a/src/socketEvents/gameMaster/assetManager/object/upload.ts b/src/socketEvents/gameMaster/assetManager/object/upload.ts
index c42ffae..7b196da 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/files'
+import { getPublicPath } from '../../../../utilities/storage'
 
 interface IObjectData {
   [key: string]: Buffer
diff --git a/src/socketEvents/gameMaster/assetManager/sprite/create.ts b/src/socketEvents/gameMaster/assetManager/sprite/create.ts
index 68ba746..b7447a7 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/files'
+import { getPublicPath } from '../../../../utilities/storage'
 
 export default class SpriteCreateEvent {
   constructor(
diff --git a/src/socketEvents/gameMaster/assetManager/sprite/delete.ts b/src/socketEvents/gameMaster/assetManager/sprite/delete.ts
index f346a8d..a97c2e3 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/files'
+import { getPublicPath } from '../../../../utilities/storage'
 
 type Payload = {
   id: string
diff --git a/src/socketEvents/gameMaster/assetManager/sprite/update.ts b/src/socketEvents/gameMaster/assetManager/sprite/update.ts
index 5d15424..9800fe9 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/files'
+import { getPublicPath } from '../../../../utilities/storage'
 
 type SpriteActionInput = Omit<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> & {
   sprites: string[]
diff --git a/src/socketEvents/gameMaster/assetManager/tile/delete.ts b/src/socketEvents/gameMaster/assetManager/tile/delete.ts
index 32becbe..1bf994a 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/files'
+import { getPublicPath } from '../../../../utilities/storage'
 
 type Payload = {
   id: string
diff --git a/src/socketEvents/gameMaster/assetManager/tile/upload.ts b/src/socketEvents/gameMaster/assetManager/tile/upload.ts
index beb8ea6..b14febf 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/files'
+import { getPublicPath } from '../../../../utilities/storage'
 
 interface ITileData {
   [key: string]: Buffer
diff --git a/src/utilities/http.ts b/src/utilities/http.ts
index 1910678..2581d5a 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 './files'
+import { getPublicPath } from './storage'
 
 async function addHttpRoutes(app: Application) {
   /**
diff --git a/src/utilities/json.ts b/src/utilities/json.ts
new file mode 100644
index 0000000..b7735bf
--- /dev/null
+++ b/src/utilities/json.ts
@@ -0,0 +1,49 @@
+import * as fs from 'fs/promises';
+import { appLogger } from './logger';
+
+export async function readJsonFile<T>(filePath: string): Promise<T> {
+  try {
+    const fileContent = await fs.readFile(filePath, 'utf-8');
+    return JSON.parse(fileContent) as T;
+  } catch (error) {
+    appLogger.error(`Error reading JSON file: ${error instanceof Error ? error.message : String(error)}`);
+    throw error;
+  }
+}
+
+export async function writeJsonFile<T>(filePath: string, data: T): Promise<void> {
+  try {
+    const jsonString = JSON.stringify(data, null, 2);
+    await fs.writeFile(filePath, jsonString, 'utf-8');
+  } catch (error) {
+    appLogger.error(`Error writing JSON file: ${error instanceof Error ? error.message : String(error)}`);
+    throw error;
+  }
+}
+
+export async function readJsonValue<T>(filePath: string, paramPath: string): Promise<T> {
+  try {
+    const jsonContent = await readJsonFile<any>(filePath);
+    const paramValue = paramPath.split('.').reduce((obj, key) => obj && obj[key], jsonContent);
+
+    if (paramValue === undefined) {
+      throw new Error(`Parameter ${paramPath} not found in the JSON file`);
+    }
+
+    return paramValue as T;
+  } catch (error) {
+    appLogger.error(`Error reading JSON parameter: ${error instanceof Error ? error.message : String(error)}`);
+    throw error;
+  }
+}
+
+export async function setJsonValue<T>(filePath: string, key: string, value: any): Promise<void> {
+  try {
+    const data = await readJsonFile<T>(filePath);
+    const updatedData = { ...data, [key]: value };
+    await writeJsonFile(filePath, updatedData);
+  } catch (error) {
+    appLogger.error(`Error setting JSON value: ${error instanceof Error ? error.message : String(error)}`);
+    throw error;
+  }
+}
\ No newline at end of file
diff --git a/src/utilities/logger.ts b/src/utilities/logger.ts
index 7e5d55c..2c0cbb9 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 './files'
+import { getRootPath } from './storage'
 
 // Array of log types
 const LOG_TYPES = ['http', 'game', 'gameMaster', 'app', 'queue', 'command'] as const
diff --git a/src/utilities/files.ts b/src/utilities/storage.ts
similarity index 100%
rename from src/utilities/files.ts
rename to src/utilities/storage.ts