Compare commits

...

43 Commits

Author SHA1 Message Date
9e55ac7990 Replaced all event names with numbers for less bandwidth usage 2025-02-11 23:12:41 +01:00
8b51f6e16a Socket enum 2025-02-11 22:28:19 +01:00
e2ded75017 WIP event delays 2025-02-10 17:56:38 +01:00
0cead14e71 Updated move interval 2025-02-10 15:33:17 +01:00
f2905247ff Value update 2025-02-10 15:30:02 +01:00
e735522d76 Rate limit walking 2025-02-10 15:27:36 +01:00
94c619192b Minor improvement 2025-02-10 14:44:35 +01:00
a8a98d0083 Returned, it's platform issue 2025-02-10 14:08:46 +01:00
646c40db27 Removed old unsupported package 2025-02-10 14:08:03 +01:00
cfaea6ec7c WS improvements 2025-02-10 13:59:25 +01:00
9030550c0f Updated packages 2025-02-10 02:00:39 +01:00
c71c393a51 Import order 2025-02-09 20:01:52 +01:00
4d192bd5ec Added mysql code but commented out 2025-02-09 20:01:17 +01:00
f46fff5b69 Updated driver 2025-02-09 18:25:12 +01:00
f5cae0db9f Updated driver 2025-02-09 18:20:59 +01:00
c627ea2412 revert 2025-02-09 18:04:25 +01:00
30145e1662 frameCount 2025-02-09 17:59:30 +01:00
778e4402ba Count fix 2025-02-09 17:53:34 +01:00
e2bc151881 package update 2025-02-09 17:25:57 +01:00
2b022ee4e0 Cleaned character create event 2025-02-09 17:12:46 +01:00
3802b2cf9d Unnecessary 2025-02-09 16:20:59 +01:00
2ee6a72984 Unnecessary 2025-02-09 16:20:38 +01:00
f50e4c75a9 final 2025-02-09 03:26:36 +01:00
51cbe87755 alternative approach cmd 2025-02-09 03:24:32 +01:00
d6aa8da2de Take cmds 2025-02-09 03:22:48 +01:00
7b4674587a Added start bash file 2025-02-09 02:27:40 +01:00
8e652f8dcb Put back required config param 2025-02-09 02:25:44 +01:00
5349e2ffe5 Improvements for prod. 2025-02-09 02:24:53 +01:00
66759a87f2 Use https if config is present 2025-02-09 02:09:58 +01:00
b7748c254f Moved more of http logic into http manager 2025-02-09 02:04:14 +01:00
ee080b6987 added https package 2025-02-09 02:01:06 +01:00
67021f9ada SSL config 2025-02-09 02:00:59 +01:00
3270ea8729 Updated .env.example 2025-02-09 01:22:34 +01:00
04710edb73 Removed docker files 2025-02-09 01:20:59 +01:00
d398764b6d P 2025-02-09 01:17:55 +01:00
be479b11c5 ? 2025-02-09 01:15:38 +01:00
45964ba7f3 bug 2025-02-09 01:12:20 +01:00
e7e187da7c Attempt 9999 2025-02-08 23:50:43 +01:00
64c0d82d48 Reverted to working state of docker compose 2025-02-08 23:01:48 +01:00
12292ea4f2 i hate docker 2025-02-08 15:23:38 +01:00
0c8df9d175 toml > yml 2025-02-08 15:21:55 +01:00
86cbe3fdcb y 2025-02-08 15:19:07 +01:00
62e31c10d7 y 2025-02-08 15:12:04 +01:00
72 changed files with 460 additions and 456 deletions

View File

@ -25,4 +25,8 @@ DEFAULT_CHARACTER_POS_Y="0"
SMTP_HOST=my.directonline.io
SMTP_PORT=587
SMTP_USER=no-reply@noxious.gg
SMTP_PASSWORD=""
SMTP_PASSWORD=""
# SSL
#PUBLIC_KEY_PATH=
#PRIVATE_KEY_PATH=

View File

@ -1,16 +0,0 @@
FROM node:lts-alpine
# Install packages
RUN apk update
RUN apk add --no-cache tmux coreutils
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
COPY . .
# Modify CMD to use tmux
CMD npx mikro-orm-esm migration:up && npm run start

View File

@ -5,7 +5,7 @@ This is the server for the Noxious game.
## Projects requirements
- NodeJS 20.x or higher
- MySQL 8.x or higher
- MariaDB 11.x or higher
- Redis 7.x or higher
## Installation

View File

@ -1,94 +0,0 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "${PORT}:${PORT}"
environment:
- ENV=${ENV}
- HOST=${HOST}
- PORT=${PORT}
- JWT_SECRET=${JWT_SECRET}
- CLIENT_URL=${CLIENT_URL}
- REDIS_URL=${REDIS_URL}
- DB_HOST=${DB_HOST}
- DB_USER=${DB_USER}
- DB_PASS=${DB_PASS}
- DB_PORT=${DB_PORT}
- DB_NAME=${DB_NAME}
- ALLOW_DIAGONAL_MOVEMENT=${ALLOW_DIAGONAL_MOVEMENT}
- DEFAULT_CHARACTER_ZONE=${DEFAULT_CHARACTER_ZONE}
- DEFAULT_CHARACTER_POS_X=${DEFAULT_CHARACTER_POS_X}
- DEFAULT_CHARACTER_POS_Y=${DEFAULT_CHARACTER_POS_Y}
- SMTP_HOST=${SMTP_HOST}
- SMTP_PORT=${SMTP_PORT}
- SMTP_USER=${SMTP_USER}
- SMTP_PASSWORD=${SMTP_PASSWORD}
volumes:
- app-public:/user/src/app/public
- app-logs:/user/src/app/logs
depends_on:
- mariadb
- redis
restart: unless-stopped
networks:
- app-network
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`${HOST}`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=le"
- "traefik.http.services.app.loadbalancer.server.port=${PORT}"
- "traefik.http.routers.app.middlewares=websocket"
traefik:
image: traefik:v3.3.3
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- traefik_data:/data
- ./etc/traefik/traefik.toml:/etc/traefik/traefik.toml
restart: unless-stopped
networks:
- app-network
mariadb:
image: mariadb:lts
environment:
- MARIADB_USER=${DB_USER}
- MARIADB_PASSWORD=${DB_PASS}
- MARIADB_DATABASE=${DB_NAME}
- MARIADB_RANDOM_ROOT_PASSWORD=yes
volumes:
- mariadb-data:/var/lib/mysql
ports:
- "${DB_PORT}:3306"
restart: unless-stopped
networks:
- app-network
command: [ 'mariadbd', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci' ]
redis:
image: redis:7.4.2-alpine
command: redis-server --appendonly yes
volumes:
- redis-data:/data
ports:
- "6379:6379"
restart: unless-stopped
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
app-public:
app-logs:
mariadb-data:
redis-data:
traefik_data:

View File

@ -1,59 +0,0 @@
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.web.http.redirections.entryPoint]
to = "websecure"
scheme = "https"
[entryPoints.websecure]
address = ":443"
[providers.docker]
endpoint = "unix:///var/run/docker.sock"
exposedByDefault = false
[certificatesResolvers.le.acme]
email = "your-email@example.com"
storage = "/data/acme.json"
[certificatesResolvers.le.acme.tlsChallenge]
[api]
dashboard = true
[ping] # Health check
entryPoint = "websecure"
[http.routers.api]
rule = "PathPrefix(`/api`)"
service = "api"
entryPoints = ["websecure"]
[http.services.api.loadBalancer]
[[http.services.api.loadBalancer.servers]]
url = "http://app:${PORT}"
# Added for websocket
[http.services.app.loadBalancer]
sticky = true
[[http.services.app.loadBalancer.servers]]
url = "http://app:${PORT}"
# Added for websocket
[http.routers.app]
rule = "Host(`${HOST}`)"
entrypoints = ["websecure"]
service = "app"
[http.routers.app.tls]
certresolver = "le"
[http.routers.app.middlewares]
# Enable websockets
- "websocket"
[http.middlewares]
[http.middlewares.websocket.headers]
accessControlAllowHeaders = ["Origin", "Content-Type", "Accept", "Authorization"]
accessControlAllowMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]
accessControlAllowOrigin = ["*"]
accessControlExposeHeaders = ["Content-Length", "Content-Range"]

160
package-lock.json generated
View File

@ -13,10 +13,12 @@
"@mikro-orm/reflection": "^6.4.2",
"@types/ioredis": "^4.28.10",
"bcryptjs": "^2.4.3",
"bufferutil": "^4.0.9",
"bullmq": "^5.13.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"https": "^1.0.0",
"ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.15",
@ -26,6 +28,7 @@
"socket.io": "^4.7.5",
"ts-node": "^10.9.2",
"typescript": "^5.5.3",
"utf-8-validate": "^6.0.5",
"zod": "^3.23.8"
},
"devDependencies": {
@ -1753,17 +1756,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz",
"integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==",
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz",
"integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.23.0",
"@typescript-eslint/type-utils": "8.23.0",
"@typescript-eslint/utils": "8.23.0",
"@typescript-eslint/visitor-keys": "8.23.0",
"@typescript-eslint/scope-manager": "8.24.0",
"@typescript-eslint/type-utils": "8.24.0",
"@typescript-eslint/utils": "8.24.0",
"@typescript-eslint/visitor-keys": "8.24.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@ -1783,16 +1786,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz",
"integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==",
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz",
"integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.23.0",
"@typescript-eslint/types": "8.23.0",
"@typescript-eslint/typescript-estree": "8.23.0",
"@typescript-eslint/visitor-keys": "8.23.0",
"@typescript-eslint/scope-manager": "8.24.0",
"@typescript-eslint/types": "8.24.0",
"@typescript-eslint/typescript-estree": "8.24.0",
"@typescript-eslint/visitor-keys": "8.24.0",
"debug": "^4.3.4"
},
"engines": {
@ -1808,14 +1811,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz",
"integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==",
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz",
"integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.23.0",
"@typescript-eslint/visitor-keys": "8.23.0"
"@typescript-eslint/types": "8.24.0",
"@typescript-eslint/visitor-keys": "8.24.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1826,14 +1829,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz",
"integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==",
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz",
"integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.23.0",
"@typescript-eslint/utils": "8.23.0",
"@typescript-eslint/typescript-estree": "8.24.0",
"@typescript-eslint/utils": "8.24.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.0.1"
},
@ -1850,9 +1853,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz",
"integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==",
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz",
"integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==",
"dev": true,
"license": "MIT",
"engines": {
@ -1864,14 +1867,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz",
"integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==",
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz",
"integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.23.0",
"@typescript-eslint/visitor-keys": "8.23.0",
"@typescript-eslint/types": "8.24.0",
"@typescript-eslint/visitor-keys": "8.24.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -1891,16 +1894,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz",
"integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==",
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz",
"integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.23.0",
"@typescript-eslint/types": "8.23.0",
"@typescript-eslint/typescript-estree": "8.23.0"
"@typescript-eslint/scope-manager": "8.24.0",
"@typescript-eslint/types": "8.24.0",
"@typescript-eslint/typescript-estree": "8.24.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1915,13 +1918,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz",
"integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==",
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz",
"integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.23.0",
"@typescript-eslint/types": "8.24.0",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
@ -2377,6 +2380,19 @@
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/bufferutil": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz",
"integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/bullmq": {
"version": "5.40.2",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.40.2.tgz",
@ -3210,9 +3226,9 @@
}
},
"node_modules/eslint": {
"version": "9.20.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.0.tgz",
"integrity": "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==",
"version": "9.20.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz",
"integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3863,9 +3879,9 @@
"license": "ISC"
},
"node_modules/for-each": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz",
"integrity": "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==",
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4261,6 +4277,12 @@
"node": ">= 0.8"
}
},
"node_modules/https": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==",
"license": "ISC"
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -5108,9 +5130,9 @@
"license": "MIT"
},
"node_modules/long": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz",
"integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.0.tgz",
"integrity": "sha512-5vvY5yF1zF/kXk+L94FRiTDa1Znom46UjPCH6/XbSvS8zBKMFBHTJk8KDMqJ+2J6QezQFi7k1k8v21ClJYHPaw==",
"license": "Apache-2.0"
},
"node_modules/lru-cache": {
@ -5428,6 +5450,17 @@
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
"license": "MIT"
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-gyp-build-optional-packages": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
@ -5894,9 +5927,9 @@
}
},
"node_modules/prettier": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz",
"integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==",
"dev": true,
"license": "MIT",
"bin": {
@ -7063,9 +7096,9 @@
}
},
"node_modules/type-fest": {
"version": "4.33.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.33.0.tgz",
"integrity": "sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g==",
"version": "4.34.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.34.1.tgz",
"integrity": "sha512-6kSc32kT0rbwxD6QL1CYe8IqdzN/J/ILMrNK+HMQCKH3insCDRY/3ITb0vcBss0a3t72fzh2YSzj8ko1HgwT3g==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
@ -7253,6 +7286,19 @@
"punycode": "^2.1.0"
}
},
"node_modules/utf-8-validate": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.5.tgz",
"integrity": "sha512-EYZR+OpIXp9Y1eG1iueg8KRsY8TuT8VNgnanZ0uA3STqhHQTLwbl+WX76/9X5OY12yQubymBpaBSmMPkSTQcKA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View File

@ -23,18 +23,20 @@
"#events/*": "./src/events/*.js"
},
"dependencies": {
"@mikro-orm/cli": "^6.4.2",
"@mikro-orm/core": "^6.4.2",
"@mikro-orm/mariadb": "^6.4.2",
"@mikro-orm/migrations": "^6.4.2",
"@mikro-orm/mysql": "^6.4.2",
"@mikro-orm/reflection": "^6.4.2",
"@mikro-orm/cli": "^6.4.2",
"@types/ioredis": "^4.28.10",
"bcryptjs": "^2.4.3",
"bufferutil": "^4.0.9",
"bullmq": "^5.13.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"https": "^1.0.0",
"ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.15",
@ -44,6 +46,7 @@
"socket.io": "^4.7.5",
"ts-node": "^10.9.2",
"typescript": "^5.5.3",
"utf-8-validate": "^6.0.5",
"zod": "^3.23.8"
},
"devDependencies": {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { Server } from 'socket.io'
import type { TSocket } from '#application/types'
@ -8,12 +9,26 @@ import CharacterRepository from '#repositories/characterRepository'
export abstract class BaseEvent {
protected readonly logger = Logger.type(LoggerType.GAME)
private lastActionTimes: Map<string, number> = new Map()
constructor(
readonly io: Server,
readonly socket: TSocket
) {}
protected isThrottled(actionId: string, throttleTime: number): boolean {
const now = Date.now()
const lastActionTime = this.lastActionTimes.get(actionId) || 0
if (now - lastActionTime < throttleTime) {
return true
}
this.lastActionTimes.set(actionId, now)
return false
}
protected async getCharacter(): Promise<Character | null> {
const characterRepository = new CharacterRepository()
return characterRepository.getById(this.socket.characterId!)
@ -25,14 +40,14 @@ export abstract class BaseEvent {
}
protected emitError(message: string): void {
this.socket.emit('notification', { title: 'Server message', message })
this.socket.emit(SocketEvent.NOTIFICATION, { title: 'Server message', message })
this.logger.error('Base event error', `Player ${this.socket.userId}: ${message}`)
}
protected handleError(context: string, error: unknown): void {
console.log(error)
const errorMessage = error instanceof Error ? error.message : error && typeof error === 'object' && 'toString' in error ? error.toString() : String(error)
this.socket.emit('notification', { title: 'Server message', message: `Server error occured. Please contact the server administrator.` })
this.socket.emit(SocketEvent.NOTIFICATION, { title: 'Server message', message: `Server error occured. Please contact the server administrator.` })
this.logger.error('Base event error', errorMessage)
}
}

View File

@ -31,6 +31,10 @@ class config {
static SMTP_PORT: number = process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT) : 587
static SMTP_USER: string = process.env.SMTP_USER || 'no-reply@noxious.gg'
static SMTP_PASSWORD: string = process.env.SMTP_PASSWORD || 'password'
// SSL
static PUBLIC_KEY_PATH: string = process.env.PUBLIC_KEY_PATH || ''
static PRIVATE_KEY_PATH: string = process.env.PRIVATE_KEY_PATH || ''
}
export default config

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import * as readline from 'readline'
export class ConsolePrompt {
@ -10,7 +11,7 @@ export class ConsolePrompt {
output: process.stdout
})
this.rl.on('close', () => {
this.rl.on(SocketEvent.CLOSE, () => {
this.isClosed = true
})
}

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import * as fs from 'fs'
import * as path from 'path'
@ -60,7 +61,7 @@ export class LogReader {
end: newPosition
})
stream.on('data', (data) => {
stream.on(SocketEvent.DATA, (data) => {
console.log(`[${filename}]`)
console.log(data.toString()) //
})

View File

@ -1,4 +1,5 @@
import { MikroORM } from '@mikro-orm/mysql'
import { MikroORM } from '@mikro-orm/mariadb'
// import { MikroORM } from '@mikro-orm/mysql'
import Logger, { LoggerType } from '#application/logger'
import config from '#root/mikro-orm.config'

View File

@ -1,17 +1,57 @@
export enum SocketEvent {
CHARACTER_CONNECT = 1,
CHARACTER_MOVE = 2,
CHARACTER_MOVE_ERROR = 3,
CHARACTER_TELEPORT = 4,
ZONE_CHARACTER_LEAVE = 5,
ZONE_CHARACTER_JOIN = 6,
ZONE_CHARACTER_LIST = 7,
ZONE_CHARACTER_DELETE = 8,
ZONE_CHARACTER_CREATE = 9,
ZONE_CHARACTER_UPDATE = 10,
ZONE_CHARACTER_HAIR_UPDATE = 11,
ZONE_CHARACTER_HAIR_LIST = 12,
ZONE_CHARACTER_TELEPORT = 13
CLOSE = '52',
DATA = '51',
CHARACTER_CONNECT = '50',
CHARACTER_CREATE = '49',
CHARACTER_DELETE = '48',
CHARACTER_LIST = '47',
GM_CHARACTERHAIR_CREATE = '46',
GM_CHARACTERHAIR_REMOVE = '45',
GM_CHARACTERHAIR_LIST = '44',
GM_CHARACTERHAIR_UPDATE = '43',
GM_CHARACTERTYPE_CREATE = '42',
GM_CHARACTERTYPE_REMOVE = '41',
GM_CHARACTERTYPE_LIST = '40',
GM_CHARACTERTYPE_UPDATE = '39',
GM_ITEM_CREATE = '38',
GM_ITEM_REMOVE = '37',
GM_ITEM_LIST = '36',
GM_ITEM_UPDATE = '35',
GM_MAPOBJECT_LIST = '34',
GM_MAPOBJECT_REMOVE = '33',
GM_MAPOBJECT_UPDATE = '32',
GM_MAPOBJECT_UPLOAD = '31',
GM_SPRITE_COPY = '30',
GM_SPRITE_CREATE = '29',
GM_SPRITE_DELETE = '28',
GM_SPRITE_LIST = '27',
GM_SPRITE_UPDATE = '26',
GM_TILE_DELETE = '25',
GM_TILE_LIST = '24',
GM_TILE_UPDATE = '23',
GM_TILE_UPLOAD = '22',
GM_MAP_CREATE = '21',
GM_MAP_DELETE = '20',
GM_MAP_REQUEST = '19',
GM_MAP_UPDATE = '18',
MAP_CHARACTER_MOVEERROR = '17',
DISCONNECT = '16',
USER_DISCONNECT = '15',
LOGIN = '14',
LOGGED_IN = '13',
NOTIFICATION = '12',
DATE = '11',
FAILED = '10',
COMPLETED = '9',
CONNECTION = '8',
WEATHER = '7',
CHARACTER_DISCONNECT = '6',
MAP_CHARACTER_ATTACK = '5',
MAP_CHARACTER_TELEPORT = '4',
MAP_CHARACTER_JOIN = '3',
MAP_CHARACTER_LEAVE = '2',
MAP_CHARACTER_MOVE = '1',
CHAT_MESSAGE = '0',
}
export enum ItemType {

View File

@ -9,7 +9,7 @@ class Storage {
constructor() {
this.rootDir = process.cwd()
this.baseDir = config.ENV === 'development' ? 'src' : 'dist'
this.baseDir = config.ENV === 'development' ? 'src' : 'src'
}
/**

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { Server } from 'socket.io'
import { BaseCommand } from '#application/base/baseCommand'
@ -8,6 +9,6 @@ export default class AlertCommand extends BaseCommand {
public execute(input: CommandInput): void {
const message: string = input.join(' ') ?? null
if (!message) return console.log('message is required')
this.io.emit('notification', { message: message })
this.io.emit(SocketEvent.NOTIFICATION, { message: message })
}
}

View File

@ -1,4 +1,4 @@
import fs from 'fs'
wimport fs from 'fs'
import sharp from 'sharp'

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -16,7 +17,7 @@ export default class CharacterConnectEvent extends BaseEvent {
private readonly characterRepository = new CharacterRepository()
public listen(): void {
this.socket.on('character:connect', this.handleEvent.bind(this))
this.socket.on(SocketEvent.CHARACTER_CONNECT, this.handleEvent.bind(this))
}
private async handleEvent(data: CharacterConnectPayload, callback: (response: any) => void): Promise<void> {

View File

@ -1,4 +1,5 @@
import { ZodError } from 'zod'
import { SocketEvent } from '#application/enums';
import { ZodError, z } from 'zod'
import { BaseEvent } from '#application/base/baseEvent'
import { ZCharacterCreate } from '#application/zodTypes'
@ -8,65 +9,77 @@ import CharacterTypeRepository from '#repositories/characterTypeRepository'
import MapRepository from '#repositories/mapRepository'
import UserRepository from '#repositories/userRepository'
const MAX_CHARACTERS = 4
export default class CharacterCreateEvent extends BaseEvent {
private readonly userRepository: UserRepository = new UserRepository()
private readonly characterRepository: CharacterRepository = new CharacterRepository()
private readonly characterTypeRepository: CharacterTypeRepository = new CharacterTypeRepository()
private readonly mapRepository: MapRepository = new MapRepository()
public listen(): void {
this.socket.on('character:create', this.handleEvent.bind(this))
this.socket.on(SocketEvent.CHARACTER_CREATE, this.handleEvent.bind(this))
}
private async handleEvent(data: any): Promise<any> {
// zod validate
private async handleEvent(data: z.infer<typeof ZCharacterCreate>, callback: (success: boolean) => void): Promise<void> {
try {
data = ZCharacterCreate.parse(data)
const userRepository = new UserRepository()
const characterRepository = new CharacterRepository()
const characterTypeRepository = new CharacterTypeRepository()
const mapRepository = new MapRepository()
const user = await userRepository.getById(this.socket.userId!)
if (!user) {
return this.socket.emit('notification', { title: 'Error', message: 'You are not logged in' })
}
// Check if character name already exists
const characterExists = await characterRepository.getByName(data.name)
if (characterExists) {
return this.socket.emit('notification', { title: 'Error', message: 'Character name already exists' })
}
let characters: Character[] = await characterRepository.getByUserId(user.getId())
if (characters.length >= 4) {
return this.socket.emit('notification', { title: 'Error', message: 'You can only create 4 characters' })
}
// @TODO: Change to default location
const map = await mapRepository.getFirst()
// @TODO: Change to selected character type
const characterType = await characterTypeRepository.getFirst()
const newCharacter = new Character()
await newCharacter.setName(data.name).setUser(user).setMap(map!).setCharacterType(characterType).save()
if (!newCharacter) {
return this.socket.emit('notification', { title: 'Error', message: 'Failed to create character. Please try again (later).' })
}
characters = [...characters, newCharacter]
this.socket.emit('character:create:success')
this.socket.emit('character:list', characters)
this.logger.info('character:create success')
} catch (error: any) {
this.logger.error(`character:create error: ${error.message}`)
if (error instanceof ZodError) {
return this.socket.emit('notification', { title: 'Error', message: error.issues[0]!.message })
}
return this.socket.emit('notification', { title: 'Error', message: 'Could not create character. Please try again (later).' })
const validatedData = ZCharacterCreate.parse(data)
await this.createCharacter(validatedData)
callback(true)
} catch (error: unknown) {
this.returnError(error)
callback(false)
}
}
private async createCharacter(data: z.infer<typeof ZCharacterCreate>): Promise<void> {
const user = await this.userRepository.getById(this.socket.userId!)
if (!user) {
throw new Error('You are not logged in')
}
const characterExists = await this.characterRepository.getByName(data.name)
if (characterExists) {
throw new Error('Character name already exists')
}
let characters = await this.characterRepository.getByUserId(user.getId())
if (characters.length >= MAX_CHARACTERS) {
throw new Error(`You can only create ${MAX_CHARACTERS} characters`)
}
const map = await this.mapRepository.getFirst()
if (!map) {
throw new Error('No default map found')
}
const characterType = await this.characterTypeRepository.getFirst()
if (!characterType) {
throw new Error('No character type found')
}
const newCharacter = new Character()
await newCharacter.setName(data.name).setUser(user).setMap(map).setCharacterType(characterType).save()
characters = await this.characterRepository.getByUserId(user.getId())
this.socket.emit(SocketEvent.CHARACTER_LIST, characters)
this.logger.info('character:create success')
}
private returnError(error: unknown): void {
this.logger.error(`character:create error: ${error instanceof Error ? error.message : 'Unknown error'}`)
let errorMessage = 'Could not create character. Please try again later.'
if (error instanceof ZodError) {
errorMessage = error.issues[0]?.message || errorMessage
} else if (error instanceof Error) {
errorMessage = error.message
}
this.socket.emit(SocketEvent.NOTIFICATION, {
title: 'Error',
message: errorMessage
})
}
}

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -14,7 +15,7 @@ type TypeResponse = {
export default class CharacterDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on('character:delete', this.handleEvent.bind(this))
this.socket.on(SocketEvent.CHARACTER_DELETE, this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: TypeResponse) => void): Promise<any> {
@ -23,9 +24,9 @@ export default class CharacterDeleteEvent extends BaseEvent {
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)
this.socket.emit(SocketEvent.CHARACTER_LIST, characters)
} catch (error: any) {
return this.socket.emit('notification', { message: 'Character delete failed. Please try again.' })
return this.socket.emit(SocketEvent.NOTIFICATION, { message: 'Character delete failed. Please try again.' })
}
}
}

View File

@ -1,10 +1,11 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import { Character } from '#entities/character'
import CharacterRepository from '#repositories/characterRepository'
export default class CharacterListEvent extends BaseEvent {
public listen(): void {
this.socket.on('character:list', this.handleEvent.bind(this))
this.socket.on(SocketEvent.CHARACTER_LIST, this.handleEvent.bind(this))
}
private async handleEvent(data: any): Promise<void> {
@ -12,7 +13,7 @@ export default class CharacterListEvent extends BaseEvent {
const characterRepository = new CharacterRepository()
let characters: Character[] = await characterRepository.getByUserId(this.socket.userId!)
this.socket.emit('character:list', characters)
this.socket.emit(SocketEvent.CHARACTER_LIST, characters)
} catch (error: any) {
this.logger.error('character:list error', error.message)
}

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import CharacterRepository from '#repositories/characterRepository'
import ChatService from '#services/chatService'
@ -8,7 +9,7 @@ type TypePayload = {
export default class AlertCommandEvent extends BaseEvent {
public listen(): void {
this.socket.on('chat:message', this.handleEvent.bind(this))
this.socket.on(SocketEvent.CHAT_MESSAGE, this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {
@ -25,7 +26,7 @@ export default class AlertCommandEvent extends BaseEvent {
return callback(false)
}
this.io.emit('notification', { title: 'Message from GM', message: args.join(' ') })
this.io.emit(SocketEvent.NOTIFICATION, { title: 'Message from GM', message: args.join(' ') })
return callback(true)
} catch (error: any) {
this.logger.error('chat:alert_command error', error.message)

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import DateManager from '#managers/dateManager'
import CharacterRepository from '#repositories/characterRepository'
@ -9,7 +10,7 @@ type TypePayload = {
export default class SetTimeCommand extends BaseEvent {
public listen(): void {
this.socket.on('chat:message', this.handleEvent.bind(this))
this.socket.on(SocketEvent.CHAT_MESSAGE, this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -12,7 +13,7 @@ type TypePayload = {
export default class TeleportCommandEvent extends BaseEvent {
public listen(): void {
this.socket.on('chat:message', this.handleEvent.bind(this))
this.socket.on(SocketEvent.CHAT_MESSAGE, this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void) {
@ -29,7 +30,7 @@ export default class TeleportCommandEvent extends BaseEvent {
const args = ChatService.getArgs('teleport', data.message)
if (!args || args.length === 0 || args.length > 3) {
this.socket.emit('notification', {
this.socket.emit(SocketEvent.NOTIFICATION, {
title: 'Server message',
message: 'Usage: /teleport <mapId> [x] [y]'
})
@ -41,7 +42,7 @@ export default class TeleportCommandEvent extends BaseEvent {
const targetY = args[2] ? parseInt(args[2], 10) : 0
if (!mapId || isNaN(targetX) || isNaN(targetY)) {
this.socket.emit('notification', {
this.socket.emit(SocketEvent.NOTIFICATION, {
title: 'Server message',
message: 'Invalid parameters. X and Y coordinates must be numbers.'
})
@ -51,7 +52,7 @@ export default class TeleportCommandEvent extends BaseEvent {
const mapRepository = new MapRepository()
const map = await mapRepository.getById(mapId)
if (!map) {
this.socket.emit('notification', {
this.socket.emit(SocketEvent.NOTIFICATION, {
title: 'Server message',
message: 'Map not found'
})
@ -59,7 +60,7 @@ export default class TeleportCommandEvent extends BaseEvent {
}
if (character.map.id === map.id && targetX === character.positionX && targetY === character.positionY) {
this.socket.emit('notification', {
this.socket.emit(SocketEvent.NOTIFICATION, {
title: 'Server message',
message: 'You are already at that location'
})
@ -74,20 +75,20 @@ export default class TeleportCommandEvent extends BaseEvent {
})
if (!success) {
return this.socket.emit('notification', {
return this.socket.emit(SocketEvent.NOTIFICATION, {
title: 'Server message',
message: 'Failed to teleport'
})
}
this.socket.emit('notification', {
this.socket.emit(SocketEvent.NOTIFICATION, {
title: 'Server message',
message: `Teleported to ${map.name} (${targetX}, ${targetY})`
})
this.logger.info('teleport', `Character ${character.id} teleported to map ${map.id} at position (${targetX}, ${targetY})`)
} catch (error: any) {
this.logger.error(`Error in teleport command: ${error.message}`)
this.socket.emit('notification', {
this.socket.emit(SocketEvent.NOTIFICATION, {
title: 'Server message',
message: 'An error occurred while teleporting'
})

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import WeatherManager from '#managers/weatherManager'
import ChatService from '#services/chatService'
@ -8,7 +9,7 @@ type TypePayload = {
export default class ToggleFogCommand extends BaseEvent {
public listen(): void {
this.socket.on('chat:message', this.handleEvent.bind(this))
this.socket.on(SocketEvent.CHAT_MESSAGE, this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import WeatherManager from '#managers/weatherManager'
import ChatService from '#services/chatService'
@ -8,7 +9,7 @@ type TypePayload = {
export default class ToggleRainCommand extends BaseEvent {
public listen(): void {
this.socket.on('chat:message', this.handleEvent.bind(this))
this.socket.on(SocketEvent.CHAT_MESSAGE, this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import MapManager from '#managers/mapManager'
import MapRepository from '#repositories/mapRepository'
@ -9,7 +10,7 @@ type TypePayload = {
export default class ChatMessageEvent extends BaseEvent {
public listen(): void {
this.socket.on('chat:message', this.handleEvent.bind(this))
this.socket.on(SocketEvent.CHAT_MESSAGE, this.handleEvent.bind(this))
}
private async handleEvent(data: TypePayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,9 +1,10 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import MapManager from '#managers/mapManager'
export default class DisconnectEvent extends BaseEvent {
public listen(): void {
this.socket.on('disconnect', this.handleEvent.bind(this))
this.socket.on(SocketEvent.DISCONNECT, this.handleEvent.bind(this))
}
private async handleEvent(): Promise<void> {
@ -13,7 +14,7 @@ export default class DisconnectEvent extends BaseEvent {
return
}
this.io.emit('user:disconnect', this.socket.userId)
this.io.emit(SocketEvent.USER_DISCONNECT, this.socket.userId)
const mapCharacter = MapManager.getCharacterById(this.socket.characterId!)
if (!mapCharacter) {

View File

@ -1,9 +1,10 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterHair } from '#entities/characterHair'
export default class CharacterHairCreateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:characterHair:create', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_CHARACTERHAIR_CREATE, this.handleEvent.bind(this))
}
private async handleEvent(data: undefined, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -9,7 +10,7 @@ interface IPayload {
export default class CharacterHairDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:characterHair:remove', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_CHARACTERHAIR_REMOVE, this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterHair } from '#entities/characterHair'
import CharacterHairRepository from '#repositories/characterHairRepository'
@ -6,7 +7,7 @@ interface IPayload {}
export default class characterHairListEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:characterHair:list', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_CHARACTERHAIR_LIST, this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: CharacterHair[]) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -15,7 +16,7 @@ type Payload = {
export default class CharacterHairUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:characterHair:update', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_CHARACTERHAIR_UPDATE, this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {

View File

@ -1,9 +1,10 @@
import { SocketEvent } from '#application/enums';
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))
this.socket.on(SocketEvent.GM_CHARACTERTYPE_CREATE, this.handleEvent.bind(this))
}
private async handleEvent(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -9,7 +10,7 @@ interface IPayload {
export default class CharacterTypeDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:characterType:remove', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_CHARACTERTYPE_REMOVE, this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import { CharacterType } from '#entities/characterType'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
@ -6,7 +7,7 @@ interface IPayload {}
export default class CharacterTypeListEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:characterType:list', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_CHARACTERTYPE_LIST, this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: CharacterType[]) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -16,7 +17,7 @@ type Payload = {
export default class CharacterTypeUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:characterType:update', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_CHARACTERTYPE_UPDATE, this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import { ItemRarity, ItemType } from '#application/enums'
import { Item } from '#entities/item'
@ -5,7 +6,7 @@ import SpriteRepository from '#repositories/spriteRepository'
export default class ItemCreateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:item:create', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_ITEM_CREATE, this.handleEvent.bind(this))
}
private async handleEvent(data: undefined, callback: (response: boolean, item?: any) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -9,7 +10,7 @@ interface IPayload {
export default class ItemDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:item:remove', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_ITEM_REMOVE, this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import { Item } from '#entities/item'
import ItemRepository from '#repositories/itemRepository'
@ -6,7 +7,7 @@ interface IPayload {}
export default class ItemListEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:item:list', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_ITEM_LIST, this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Item[]) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -17,7 +18,7 @@ type Payload = {
export default class ItemUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:item:update', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_ITEM_UPDATE, this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import { MapObject } from '#entities/mapObject'
import MapObjectRepository from '#repositories/mapObjectRepository'
@ -6,7 +7,7 @@ interface IPayload {}
export default class MapObjectListEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:mapObject:list', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_MAPOBJECT_LIST, this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: MapObject[]) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import fs from 'fs'
import type { UUID } from '#application/types'
@ -12,7 +13,7 @@ interface IPayload {
export default class MapObjectRemoveEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:mapObject:remove', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_MAPOBJECT_REMOVE, this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -16,7 +17,7 @@ type Payload = {
export default class MapObjectUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:mapObject:update', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_MAPOBJECT_UPDATE, this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {
@ -40,7 +41,7 @@ export default class MapObjectUpdateEvent extends BaseEvent {
return callback(true)
} catch (error) {
this.socket.emit('notification', { title: 'Error', message: 'Failed to update mapObject.' })
this.socket.emit(SocketEvent.NOTIFICATION, { title: 'Error', message: 'Failed to update mapObject.' })
return callback(false)
}
}

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import fs from 'fs/promises'
import { writeFile } from 'node:fs/promises'
@ -13,7 +14,7 @@ interface IObjectData {
export default class MapObjectUploadEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:mapObject:upload', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_MAPOBJECT_UPLOAD, this.handleEvent.bind(this))
}
private async handleEvent(data: IObjectData, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -10,7 +11,7 @@ interface CopyPayload {
export default class SpriteCopyEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:sprite:copy', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_SPRITE_COPY, this.handleEvent.bind(this))
}
private async handleEvent(payload: CopyPayload, callback: (success: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import fs from 'fs/promises'
import { BaseEvent } from '#application/base/baseEvent'
@ -6,7 +7,7 @@ import { Sprite } from '#entities/sprite'
export default class SpriteCreateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:sprite:create', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_SPRITE_CREATE, this.handleEvent.bind(this))
}
private async handleEvent(data: undefined, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import fs from 'fs'
import type { UUID } from '#application/types'
@ -12,7 +13,7 @@ type Payload = {
export default class GMSpriteDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:sprite:delete', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_SPRITE_DELETE, this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import { Sprite } from '#entities/sprite'
import SpriteRepository from '#repositories/spriteRepository'
@ -6,7 +7,7 @@ interface IPayload {}
export default class SpriteListEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:sprite:list', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_SPRITE_LIST, this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Sprite[]) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import fs from 'fs'
import sharp from 'sharp'
@ -44,7 +45,7 @@ type Payload = {
export default class SpriteUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:sprite:update', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_SPRITE_UPDATE, this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import fs from 'fs/promises'
import type { UUID } from '#application/types'
@ -12,7 +13,7 @@ type Payload = {
export default class GMTileDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:tile:delete', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_TILE_DELETE, this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import { Tile } from '#entities/tile'
import TileRepository from '#repositories/tileRepository'
@ -6,7 +7,7 @@ interface IPayload {}
export default class TileListEven extends BaseEvent {
public listen(): void {
this.socket.on('gm:tile:list', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_TILE_LIST, this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Tile[]) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -11,7 +12,7 @@ type Payload = {
export default class TileUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:tile:update', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_TILE_UPDATE, this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (success: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import fs from 'fs/promises'
import { writeFile } from 'node:fs/promises'
@ -11,7 +12,7 @@ interface ITileData {
export default class TileUploadEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:tile:upload', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_TILE_UPLOAD, this.handleEvent.bind(this))
}
private async handleEvent(data: ITileData, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { MapCacheT } from '#entities/map'
import { BaseEvent } from '#application/base/baseEvent'
@ -11,7 +12,7 @@ type Payload = {
export default class MapCreateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:map:create', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_MAP_CREATE, this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (response: MapCacheT | false) => void): Promise<void> {
@ -21,12 +22,12 @@ export default class MapCreateEvent extends BaseEvent {
this.logger.info(`GM ${(await this.getCharacter())!.getId()} has created a new map via map editor.`)
if (data.name === '') {
this.socket.emit('notification', { title: 'Error', message: 'Map name cannot be empty.' })
this.socket.emit(SocketEvent.NOTIFICATION, { title: 'Error', message: 'Map name cannot be empty.' })
return callback(false)
}
if (data.width < 1 || data.height < 1) {
this.socket.emit('notification', { title: 'Error', message: 'Map width and height must be greater than 0.' })
this.socket.emit(SocketEvent.NOTIFICATION, { title: 'Error', message: 'Map width and height must be greater than 0.' })
return callback(false)
}
@ -41,7 +42,7 @@ export default class MapCreateEvent extends BaseEvent {
return callback(await map.cache())
} catch (error: any) {
this.logger.error('gm:map:create error', error.message)
this.socket.emit('notification', { message: 'Failed to create map.' })
this.socket.emit(SocketEvent.NOTIFICATION, { message: 'Failed to create map.' })
return callback(false)
}
}

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -9,7 +10,7 @@ type Payload = {
export default class MapDeleteEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:map:delete', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_MAP_DELETE, this.handleEvent.bind(this))
}
private async handleEvent(data: Payload, callback: (response: boolean) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -10,7 +11,7 @@ interface IPayload {
export default class MapRequestEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:map:request', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_MAP_REQUEST, this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Map | null) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -34,7 +35,7 @@ interface IPayload {
export default class MapUpdateEvent extends BaseEvent {
public listen(): void {
this.socket.on('gm:map:update', this.handleEvent.bind(this))
this.socket.on(SocketEvent.GM_MAP_UPDATE, this.handleEvent.bind(this))
}
private async handleEvent(data: IPayload, callback: (response: Map | null) => void): Promise<void> {

View File

@ -1,9 +1,10 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import UserRepository from '#repositories/userRepository'
export default class LoginEvent extends BaseEvent {
public listen(): void {
this.socket.on('login', this.handleEvent.bind(this))
this.socket.on(SocketEvent.LOGIN, this.handleEvent.bind(this))
}
private async handleEvent() {
@ -14,7 +15,7 @@ export default class LoginEvent extends BaseEvent {
}
const userRepository = new UserRepository()
this.socket.emit('logged_in', { user: userRepository.getById(this.socket.userId) })
this.socket.emit(SocketEvent.LOGGED_IN, { user: userRepository.getById(this.socket.userId) })
this.logger.info(`User logged in: ${this.socket.userId}`)
} catch (error: any) {
this.logger.error('login error: ' + error.message)

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import CharacterAttackService from '#services/characterAttackService'
@ -5,7 +6,7 @@ export default class CharacterMove extends BaseEvent {
private readonly characterAttackService = CharacterAttackService
public listen(): void {
this.socket.on('map:character:attack', this.handleEvent.bind(this))
this.socket.on(SocketEvent.MAP_CHARACTER_ATTACK, this.handleEvent.bind(this))
}
private async handleEvent(data: any, callback: (response: any) => void): Promise<void> {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { MapEventTileWithTeleport } from '#application/types'
import { BaseEvent } from '#application/base/baseEvent'
@ -9,11 +10,10 @@ import TeleportService from '#services/characterTeleportService'
export default class CharacterMove extends BaseEvent {
private readonly characterService = CharacterService
private readonly MOVEMENT_CANCEL_DELAY = 250
private movementTimeouts: Map<string, NodeJS.Timeout> = new Map()
private readonly MOVEMENT_THROTTLE = 230 // Minimum time between movement requests
public listen(): void {
this.socket.on('map:character:move', this.handleEvent.bind(this))
this.socket.on(SocketEvent.MAP_CHARACTER_MOVE, this.handleEvent.bind(this))
}
private async handleEvent({ positionX, positionY }: { positionX: number; positionY: number }): Promise<void> {
@ -23,23 +23,22 @@ export default class CharacterMove extends BaseEvent {
return
}
// Clear any existing movement timeout
const existingTimeout = this.movementTimeouts.get(this.socket.characterId!)
if (existingTimeout) {
clearTimeout(existingTimeout)
this.movementTimeouts.delete(this.socket.characterId!)
if (this.isThrottled(`movement_${this.socket.characterId}`, this.MOVEMENT_THROTTLE)) {
// Only cancel current movement if the new target is different
if (mapCharacter.isMoving &&
(Math.floor(positionX) !== Math.floor(mapCharacter.character.positionX) ||
Math.floor(positionY) !== Math.floor(mapCharacter.character.positionY))) {
mapCharacter.isMoving = false
mapCharacter.currentPath = null
// this.finalizeMovement(mapCharacter)
}
return
}
// If already moving, cancel current movement
if (mapCharacter.isMoving) {
if (mapCharacter.isMoving && mapCharacter.currentPath && mapCharacter.currentPath.length > 2) {
mapCharacter.isMoving = false
mapCharacter.currentPath = null
// Add small delay before starting new movement
await new Promise((resolve) => {
const timeout = setTimeout(resolve, this.MOVEMENT_CANCEL_DELAY)
this.movementTimeouts.set(this.socket.characterId!, timeout)
})
}
// Validate target position is within reasonable range
@ -47,15 +46,9 @@ export default class CharacterMove extends BaseEvent {
const currentY = mapCharacter.character.positionY
const distance = Math.sqrt(Math.pow(positionX - currentX, 2) + Math.pow(positionY - currentY, 2))
if (distance > 20) {
// Maximum allowed distance
this.io.in(mapCharacter.character.map.id).emit('map:character:moveError', 'Target position too far')
return
}
const path = await this.characterService.calculatePath(mapCharacter.character, positionX, positionY)
if (!path?.length) {
this.io.in(mapCharacter.character.map.id).emit('map:character:moveError', 'No valid path found')
this.io.in(mapCharacter.character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVEERROR, 'No valid path found')
return
}
@ -81,6 +74,10 @@ export default class CharacterMove extends BaseEvent {
break
}
if (i !== 0) {
await this.characterService.applyMovementDelay()
}
// Validate each step
if (Math.abs(end.positionX - start.positionX) > 1 || Math.abs(end.positionY - start.positionY) > 1) {
this.logger.error('Invalid path step detected')
@ -102,7 +99,7 @@ export default class CharacterMove extends BaseEvent {
character.setPositionX(end.positionX).setPositionY(end.positionY)
// Then emit with the same properties
this.io.in(character.map.id).emit('map:character:move', {
this.io.in(character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, {
characterId: character.id,
positionX: character.getPositionX(),
positionY: character.getPositionY(),
@ -132,7 +129,7 @@ export default class CharacterMove extends BaseEvent {
private finalizeMovement(mapCharacter: MapCharacter): void {
mapCharacter.isMoving = false
this.io.in(mapCharacter.character.map.id).emit('map:character:move', {
this.io.in(mapCharacter.character.map.id).emit(SocketEvent.MAP_CHARACTER_MOVE, {
characterId: mapCharacter.character.id,
positionX: mapCharacter.character.positionX,
positionY: mapCharacter.character.positionY,

View File

@ -1,15 +1,16 @@
import { SocketEvent } from '#application/enums';
import { BaseEvent } from '#application/base/baseEvent'
import WeatherManager from '#managers/weatherManager'
export default class Weather extends BaseEvent {
public listen(): void {
this.socket.on('weather', this.handleEvent.bind(this))
this.socket.on(SocketEvent.WEATHER, this.handleEvent.bind(this))
}
private async handleEvent(): Promise<void> {
try {
const weather = WeatherManager.getWeatherState()
this.socket.emit('weather', weather)
this.socket.emit(SocketEvent.WEATHER, weather)
} catch (error: any) {
this.logger.error('weather error: ' + error.message)
}

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { Server as SocketServer } from 'socket.io'
import type { TSocket } from '#application/types'
@ -8,9 +9,9 @@ export default class SomeJob {
async execute(io: SocketServer, socket?: TSocket) {
// Handle the event
if (socket) {
socket.emit('notification', { message: 'Something happened with socket' })
socket.emit(SocketEvent.NOTIFICATION, { message: 'Something happened with socket' })
}
// Use io for broadcasting if needed
io.emit('notification', { message: 'Something happened' })
io.emit(SocketEvent.NOTIFICATION, { message: 'Something happened' })
}
}

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { Server } from 'socket.io'
import Logger, { LoggerType } from '#application/logger'
@ -8,16 +9,14 @@ import WorldRepository from '#repositories/worldRepository'
class DateManager {
private static readonly CONFIG = {
GAME_SPEED: 8, // 24 game hours / 3 real hours
UPDATE_INTERVAL: 1000 // 1 second
UPDATE_INTERVAL: 1000 // 1 minute
} as const
private io: Server | null = null
private intervalId: NodeJS.Timeout | null = null
private currentDate = new Date()
private readonly logger = Logger.type(LoggerType.APP)
private currentDate = new Date()
private intervalId: NodeJS.Timeout | null = null
public async boot(): Promise<void> {
this.io = SocketManager.getIO()
await this.loadDate()
this.startDateLoop()
this.logger.info('Date manager loaded')
@ -85,7 +84,8 @@ class DateManager {
}
private emitDate(): void {
this.io?.emit('date', this.currentDate)
const io = SocketManager.getIO()
io?.emit(SocketEvent.DATE, this.currentDate)
}
private async saveDate(): Promise<void> {

View File

@ -1,8 +1,12 @@
import cors from 'cors'
import fs from 'fs'
import { createServer as httpServer, Server as HTTPServer } from 'http'
import { createServer as httpsServer, Server as HTTPSServer } from 'https'
import type { Application } from 'express'
import cors from 'cors'
import express, { type Application } from 'express'
import config from '#application/config'
import Logger, { LoggerType } from '#application/logger.js'
import { AuthController } from '#controllers/auth'
import { AvatarController } from '#controllers/avatar'
import { CacheController } from '#controllers/cache'
@ -12,16 +16,40 @@ import { TexturesController } from '#controllers/textures'
* HTTP manager
*/
class HttpManager {
private readonly app: Application
private readonly server: HTTPServer | HTTPSServer
private readonly logger = Logger.type(LoggerType.APP)
private readonly authController: AuthController = new AuthController()
private readonly avatarController: AvatarController = new AvatarController()
private readonly texturesController: TexturesController = new TexturesController()
private readonly cacheController: CacheController = new CacheController()
constructor() {
this.app = express()
this.app.use(cors())
this.app.use(express.json())
this.app.use(express.urlencoded({ extended: true }))
if (config.PUBLIC_KEY_PATH && config.PRIVATE_KEY_PATH) {
const credentials = {
key: fs.readFileSync(config.PRIVATE_KEY_PATH),
cert: fs.readFileSync(config.PUBLIC_KEY_PATH)
}
this.server = httpsServer(credentials, this.app)
this.logger.info('HTTPS server initialized')
} else {
this.server = httpServer(this.app)
this.logger.info('HTTP server initialized')
}
}
/**
* Initialize HTTP manager
* @param app
*/
public async boot(app: Application) {
this.server.listen(config.PORT, config.HOST)
// Add CORS middleware
app.use(
cors({
@ -34,6 +62,8 @@ class HttpManager {
// Add routes
await this.addRoutes(app)
this.logger.info(`HTTP running on port ${config.PORT}`)
}
private async addRoutes(app: Application) {
@ -58,6 +88,14 @@ class HttpManager {
app.get('/cache/character_types', (req, res) => this.cacheController.characterTypes(req, res))
app.get('/cache/character_hair', (req, res) => this.cacheController.characterHair(req, res))
}
getAppInstance(): Application {
return this.app
}
getServerInstance(): HTTPServer | HTTPSServer {
return this.server
}
}
export default new HttpManager()

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { Server } from 'socket.io'
import Logger, { LoggerType } from '#application/logger'
@ -20,7 +21,6 @@ class WeatherManager {
} as const
private readonly logger = Logger.type(LoggerType.APP)
private io: Server | null = null
private intervalId: NodeJS.Timeout | null = null
private weatherState: WeatherState = {
@ -29,7 +29,6 @@ class WeatherManager {
}
public async boot(): Promise<void> {
this.io = SocketManager.getIO()
await this.loadWeather()
this.startWeatherLoop()
this.logger.info('Weather manager loaded')
@ -113,7 +112,8 @@ class WeatherManager {
}
private emitWeather(): void {
this.io?.emit('weather', this.weatherState)
const io = SocketManager.getIO()
io?.emit(SocketEvent.WEATHER, this.weatherState)
}
private async saveWeather(): Promise<void> {

View File

@ -1,6 +1,6 @@
// import { defineConfig, MariaDbDriver } from '@mikro-orm/mariadb'
import { defineConfig, MariaDbDriver } from '@mikro-orm/mariadb'
// import { defineConfig, MySqlDriver } from '@mikro-orm/mysql'
import { Migrator } from '@mikro-orm/migrations'
import { defineConfig, MySqlDriver } from '@mikro-orm/mysql'
import { TsMorphMetadataProvider } from '@mikro-orm/reflection'
import serverConfig from '#application/config'
@ -10,7 +10,7 @@ export default defineConfig({
metadataProvider: TsMorphMetadataProvider,
entities: ['./dist/entities/*.js'],
entitiesTs: ['./src/entities/*.ts'],
driver: MySqlDriver,
driver: MariaDbDriver,
host: serverConfig.DB_HOST,
port: serverConfig.DB_PORT,
user: serverConfig.DB_USER,

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import { Server } from 'socket.io'
import type { TSocket, UUID } from '#application/types'
@ -44,11 +45,11 @@ class MapCharacter {
MapManager.removeCharacter(this.character.id)
// Notify map players
io.in(this.character.map.id).emit('map:character:leave', this.character.id)
io.in(this.character.map.id).emit(SocketEvent.MAP_CHARACTER_LEAVE, this.character.id)
}
// Notify all players
io.emit('character:disconnect', this.character.id)
io.emit(SocketEvent.CHARACTER_DISCONNECT, this.character.id)
} catch (error) {
console.error(`Error disconnecting character ${this.character.id}:`, error)
}

View File

@ -1,12 +1,4 @@
import 'reflect-metadata'
import { createServer as httpServer, Server as HTTPServer } from 'http'
import cors from 'cors'
import express from 'express'
import type { Application } from 'express'
import config from '#application/config'
import Database from '#application/database'
import Logger, { LoggerType } from '#application/logger'
import ConsoleManager from '#managers/consoleManager'
@ -19,31 +11,17 @@ import UserManager from '#managers/userManager'
import WeatherManager from '#managers/weatherManager'
export class Server {
private readonly app: Application
private readonly http: HTTPServer
private readonly logger = Logger.type(LoggerType.APP)
constructor() {
this.app = express()
this.app.use(cors())
this.app.use(express.json())
this.app.use(express.urlencoded({ extended: true }))
this.http = httpServer(this.app)
}
public async start(): Promise<void> {
try {
// Initialize database
await Database.initialize()
// Start HTTP server
this.http.listen(config.PORT, config.HOST)
this.logger.info(`Server running on port ${config.PORT}`)
// Initialize managers
await Promise.all([
HttpManager.boot(this.app),
SocketManager.boot(this.app, this.http),
HttpManager.boot(HttpManager.getAppInstance()),
SocketManager.boot(HttpManager.getAppInstance(), HttpManager.getServerInstance()),
QueueManager.boot(),
UserManager.boot(),
MapManager.boot(),
@ -52,6 +30,7 @@ export class Server {
ConsoleManager.boot()
])
} catch (error: any) {
console.error(error)
this.logger.error(`Server failed to start: ${error.message}`)
process.exit(1)
}

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseService } from '#application/base/baseService'
@ -27,7 +28,7 @@ class CharacterAttackService extends BaseService {
}
// Emit attack event
io.in(character.character.map.id).emit('map:character:attack', character.character.id)
io.in(character.character.map.id).emit(SocketEvent.MAP_CHARACTER_ATTACK, character.character.id)
return true
}
}

View File

@ -7,8 +7,7 @@ type Position = { positionX: number; positionY: number }
export type Node = Position & { parent?: Node; g: number; h: number; f: number }
class CharacterMoveService extends BaseService {
private readonly MOVEMENT_DELAY_MS = 200
private readonly MAX_PATH_LENGTH = 20 // Limit maximum path length
private readonly MOVEMENT_DELAY_MS = 90
private readonly DIRECTIONS = [
{ x: 0, y: -1 }, // Up
@ -46,21 +45,7 @@ class CharacterMoveService extends BaseService {
return null
}
// Add maximum distance check
const directDistance = Math.sqrt(Math.pow(targetX - character.positionX, 2) + Math.pow(targetY - character.positionY, 2))
if (directDistance > this.MAX_PATH_LENGTH) {
return null
}
const path = this.findPath(start, end, grid)
// Validate path length
if (path.length > this.MAX_PATH_LENGTH) {
return path.slice(0, this.MAX_PATH_LENGTH)
}
return path
return this.findPath(start, end, grid)
}
public calculateRotation(X1: number, Y1: number, X2: number, Y2: number): number {

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import Logger, { LoggerType } from '#application/logger'
@ -61,7 +62,7 @@ class CharacterTeleportService {
// If the current map is the target map and we are not joining, send move event
if (currentMapId === options.targetMapId && !options.isInitialJoin) {
// If the current map is the target map, send move event
io.in(currentMapId).emit('map:character:move', {
io.in(currentMapId).emit(SocketEvent.MAP_CHARACTER_MOVE, {
characterId: mapCharacter.character.id,
positionX: options.targetX,
positionY: options.targetY,
@ -75,7 +76,7 @@ class CharacterTeleportService {
if (currentMapId) {
socket.leave(currentMapId)
MapManager.removeCharacter(characterId)
io.in(currentMapId).emit('map:character:leave', characterId)
io.in(currentMapId).emit(SocketEvent.MAP_CHARACTER_LEAVE, characterId)
}
// Join new map
@ -86,8 +87,8 @@ class CharacterTeleportService {
await mapRepository.getEntityManager().populate(map!, mapRepository.POPULATE_TELEPORT as any)
// Notify clients
io.in(options.targetMapId).emit('map:character:join', mapCharacter)
socket.emit('map:character:teleport', {
io.in(options.targetMapId).emit(SocketEvent.MAP_CHARACTER_JOIN, mapCharacter)
socket.emit(SocketEvent.MAP_CHARACTER_TELEPORT, {
mapId: options.targetMapId,
characters: targetMap.getCharactersInMap()
})

View File

@ -1,3 +1,4 @@
import { SocketEvent } from '#application/enums';
import type { UUID } from '#application/types'
import { BaseService } from '#application/base/baseService'
@ -22,7 +23,7 @@ class ChatService extends BaseService {
await chat.setCharacter(character).setMap(map).setMessage(message).save()
const io = SocketManager.getIO()
io.to(mapId).emit('chat:message', chat)
io.to(mapId).emit(SocketEvent.CHAT_MESSAGE, chat)
return true
} catch (error: any) {
this.logger.error(`Failed to save chat message: ${error instanceof Error ? error.message : String(error)}`)

View File

@ -20,8 +20,6 @@
"module": "NodeNext",
"baseUrl": "./src",
"rootDir": "./src",
"outDir": "./dist",
"sourceMap": true,
"paths": {
"#root/*": ["./*"],