forked from noxious/server
Compare commits
65 Commits
how-i-see-
...
feature/#1
Author | SHA1 | Date | |
---|---|---|---|
27d8c7cff6 | |||
b9a7f9aa8e | |||
5b6b968541 | |||
93abf4b631 | |||
0de574b9e1 | |||
c04c52aed0 | |||
3f19730bd8 | |||
d0e3c95bb0 | |||
82f51b2b7e | |||
1b9db64854 | |||
41c71d5964 | |||
bd04dc2ab8 | |||
a4e96f9ede | |||
f6bac403a2 | |||
8460d0b535 | |||
5a36d10f0e | |||
8f8f019ab7 | |||
6a1823586a | |||
9d08073fa8 | |||
5631930bf5 | |||
b6e7a5d7fe | |||
63804336be | |||
0b62b4231b | |||
4e1e7d95ac | |||
d29420cbf3 | |||
acc04daa27 | |||
8abf5acef3 | |||
780cac9644 | |||
44481e19a8 | |||
9075bfaad5 | |||
bfd941c091 | |||
bb9f62a9c8 | |||
049b9de2b3 | |||
2008646a3f | |||
075592702c | |||
d271efc1ec | |||
297d4742a4 | |||
ab649b9fa1 | |||
4f643269eb | |||
ce1708a55e | |||
4cbd62cbb0 | |||
7b3c4b92a5 | |||
da8ef9fa65 | |||
4f9a1bc879 | |||
3638e2a793 | |||
6ac827630a | |||
3ec4bc2557 | |||
6a286590b4 | |||
34ed2ba7cb | |||
72159cdc17 | |||
70d8c43350 | |||
3a83f2c1ff | |||
ddeee356b4 | |||
e9fb277d63 | |||
e8aee51248 | |||
46fdb3edb6 | |||
dec6b36699 | |||
21a75f6cbe | |||
10a231b54c | |||
cc9eada654 | |||
6f057639c0 | |||
251a72aa97 | |||
0d7ed18b03 | |||
4a9b7987dc | |||
a729371741 |
@ -12,3 +12,9 @@ ALLOW_DIAGONAL_MOVEMENT=false
|
||||
DEFAULT_CHARACTER_ZONE="0"
|
||||
DEFAULT_CHARACTER_POS_X="0"
|
||||
DEFAULT_CHARACTER_POS_Y="0"
|
||||
|
||||
# SMTP configuration
|
||||
SMTP_HOST="my.directonline.io"
|
||||
SMTP_PORT="587"
|
||||
SMTP_USER="no-reply@sylvan.quest"
|
||||
SMTP_PASSWORD="Z%kI*1xe67WuGg"
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -310,6 +310,3 @@ $RECYCLE.BIN/
|
||||
*.lnk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node,jetbrains+all,visualstudiocode,macos,windows
|
||||
|
||||
prisma/dev.db
|
||||
prisma/dev.db-journal
|
10
Dockerfile
10
Dockerfile
@ -1,8 +1,8 @@
|
||||
# Use the official Node.js 22.4.1 image
|
||||
FROM node:22.4.1-alpine
|
||||
|
||||
# Install Redis
|
||||
RUN apk add --no-cache redis
|
||||
# Install Redis and tmux
|
||||
RUN apk add --no-cache redis tmux
|
||||
|
||||
# Set the working directory in the container
|
||||
WORKDIR /usr/src/
|
||||
@ -28,11 +28,13 @@ RUN npm run build
|
||||
# Expose the ports your Node.js application and Redis will listen on
|
||||
EXPOSE 80 6379
|
||||
|
||||
# Create a shell script to run Redis, run migrations, and start the application
|
||||
# Create a shell script to run Redis, run migrations, and start the application in a tmux session
|
||||
RUN echo '#!/bin/sh' > /usr/src/start.sh && \
|
||||
echo 'redis-server --daemonize yes' >> /usr/src/start.sh && \
|
||||
echo 'npx prisma migrate deploy' >> /usr/src/start.sh && \
|
||||
echo 'node dist/server.js' >> /usr/src/start.sh && \
|
||||
echo 'tmux new-session -d -s nodeapp "node dist/server.js"' >> /usr/src/start.sh && \
|
||||
echo 'echo "App is running in tmux session. Attach with: tmux attach-session -t nodeapp"' >> /usr/src/start.sh && \
|
||||
echo 'tail -f /dev/null' >> /usr/src/start.sh && \
|
||||
chmod +x /usr/src/start.sh
|
||||
|
||||
# Use the shell script as the entry point
|
||||
|
312
package-lock.json
generated
312
package-lock.json
generated
@ -14,6 +14,7 @@
|
||||
"express": "^4.19.2",
|
||||
"ioredis": "^5.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nodemailer": "^6.9.15",
|
||||
"pino": "^9.3.2",
|
||||
"prisma": "^5.17.0",
|
||||
"sharp": "^0.33.4",
|
||||
@ -27,6 +28,7 @@
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"nodemon": "^3.1.4",
|
||||
"prettier": "^3.3.3"
|
||||
}
|
||||
@ -44,9 +46,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
|
||||
"integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==",
|
||||
"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": {
|
||||
@ -524,9 +526,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "5.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.19.1.tgz",
|
||||
"integrity": "sha512-x30GFguInsgt+4z5I4WbkZP2CGpotJMUXy+Gl/aaUjHn2o1DnLYNTA+q9XdYmAQZM8fIIkvUiA2NpgosM3fneg==",
|
||||
"version": "5.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.21.1.tgz",
|
||||
"integrity": "sha512-3n+GgbAZYjaS/k0M03yQsQfR1APbr411r74foknnsGpmhNKBG49VuUkxIU6jORgvJPChoD4WC4PqoHImN1FP0w==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@ -542,48 +544,48 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "5.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.19.1.tgz",
|
||||
"integrity": "sha512-lAG6A6QnG2AskAukIEucYJZxxcSqKsMK74ZFVfCTOM/7UiyJQi48v6TQ47d6qKG3LbMslqOvnTX25dj/qvclGg==",
|
||||
"version": "5.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.21.1.tgz",
|
||||
"integrity": "sha512-uY8SAhcnORhvgtOrNdvWS98Aq/nkQ9QDUxrWAgW8XrCZaI3j2X7zb7Xe6GQSh6xSesKffFbFlkw0c2luHQviZA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "5.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.19.1.tgz",
|
||||
"integrity": "sha512-kR/PoxZDrfUmbbXqqb8SlBBgCjvGaJYMCOe189PEYzq9rKqitQ2fvT/VJ8PDSe8tTNxhc2KzsCfCAL+Iwm/7Cg==",
|
||||
"version": "5.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.21.1.tgz",
|
||||
"integrity": "sha512-hGVTldUkIkTwoV8//hmnAAiAchi4oMEKD3aW5H2RrnI50tTdwza7VQbTTAyN3OIHWlK5DVg6xV7X8N/9dtOydA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.19.1",
|
||||
"@prisma/engines-version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3",
|
||||
"@prisma/fetch-engine": "5.19.1",
|
||||
"@prisma/get-platform": "5.19.1"
|
||||
"@prisma/debug": "5.21.1",
|
||||
"@prisma/engines-version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36",
|
||||
"@prisma/fetch-engine": "5.21.1",
|
||||
"@prisma/get-platform": "5.21.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3.tgz",
|
||||
"integrity": "sha512-xR6rt+z5LnNqTP5BBc+8+ySgf4WNMimOKXRn6xfNRDSpHvbOEmd7+qAOmzCrddEc4Cp8nFC0txU14dstjH7FXA==",
|
||||
"version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36.tgz",
|
||||
"integrity": "sha512-qvnEflL0//lh44S/T9NcvTMxfyowNeUxTunPcDfKPjyJNrCNf2F1zQLcUv5UHAruECpX+zz21CzsC7V2xAeM7Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "5.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.19.1.tgz",
|
||||
"integrity": "sha512-pCq74rtlOVJfn4pLmdJj+eI4P7w2dugOnnTXpRilP/6n5b2aZiA4ulJlE0ddCbTPkfHmOL9BfaRgA8o+1rfdHw==",
|
||||
"version": "5.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.21.1.tgz",
|
||||
"integrity": "sha512-70S31vgpCGcp9J+mh/wHtLCkVezLUqe/fGWk3J3JWZIN7prdYSlr1C0niaWUyNK2VflLXYi8kMjAmSxUVq6WGQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.19.1",
|
||||
"@prisma/engines-version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3",
|
||||
"@prisma/get-platform": "5.19.1"
|
||||
"@prisma/debug": "5.21.1",
|
||||
"@prisma/engines-version": "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36",
|
||||
"@prisma/get-platform": "5.21.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "5.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.19.1.tgz",
|
||||
"integrity": "sha512-sCeoJ+7yt0UjnR+AXZL7vXlg5eNxaFOwC23h0KvW1YIXUoa7+W2ZcAUhoEQBmJTW4GrFqCuZ8YSP0mkDa4k3Zg==",
|
||||
"version": "5.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.21.1.tgz",
|
||||
"integrity": "sha512-sRxjL3Igst3ct+e8ya/x//cDXmpLbZQ5vfps2N4tWl4VGKQAmym77C/IG/psSMsQKszc8uFC/q1dgmKFLUgXZQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.19.1"
|
||||
"@prisma/debug": "5.21.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
@ -673,9 +675,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express-serve-static-core": {
|
||||
"version": "4.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz",
|
||||
"integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==",
|
||||
"version": "4.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
|
||||
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -719,14 +721,24 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.16.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
|
||||
"integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==",
|
||||
"version": "20.17.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.5.tgz",
|
||||
"integrity": "sha512-n8FYY/pRxu496441gIcAQFZPKXbhsd6VZygcq+PTSZ75eMh/Ke0hCAROdUa21qiFqKNsPPYic46yXDO1JGiPBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nodemailer": {
|
||||
"version": "6.4.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.16.tgz",
|
||||
"integrity": "sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
|
||||
@ -764,18 +776,6 @@
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@ -790,9 +790,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@ -855,26 +855,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
@ -951,30 +931,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
@ -982,9 +938,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/bullmq": {
|
||||
"version": "5.13.2",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.13.2.tgz",
|
||||
"integrity": "sha512-McGE8k3mrCvdUHdU0sHkTKDS1xr4pff+hbEKBY51wk5S6Za0gkuejYA620VQTo3Zz37E/NVWMgumwiXPQ3yZcA==",
|
||||
"version": "5.23.0",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.23.0.tgz",
|
||||
"integrity": "sha512-VILKTIOwo9AopMyVqvDhQ1qyLrOtBSfu+G2bntgauQfxYzT7ETj+h2HeUe7a9i9AU/+OXJGYYm49NHJedEz7VQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron-parser": "^4.6.0",
|
||||
@ -1128,9 +1084,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 +1238,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 +1248,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 +1268,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"
|
||||
@ -1379,28 +1335,10 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"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 +1346,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",
|
||||
@ -1650,26 +1588,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ignore-by-default": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||
@ -1989,9 +1907,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/msgpackr": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.0.tgz",
|
||||
"integrity": "sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw==",
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz",
|
||||
"integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"msgpackr-extract": "^3.0.2"
|
||||
@ -2049,6 +1967,15 @@
|
||||
"node-gyp-build-optional-packages-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "6.9.16",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz",
|
||||
"integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz",
|
||||
@ -2184,15 +2111,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pino": {
|
||||
"version": "9.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-9.4.0.tgz",
|
||||
"integrity": "sha512-nbkQb5+9YPhQRz/BeQmrWpEknAaqjpAqRK8NwJpmrX/JHu7JuZC5G1CeAwJDJfGes4h+YihC6in3Q2nGb+Y09w==",
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz",
|
||||
"integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"atomic-sleep": "^1.0.0",
|
||||
"fast-redact": "^3.1.1",
|
||||
"on-exit-leak-free": "^2.1.0",
|
||||
"pino-abstract-transport": "^1.2.0",
|
||||
"pino-abstract-transport": "^2.0.0",
|
||||
"pino-std-serializers": "^7.0.0",
|
||||
"process-warning": "^4.0.0",
|
||||
"quick-format-unescaped": "^4.0.3",
|
||||
@ -2206,12 +2133,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pino-abstract-transport": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz",
|
||||
"integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
|
||||
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readable-stream": "^4.0.0",
|
||||
"split2": "^4.0.0"
|
||||
}
|
||||
},
|
||||
@ -2238,13 +2164,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "5.19.1",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.19.1.tgz",
|
||||
"integrity": "sha512-c5K9MiDaa+VAAyh1OiYk76PXOme9s3E992D7kvvIOhCrNsBQfy2mP2QAQtX0WNj140IgG++12kwZpYB9iIydNQ==",
|
||||
"version": "5.21.1",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.21.1.tgz",
|
||||
"integrity": "sha512-PB+Iqzld/uQBPaaw2UVIk84kb0ITsLajzsxzsadxxl54eaU5Gyl2/L02ysivHxK89t7YrfQJm+Ggk37uvM70oQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/engines": "5.19.1"
|
||||
"@prisma/engines": "5.21.1"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
@ -2256,15 +2182,6 @@
|
||||
"fsevents": "2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz",
|
||||
@ -2336,22 +2253,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
|
||||
"integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@ -2599,9 +2500,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz",
|
||||
"integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==",
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
@ -2709,9 +2610,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sonic-boom": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.1.0.tgz",
|
||||
"integrity": "sha512-NGipjjRicyJJ03rPiZCJYjwlsuP2d1/5QUviozRXC7S3WdVWNK5e3Ojieb9CCyfhq2UC+3+SRd9nG3I2lPRvUw==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
|
||||
"integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"atomic-sleep": "^1.0.0"
|
||||
@ -2741,15 +2642,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
@ -2848,9 +2740,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
@ -2867,9 +2759,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",
|
||||
|
@ -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/"
|
||||
},
|
||||
@ -15,6 +15,7 @@
|
||||
"express": "^4.19.2",
|
||||
"ioredis": "^5.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nodemailer": "^6.9.15",
|
||||
"pino": "^9.3.2",
|
||||
"prisma": "^5.17.0",
|
||||
"sharp": "^0.33.4",
|
||||
@ -28,6 +29,7 @@
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"nodemon": "^3.1.4",
|
||||
"prettier": "^3.3.3"
|
||||
}
|
||||
|
0
prisma/.gitignore
vendored
Normal file
0
prisma/.gitignore
vendored
Normal file
@ -40,10 +40,23 @@ CREATE TABLE `SpriteAction` (
|
||||
CREATE TABLE `User` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`username` VARCHAR(191) NOT NULL,
|
||||
`email` VARCHAR(191) NOT NULL,
|
||||
`password` VARCHAR(191) NOT NULL,
|
||||
`online` BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
UNIQUE INDEX `User_username_key`(`username`),
|
||||
UNIQUE INDEX `User_email_key`(`email`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `PasswordResetToken` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`userId` INTEGER NOT NULL,
|
||||
`token` VARCHAR(191) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
UNIQUE INDEX `PasswordResetToken_token_key`(`token`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
@ -53,7 +66,7 @@ CREATE TABLE `CharacterType` (
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`gender` ENUM('MALE', 'FEMALE') NOT NULL,
|
||||
`race` ENUM('HUMAN', 'ELF', 'DWARF', 'ORC', 'GOBLIN') NOT NULL,
|
||||
`spriteId` VARCHAR(191) NOT NULL,
|
||||
`spriteId` VARCHAR(191) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
|
||||
@ -146,12 +159,23 @@ CREATE TABLE `Zone` (
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `ZoneEffect` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`zoneId` INTEGER NOT NULL,
|
||||
`effect` VARCHAR(191) NOT NULL,
|
||||
`strength` INTEGER NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `ZoneObject` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`zoneId` INTEGER NOT NULL,
|
||||
`objectId` VARCHAR(191) NOT NULL,
|
||||
`depth` INTEGER NOT NULL DEFAULT 0,
|
||||
`isRotated` BOOLEAN NOT NULL DEFAULT false,
|
||||
`positionX` INTEGER NOT NULL DEFAULT 0,
|
||||
`positionY` INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
@ -174,6 +198,7 @@ CREATE TABLE `ZoneEventTileTeleport` (
|
||||
`id` VARCHAR(191) NOT NULL,
|
||||
`zoneEventTileId` VARCHAR(191) NOT NULL,
|
||||
`toZoneId` INTEGER NOT NULL,
|
||||
`toRotation` INTEGER NOT NULL,
|
||||
`toPositionX` INTEGER NOT NULL,
|
||||
`toPositionY` INTEGER NOT NULL,
|
||||
|
||||
@ -190,6 +215,9 @@ ALTER TABLE `Chat` ADD CONSTRAINT `Chat_zoneId_fkey` FOREIGN KEY (`zoneId`) REFE
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `SpriteAction` ADD CONSTRAINT `SpriteAction_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `PasswordResetToken` ADD CONSTRAINT `PasswordResetToken_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CharacterType` ADD CONSTRAINT `CharacterType_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
@ -208,6 +236,9 @@ ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_characterId_fkey` FORE
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CharacterItem` ADD CONSTRAINT `CharacterItem_itemId_fkey` FOREIGN KEY (`itemId`) REFERENCES `Item`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `ZoneEffect` ADD CONSTRAINT `ZoneEffect_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `ZoneObject` ADD CONSTRAINT `ZoneObject_zoneId_fkey` FOREIGN KEY (`zoneId`) REFERENCES `Zone`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
@ -1,9 +1,19 @@
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
email String @unique
|
||||
password String
|
||||
online Boolean @default(false)
|
||||
characters Character[]
|
||||
passwordResetTokens PasswordResetToken[]
|
||||
}
|
||||
|
||||
model PasswordResetToken {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
token String @unique
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
enum CharacterGender {
|
||||
@ -25,8 +35,8 @@ model CharacterType {
|
||||
gender CharacterGender
|
||||
race CharacterRace
|
||||
characters Character[]
|
||||
spriteId String
|
||||
sprite Sprite @relation(fields: [spriteId], references: [id], onDelete: Cascade)
|
||||
spriteId String?
|
||||
sprite Sprite? @relation(fields: [spriteId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ model Zone {
|
||||
height Int @default(10)
|
||||
tiles Json?
|
||||
pvp Boolean @default(false)
|
||||
zoneEffects 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
|
||||
@ -54,6 +63,7 @@ model ZoneObject {
|
||||
objectId String
|
||||
object Object @relation(fields: [objectId], references: [id], onDelete: Cascade)
|
||||
depth Int @default(0)
|
||||
isRotated Boolean @default(false)
|
||||
positionX Int @default(0)
|
||||
positionY Int @default(0)
|
||||
}
|
||||
@ -81,6 +91,7 @@ model ZoneEventTileTeleport {
|
||||
zoneEventTile ZoneEventTile @relation(fields: [zoneEventTileId], references: [id], onDelete: Cascade)
|
||||
toZoneId Int
|
||||
toZone Zone @relation(fields: [toZoneId], references: [id], onDelete: Cascade)
|
||||
toRotation Int
|
||||
toPositionX Int
|
||||
toPositionY Int
|
||||
}
|
||||
|
@ -2,8 +2,12 @@ import { Server } from 'socket.io'
|
||||
|
||||
type CommandInput = string[]
|
||||
|
||||
export default function (input: CommandInput, io: Server) {
|
||||
export default class AlertCommand {
|
||||
constructor(private readonly io: Server) {}
|
||||
|
||||
public execute(input: CommandInput): void {
|
||||
const message: string = input.join(' ') ?? null
|
||||
if (!message) return console.log('message is required')
|
||||
io.emit('notification', { message: message })
|
||||
this.io.emit('notification', { message: message })
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,10 @@ import ZoneManager from '../managers/zoneManager'
|
||||
|
||||
type CommandInput = string[]
|
||||
|
||||
export default function (input: CommandInput, io: Server) {
|
||||
export default class ListZonesCommand {
|
||||
constructor(private readonly io: Server) {}
|
||||
|
||||
public execute(input: CommandInput): void {
|
||||
console.log(ZoneManager.getLoadedZones())
|
||||
}
|
||||
}
|
||||
|
58
src/commands/tiles.ts
Normal file
58
src/commands/tiles.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import fs from 'fs'
|
||||
import sharp from 'sharp'
|
||||
import { commandLogger } from '../utilities/logger'
|
||||
import { Server } from 'socket.io'
|
||||
import { getPublicPath } from '../utilities/storage'
|
||||
import path from 'path'
|
||||
|
||||
export default class TilesCommand {
|
||||
constructor(private readonly io: Server) {}
|
||||
|
||||
public async execute(): Promise<void> {
|
||||
// Get all tiles
|
||||
const tilesDir = getPublicPath('tiles')
|
||||
const tiles = fs.readdirSync(tilesDir).filter((file) => file.endsWith('.png'))
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
if (!fs.existsSync(tilesDir)) {
|
||||
fs.mkdirSync(tilesDir, { recursive: true })
|
||||
}
|
||||
|
||||
for (const tile of tiles) {
|
||||
// Check if tile is already 66x34
|
||||
const metadata = await sharp(getPublicPath('tiles', tile)).metadata()
|
||||
if (metadata.width === 66 && metadata.height === 34) {
|
||||
commandLogger.info(`Tile ${tile} already processed`)
|
||||
continue
|
||||
}
|
||||
|
||||
const inputPath = getPublicPath('tiles', tile)
|
||||
const tempPath = getPublicPath('tiles', `temp_${tile}`)
|
||||
|
||||
try {
|
||||
await sharp(inputPath)
|
||||
.resize({
|
||||
width: 66,
|
||||
height: 34,
|
||||
fit: 'fill',
|
||||
kernel: 'nearest'
|
||||
})
|
||||
.toFile(tempPath)
|
||||
|
||||
// Replace original file with processed file
|
||||
fs.unlinkSync(inputPath)
|
||||
fs.renameSync(tempPath, inputPath)
|
||||
|
||||
commandLogger.info(`Processed and replaced: ${tile}`)
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${tile}:`, error)
|
||||
// Clean up temp file if it exists
|
||||
if (fs.existsSync(tempPath)) {
|
||||
fs.unlinkSync(tempPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commandLogger.info('Tile processing completed.')
|
||||
}
|
||||
}
|
@ -2,9 +2,11 @@ import * as readline from 'readline'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { Server } from 'socket.io'
|
||||
import { commandLogger } from '../utilities/logger'
|
||||
import { getAppPath } from '../utilities/storage'
|
||||
|
||||
class CommandManager {
|
||||
private commands: Map<string, Function> = new Map()
|
||||
private commands: Map<string, any> = new Map()
|
||||
private rl: readline.Interface
|
||||
private io: Server | null = null
|
||||
private rlClosed: boolean = false
|
||||
@ -23,7 +25,7 @@ class CommandManager {
|
||||
public async boot(io: Server) {
|
||||
this.io = io
|
||||
await this.loadCommands()
|
||||
console.log('[✅] Command manager loaded')
|
||||
commandLogger.info('Command manager loaded')
|
||||
this.startPrompt()
|
||||
}
|
||||
|
||||
@ -39,7 +41,9 @@ class CommandManager {
|
||||
private async processCommand(command: string): Promise<void> {
|
||||
const [cmd, ...args] = command.trim().split(' ')
|
||||
if (this.commands.has(cmd)) {
|
||||
this.commands.get(cmd)?.(args, this.io as Server)
|
||||
const CommandClass = this.commands.get(cmd)
|
||||
const commandInstance = new CommandClass(this.io as Server)
|
||||
await commandInstance.execute(args)
|
||||
} else {
|
||||
this.handleUnknownCommand(cmd)
|
||||
}
|
||||
@ -48,7 +52,6 @@ class CommandManager {
|
||||
private handleUnknownCommand(command: string) {
|
||||
switch (command) {
|
||||
case 'exit':
|
||||
console.log('Goodbye!')
|
||||
this.rl.close()
|
||||
break
|
||||
default:
|
||||
@ -58,37 +61,43 @@ class CommandManager {
|
||||
}
|
||||
|
||||
private async loadCommands() {
|
||||
const commandsDir = path.resolve(__dirname, 'commands')
|
||||
const directory = getAppPath('commands')
|
||||
commandLogger.info(`Loading commands from: ${directory}`)
|
||||
|
||||
try {
|
||||
const files: string[] = await fs.promises.readdir(commandsDir)
|
||||
const files = await fs.promises.readdir(directory, { withFileTypes: true })
|
||||
|
||||
for (const file of files) {
|
||||
await this.loadCommand(commandsDir, file)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[❌] Failed to read commands directory:', error)
|
||||
}
|
||||
if (!file.isFile() || (!file.name.endsWith('.ts') && !file.name.endsWith('.js'))) {
|
||||
continue
|
||||
}
|
||||
|
||||
private async loadCommand(commandsDir: string, file: string) {
|
||||
const fullPath = getAppPath('commands', file.name)
|
||||
const commandName = path.basename(file.name, path.extname(file.name))
|
||||
|
||||
try {
|
||||
const ext = path.extname(file)
|
||||
const commandName = path.basename(file, ext)
|
||||
const commandPath = path.join(commandsDir, file)
|
||||
const module = await import(commandPath)
|
||||
const module = await import(fullPath)
|
||||
if (typeof module.default !== 'function') {
|
||||
commandLogger.warn(`Unrecognized export in ${file.name}`)
|
||||
continue
|
||||
}
|
||||
|
||||
this.registerCommand(commandName, module.default)
|
||||
} catch (error) {
|
||||
console.error('[❌] Failed to load command:', file, error)
|
||||
commandLogger.error(`Error loading command ${file.name}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
commandLogger.error(`Failed to read commands directory: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
private registerCommand(name: string, command: (args: string[], io: Server) => void) {
|
||||
private registerCommand(name: string, CommandClass: any) {
|
||||
if (this.commands.has(name)) {
|
||||
console.warn(`Command '${name}' is already registered. Overwriting...`)
|
||||
commandLogger.warn(`Command '${name}' is already registered. Overwriting...`)
|
||||
}
|
||||
this.commands.set(name, command)
|
||||
console.log(`Registered command: ${name}`)
|
||||
this.commands.set(name, CommandClass)
|
||||
commandLogger.info(`Registered command: ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
68
src/managers/dateManager.ts
Normal file
68
src/managers/dateManager.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { appLogger } from '../utilities/logger'
|
||||
import { getRootPath } from '../utilities/storage'
|
||||
import { readJsonValue, setJsonValue } from '../utilities/json'
|
||||
|
||||
class DateManager {
|
||||
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 currentDate: Date = new Date()
|
||||
|
||||
public async boot(io: Server): Promise<void> {
|
||||
this.io = io
|
||||
await this.loadDate()
|
||||
this.startDateLoop()
|
||||
appLogger.info('Date manager loaded')
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId)
|
||||
this.intervalId = null
|
||||
}
|
||||
}
|
||||
|
||||
private async loadDate(): Promise<void> {
|
||||
try {
|
||||
const dateString = await readJsonValue<string>(this.getWorldFilePath(), 'date')
|
||||
this.currentDate = new Date(dateString)
|
||||
} catch (error) {
|
||||
appLogger.error(`Failed to load date: ${error instanceof Error ? error.message : String(error)}`)
|
||||
this.currentDate = new Date() // Use current date as fallback
|
||||
}
|
||||
}
|
||||
|
||||
private startDateLoop(): void {
|
||||
this.intervalId = setInterval(() => {
|
||||
this.advanceGameTime()
|
||||
this.emitDate()
|
||||
this.saveDate()
|
||||
}, DateManager.UPDATE_INTERVAL)
|
||||
}
|
||||
|
||||
private advanceGameTime(): void {
|
||||
const advanceMilliseconds = DateManager.GAME_SPEED * DateManager.UPDATE_INTERVAL
|
||||
this.currentDate = new Date(this.currentDate.getTime() + advanceMilliseconds)
|
||||
}
|
||||
|
||||
private emitDate(): void {
|
||||
this.io?.emit('date', this.currentDate)
|
||||
}
|
||||
|
||||
private async saveDate(): Promise<void> {
|
||||
try {
|
||||
await setJsonValue(this.getWorldFilePath(), 'date', this.currentDate)
|
||||
} catch (error) {
|
||||
appLogger.error(`Failed to save date: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
private getWorldFilePath(): string {
|
||||
return getRootPath('data', 'world.json')
|
||||
}
|
||||
}
|
||||
|
||||
export default new DateManager()
|
@ -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 path from 'path'
|
||||
import { getAppPath } from '../utilities/storage'
|
||||
|
||||
class QueueManager {
|
||||
private connection!: IORedis
|
||||
@ -52,9 +52,9 @@ class QueueManager {
|
||||
const { jobName, params, socketId } = job.data
|
||||
|
||||
try {
|
||||
const jobsDir = path.join(process.cwd(), 'src', 'jobs')
|
||||
const jobsDir = getAppPath('jobs')
|
||||
const extension = config.ENV === 'development' ? '.ts' : '.js'
|
||||
const jobPath = path.join(jobsDir, `${jobName}${extension}`)
|
||||
const jobPath = getAppPath('jobs', `${jobName}${extension}`)
|
||||
|
||||
if (!fs.existsSync(jobPath)) {
|
||||
queueLogger.warn(`Job file not found: ${jobPath}`)
|
||||
|
0
src/managers/weatherManager.ts
Normal file
0
src/managers/weatherManager.ts
Normal file
@ -2,8 +2,7 @@ import { Zone } from '@prisma/client'
|
||||
import ZoneRepository from '../repositories/zoneRepository'
|
||||
import ZoneService from '../services/zoneService'
|
||||
import LoadedZone from '../models/loadedZone'
|
||||
import zoneRepository from '../repositories/zoneRepository'
|
||||
import { gameMasterLogger } from '../utilities/logger'
|
||||
import { gameLogger } from '../utilities/logger'
|
||||
|
||||
class ZoneManager {
|
||||
private loadedZones: LoadedZone[] = []
|
||||
@ -21,36 +20,20 @@ class ZoneManager {
|
||||
await this.loadZone(zone)
|
||||
}
|
||||
|
||||
gameMasterLogger.info('Zone manager loaded')
|
||||
}
|
||||
|
||||
public async getZoneAssets(zone: Zone): Promise<ZoneAssets> {
|
||||
const tiles: string[] = this.getUnique((JSON.parse(JSON.stringify(zone.tiles)) as string[][]).reduce((acc, val) => [...acc, ...val]))
|
||||
const objects = await zoneRepository.getObjects(zone.id)
|
||||
const mappedObjects = this.getUnique(objects.map((x) => x.objectId))
|
||||
|
||||
return {
|
||||
tiles: tiles,
|
||||
objects: mappedObjects
|
||||
} as ZoneAssets
|
||||
}
|
||||
|
||||
private getUnique<T>(array: T[]) {
|
||||
return [...new Set<T>(array)]
|
||||
gameLogger.info('Zone manager loaded')
|
||||
}
|
||||
|
||||
// Method to handle individual zoneEditor loading
|
||||
public async loadZone(zone: Zone) {
|
||||
const loadedZone = new LoadedZone(zone)
|
||||
this.loadedZones.push(loadedZone)
|
||||
await this.getZoneAssets(zone)
|
||||
gameMasterLogger.info(`Zone ID ${zone.id} loaded`)
|
||||
gameLogger.info(`Zone ID ${zone.id} loaded`)
|
||||
}
|
||||
|
||||
// Method to handle individual zoneEditor unloading
|
||||
public unloadZone(zoneId: number) {
|
||||
this.loadedZones = this.loadedZones.filter((loadedZone) => loadedZone.getZone().id !== zoneId)
|
||||
gameMasterLogger.info(`Zone ID ${zoneId} unloaded`)
|
||||
gameLogger.info(`Zone ID ${zoneId} unloaded`)
|
||||
}
|
||||
|
||||
// Getter for loaded zones
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { Zone } from '@prisma/client'
|
||||
import zoneRepository from '../repositories/zoneRepository'
|
||||
import characterManager from '../managers/characterManager'
|
||||
import { ExtendedCharacter } from '../utilities/types'
|
||||
|
||||
class LoadedZone {
|
||||
private readonly zone: Zone
|
||||
|
10
src/repositories/characterTypeRepository.ts
Normal file
10
src/repositories/characterTypeRepository.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
||||
import { CharacterType } from '@prisma/client'
|
||||
|
||||
class CharacterTypeRepository {
|
||||
async getAll(): Promise<CharacterType[]> {
|
||||
return prisma.characterType.findMany()
|
||||
}
|
||||
}
|
||||
|
||||
export default new CharacterTypeRepository()
|
44
src/repositories/passwordResetTokenRepository.ts
Normal file
44
src/repositories/passwordResetTokenRepository.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
||||
|
||||
class PasswordResetTokenRepository {
|
||||
async getById(id: number): Promise<any> {
|
||||
try {
|
||||
return await prisma.passwordResetToken.findUnique({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
throw new Error(`Failed to get password reset token by ID: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async getByUserId(userId: number): Promise<any> {
|
||||
try {
|
||||
return await prisma.passwordResetToken.findFirst({
|
||||
where: {
|
||||
userId
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
throw new Error(`Failed to get password reset token by user ID: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async getByToken(token: string): Promise<any> {
|
||||
try {
|
||||
return await prisma.passwordResetToken.findFirst({
|
||||
where: {
|
||||
token
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
throw new Error(`Failed to get password reset token by token: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new PasswordResetTokenRepository()
|
@ -1,5 +1,5 @@
|
||||
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
||||
import { Sprite, SpriteAction } from '@prisma/client'
|
||||
import { SpriteAction } from '@prisma/client'
|
||||
|
||||
class SpriteRepository {
|
||||
async getById(id: string) {
|
||||
|
@ -1,5 +1,8 @@
|
||||
import prisma from '../utilities/prisma' // Import the global Prisma instance
|
||||
import { Tile } from '@prisma/client'
|
||||
import zoneRepository from './zoneRepository'
|
||||
import { unduplicateArray } from '../utilities/utilities'
|
||||
import { FlattenZoneArray } from '../utilities/zone'
|
||||
|
||||
class TileRepository {
|
||||
async getById(id: string): Promise<Tile | null> {
|
||||
@ -8,9 +11,28 @@ class TileRepository {
|
||||
})
|
||||
}
|
||||
|
||||
async getByIds(ids: string[]): Promise<Tile[]> {
|
||||
return prisma.tile.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: ids
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getAll(): Promise<Tile[]> {
|
||||
return prisma.tile.findMany()
|
||||
}
|
||||
|
||||
async getByZoneId(zoneId: number): Promise<Tile[]> {
|
||||
const zone = await zoneRepository.getById(zoneId)
|
||||
if (!zone) return []
|
||||
|
||||
const zoneTileArray = unduplicateArray(FlattenZoneArray(JSON.parse(JSON.stringify(zone.tiles))))
|
||||
|
||||
return this.getByIds(zoneTileArray)
|
||||
}
|
||||
}
|
||||
|
||||
export default new TileRepository()
|
||||
|
@ -27,6 +27,19 @@ class UserRepository {
|
||||
throw new Error(`Failed to get user by username: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async getByEmail(email: string): Promise<User | null> {
|
||||
try {
|
||||
return await prisma.user.findUnique({
|
||||
where: {
|
||||
email
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Handle error
|
||||
throw new Error(`Failed to get user by email: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new UserRepository()
|
||||
|
@ -2,6 +2,8 @@ import { Zone, ZoneEventTile, ZoneEventTileType, ZoneObject } from '@prisma/clie
|
||||
import prisma from '../utilities/prisma'
|
||||
import { ZoneEventTileWithTeleport } from '../socketEvents/zone/characterMove'
|
||||
import { appLogger } from '../utilities/logger'
|
||||
import { AssetData } from '../utilities/types'
|
||||
import tileRepository from './tileRepository'
|
||||
|
||||
class ZoneRepository {
|
||||
async getFirst(): Promise<Zone | null> {
|
||||
@ -22,7 +24,7 @@ class ZoneRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async getById(id: number): Promise<Zone | null> {
|
||||
async getById(id: number) {
|
||||
try {
|
||||
return await prisma.zone.findUnique({
|
||||
where: {
|
||||
@ -39,7 +41,8 @@ class ZoneRepository {
|
||||
include: {
|
||||
object: true
|
||||
}
|
||||
}
|
||||
},
|
||||
zoneEffects: true
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
@ -76,7 +79,7 @@ class ZoneRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async getObjects(id: number): Promise<ZoneObject[]> {
|
||||
async getZoneObjects(id: number): Promise<ZoneObject[]> {
|
||||
try {
|
||||
return await prisma.zoneObject.findMany({
|
||||
where: {
|
||||
|
@ -1,21 +1,21 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import express, { Application } from 'express'
|
||||
import config from './utilities/config'
|
||||
import { getAppPath } from './utilities/storage'
|
||||
import { createServer as httpServer, Server as HTTPServer } from 'http'
|
||||
import { addHttpRoutes } from './utilities/http'
|
||||
import cors from 'cors'
|
||||
import { Server as SocketServer } from 'socket.io'
|
||||
import { Authentication } from './middleware/authentication'
|
||||
import { TSocket } from './utilities/types'
|
||||
import config from './utilities/config'
|
||||
import prisma from './utilities/prisma'
|
||||
import { appLogger, watchLogs } from './utilities/logger'
|
||||
import ZoneManager from './managers/zoneManager'
|
||||
import UserManager from './managers/userManager'
|
||||
import { Authentication } from './middleware/authentication'
|
||||
// import CommandManager from './managers/CommandManager'
|
||||
import { Dirent } from 'node:fs'
|
||||
import { appLogger, watchLogs } from './utilities/logger'
|
||||
import CommandManager from './managers/commandManager'
|
||||
import CharacterManager from './managers/characterManager'
|
||||
import QueueManager from './managers/queueManager'
|
||||
import DateManager from './managers/dateManager'
|
||||
|
||||
export class Server {
|
||||
private readonly app: Application
|
||||
@ -58,23 +58,26 @@ export class Server {
|
||||
appLogger.error(`Socket.IO failed to start: ${error.message}`)
|
||||
}
|
||||
|
||||
// Load queue manager
|
||||
await QueueManager.boot(this.io)
|
||||
|
||||
// Add http API routes
|
||||
await addHttpRoutes(this.app)
|
||||
|
||||
// Load queue manager
|
||||
await QueueManager.boot(this.io)
|
||||
|
||||
// Load user manager
|
||||
await UserManager.boot()
|
||||
|
||||
// Load date manager
|
||||
await DateManager.boot(this.io)
|
||||
|
||||
// Load zoneEditor manager
|
||||
await ZoneManager.boot()
|
||||
|
||||
// Load character manager
|
||||
await CharacterManager.boot()
|
||||
|
||||
// Load command manager - Disabled for now
|
||||
// await CommandManager.boot(this.io);
|
||||
// Load command manager
|
||||
await CommandManager.boot(this.io)
|
||||
|
||||
// Listen for socket connections
|
||||
this.io.on('connection', this.handleConnection.bind(this))
|
||||
@ -86,42 +89,46 @@ export class Server {
|
||||
* @private
|
||||
*/
|
||||
private async handleConnection(socket: TSocket) {
|
||||
const eventsPath = path.join(__dirname, 'socketEvents')
|
||||
try {
|
||||
await this.loadEventHandlers(eventsPath, socket)
|
||||
await this.loadEventHandlers('socketEvents', '', socket)
|
||||
} catch (error: any) {
|
||||
appLogger.error(`Failed to load event handlers: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async loadEventHandlers(dir: string, socket: TSocket) {
|
||||
const files: Dirent[] = await fs.promises.readdir(dir, { withFileTypes: true })
|
||||
private async loadEventHandlers(baseDir: string, subDir: string, socket: TSocket) {
|
||||
try {
|
||||
const fullDir = getAppPath(baseDir, subDir)
|
||||
const files = await fs.promises.readdir(fullDir, { withFileTypes: true })
|
||||
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(dir, file.name)
|
||||
const filePath = getAppPath(baseDir, subDir, file.name)
|
||||
|
||||
if (file.isDirectory()) {
|
||||
await this.loadEventHandlers(fullPath, socket)
|
||||
} else if (file.isFile() && (file.name.endsWith('.ts') || file.name.endsWith('.js'))) {
|
||||
await this.loadEventHandlers(baseDir, `${subDir}/${file.name}`, socket)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!file.isFile() || (!file.name.endsWith('.ts') && !file.name.endsWith('.js'))) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const module = await import(fullPath)
|
||||
if (typeof module.default === 'function') {
|
||||
if (module.default.prototype && module.default.prototype.listen) {
|
||||
// This is a class-based event
|
||||
const module = await import(filePath)
|
||||
if (typeof module.default !== 'function') {
|
||||
appLogger.warn(`Unrecognized export in ${file.name}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const EventClass = module.default
|
||||
const eventInstance = new EventClass(this.io, socket)
|
||||
eventInstance.listen()
|
||||
} else {
|
||||
// This is a function-based event
|
||||
module.default(this.io, socket)
|
||||
}
|
||||
} else {
|
||||
appLogger.warn(`Unrecognized export in ${file.name}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
appLogger.error(`Error loading event handler ${file.name}: ${error.message}`)
|
||||
} catch (error) {
|
||||
appLogger.error(`Error loading event handler ${file.name}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
appLogger.error(`Error reading directory: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
class AssetService {}
|
||||
|
||||
export default AssetService
|
@ -1,5 +0,0 @@
|
||||
import { Character } from '@prisma/client'
|
||||
|
||||
class CharacterService {}
|
||||
|
||||
export default CharacterService
|
@ -1,7 +1,10 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
import UserRepository from '../repositories/userRepository'
|
||||
import PasswordResetTokenRepository from '../repositories/passwordResetTokenRepository'
|
||||
import prisma from '../utilities/prisma'
|
||||
import { User } from '@prisma/client'
|
||||
import { User, PasswordResetToken } from '@prisma/client'
|
||||
import config from '../utilities/config'
|
||||
import NodeMailer from 'nodemailer'
|
||||
|
||||
/**
|
||||
* User service
|
||||
@ -31,18 +34,104 @@ class UserService {
|
||||
/**
|
||||
* Register user
|
||||
* @param username
|
||||
* @param email
|
||||
* @param password
|
||||
*/
|
||||
async register(username: string, password: string): Promise<boolean | User> {
|
||||
async register(username: string, email: string, password: string): Promise<boolean | User> {
|
||||
const user = await UserRepository.getByUsername(username)
|
||||
if (user) {
|
||||
return false
|
||||
}
|
||||
|
||||
const userByEmail = await UserRepository.getByEmail(email)
|
||||
if (userByEmail) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10)
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
email,
|
||||
password: hashedPassword
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password
|
||||
* @param email
|
||||
*/
|
||||
async resetPassword(email: string): Promise<boolean> {
|
||||
|
||||
const user = await UserRepository.getByEmail(email)
|
||||
if ( !user ) return false
|
||||
|
||||
const token = await bcrypt.hash(new Date().getTime().toString(), 10)
|
||||
const latestToken = await PasswordResetTokenRepository.getByUserId(user.id)
|
||||
|
||||
//Check if password reset has been requested recently
|
||||
if (latestToken) {
|
||||
|
||||
const tokenExpiryDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
const isTokenExpired = latestToken.createdAt < tokenExpiryDate
|
||||
|
||||
if (!isTokenExpired) return false
|
||||
|
||||
await prisma.passwordResetToken.delete({
|
||||
where: {
|
||||
id: latestToken.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await prisma.passwordResetToken.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
token: token,
|
||||
}
|
||||
});
|
||||
|
||||
const transporter = NodeMailer.createTransport({
|
||||
host: config.SMTP_HOST,
|
||||
port: config.SMTP_PORT,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: config.SMTP_USER,
|
||||
pass: config.SMTP_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: config.SMTP_USER,
|
||||
to: email,
|
||||
subject: "Reset your password",
|
||||
text: "A password reset has been requested, reset your password here: " + config.CLIENT_URL + "#" + token, // Plain text body
|
||||
html: "<p>A password reset has been requested, reset your password here: <a href='" + config.CLIENT_URL + "#" + token + "'>" + config.CLIENT_URL + "#" + token + "</a></p>", // Html body
|
||||
});
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new password
|
||||
* @param urlToken
|
||||
* @param password
|
||||
*/
|
||||
async newPassword(urlToken: string, password: string): Promise<boolean | User> {
|
||||
const tokenData = await PasswordResetTokenRepository.getByToken(urlToken)
|
||||
if (!tokenData) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10)
|
||||
return prisma.user.update({
|
||||
where: { id: tokenData.userId },
|
||||
data: {
|
||||
password: hashedPassword
|
||||
}
|
||||
})
|
||||
|
@ -23,14 +23,17 @@ export class ZoneEventTileService {
|
||||
data: {
|
||||
zoneId: newZoneId,
|
||||
positionX: teleport.toPositionX,
|
||||
positionY: teleport.toPositionY
|
||||
positionY: teleport.toPositionY,
|
||||
rotation: teleport.toRotation
|
||||
}
|
||||
})
|
||||
|
||||
// Update local character object
|
||||
character.zoneId = newZoneId
|
||||
character.rotation = teleport.toRotation
|
||||
character.positionX = teleport.toPositionX
|
||||
character.positionY = teleport.toPositionY
|
||||
character.isMoving = false
|
||||
|
||||
// Emit events
|
||||
io.to(oldZoneId.toString()).emit('zone:character:leave', character.id)
|
||||
|
@ -1,4 +1,14 @@
|
||||
import prisma from '../utilities/prisma'
|
||||
import { AssetData } from '../utilities/types'
|
||||
import tileRepository from '../repositories/tileRepository'
|
||||
import zoneRepository from '../repositories/zoneRepository'
|
||||
import { Object, Zone, ZoneObject } from '@prisma/client'
|
||||
|
||||
type getZoneAsetsZoneType = Zone & {
|
||||
zoneObjects: (ZoneObject & {
|
||||
object: Object
|
||||
})[]
|
||||
}
|
||||
|
||||
class ZoneService {
|
||||
async createDemoZone(): Promise<boolean> {
|
||||
@ -27,6 +37,49 @@ class ZoneService {
|
||||
console.log('Demo zone created.')
|
||||
return true
|
||||
}
|
||||
|
||||
async getZoneAssets(zone: getZoneAsetsZoneType): Promise<AssetData[]> {
|
||||
const assets: AssetData[] = []
|
||||
|
||||
// zone.tiles is prisma jsonvalue
|
||||
let tiles = JSON.parse(JSON.stringify(zone.tiles))
|
||||
tiles = [...new Set(tiles.flat())]
|
||||
|
||||
// Add tile assets
|
||||
for (const tile of tiles) {
|
||||
const tileInfo = await tileRepository.getById(tile)
|
||||
if (!tileInfo) continue
|
||||
|
||||
assets.push({
|
||||
key: tileInfo.id,
|
||||
data: '/assets/tiles/' + tileInfo.id + '.png',
|
||||
group: 'tiles',
|
||||
updatedAt: tileInfo?.updatedAt || new Date()
|
||||
} as AssetData)
|
||||
}
|
||||
|
||||
// Add object assets
|
||||
for (const zoneObject of zone.zoneObjects) {
|
||||
if (!zoneObject.object) continue
|
||||
|
||||
assets.push({
|
||||
key: zoneObject.object.id,
|
||||
data: '/assets/objects/' + zoneObject.object.id + '.png',
|
||||
group: 'objects',
|
||||
updatedAt: zoneObject.object.updatedAt || new Date()
|
||||
} as AssetData)
|
||||
}
|
||||
|
||||
// Filter out duplicate assets
|
||||
return assets.reduce((acc: AssetData[], current) => {
|
||||
const x = acc.find((item) => item.key === current.key && item.group === current.group)
|
||||
if (!x) {
|
||||
return acc.concat([current])
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
}
|
||||
|
||||
export default ZoneService
|
||||
|
@ -1,24 +1,31 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket, ExtendedCharacter } from '../../utilities/types'
|
||||
import { TSocket } from '../../utilities/types'
|
||||
import CharacterRepository from '../../repositories/characterRepository'
|
||||
import CharacterManager from '../../managers/characterManager'
|
||||
|
||||
type SocketResponseT = {
|
||||
character_id: number
|
||||
}
|
||||
|
||||
export default function (io: Server, socket: TSocket) {
|
||||
socket.on('character:connect', async (data: SocketResponseT) => {
|
||||
export default class CharacterConnectEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('character:connect', this.handleCharacterConnect.bind(this))
|
||||
}
|
||||
|
||||
private async handleCharacterConnect(data: SocketResponseT): Promise<void> {
|
||||
console.log('character:connect requested', data)
|
||||
try {
|
||||
const character = await CharacterRepository.getByUserAndId(socket?.user?.id as number, data.character_id)
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket?.user?.id as number, data.character_id)
|
||||
if (!character) return
|
||||
socket.characterId = character.id
|
||||
|
||||
CharacterManager.initCharacter(character as ExtendedCharacter)
|
||||
socket.emit('character:connect', character)
|
||||
this.socket.characterId = character.id
|
||||
this.socket.emit('character:connect', character)
|
||||
} catch (error: any) {
|
||||
console.log('character:connect error', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -5,27 +5,37 @@ import CharacterRepository from '../../repositories/characterRepository'
|
||||
import { ZCharacterCreate } from '../../utilities/zodTypes'
|
||||
import prisma from '../../utilities/prisma'
|
||||
import { gameLogger } from '../../utilities/logger'
|
||||
import { ZodError } from 'zod'
|
||||
|
||||
export default function (io: Server, socket: TSocket) {
|
||||
socket.on('character:create', async (data: any) => {
|
||||
export default class CharacterCreateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('character:create', this.handleCharacterCreate.bind(this))
|
||||
}
|
||||
|
||||
private async handleCharacterCreate(data: any): Promise<any> {
|
||||
console.log('character:create requested', data)
|
||||
// zod validate
|
||||
try {
|
||||
data = ZCharacterCreate.parse(data)
|
||||
|
||||
const user_id = socket.user?.id as number
|
||||
const user_id = this.socket.user?.id as number
|
||||
|
||||
// Check if character name already exists
|
||||
const characterExists = await CharacterRepository.getByName(data.name)
|
||||
|
||||
if (characterExists) {
|
||||
return socket.emit('notification', { message: 'Character name already exists' })
|
||||
return this.socket.emit('notification', { message: 'Character name already exists' })
|
||||
}
|
||||
|
||||
let characters: Character[] = (await CharacterRepository.getByUserId(user_id)) as Character[]
|
||||
|
||||
if (characters.length >= 4) {
|
||||
return socket.emit('notification', { message: 'You can only have 4 characters' })
|
||||
return this.socket.emit('notification', { message: 'You can only have 4 characters' })
|
||||
}
|
||||
|
||||
const character: Character = await prisma.character.create({
|
||||
@ -38,14 +48,16 @@ export default function (io: Server, socket: TSocket) {
|
||||
|
||||
characters = [...characters, character]
|
||||
|
||||
socket.emit('character:create:success')
|
||||
socket.emit('character:list', characters)
|
||||
this.socket.emit('character:create:success')
|
||||
this.socket.emit('character:list', characters)
|
||||
|
||||
gameLogger.info('character:create success')
|
||||
} catch (error: any) {
|
||||
console.log(error)
|
||||
gameLogger.error(`character:create error: ${error.message}`)
|
||||
return socket.emit('notification', { message: 'Could not create character. Please try again (later).' })
|
||||
if (error instanceof ZodError) {
|
||||
return this.socket.emit('notification', { message: error.issues[0].message })
|
||||
}
|
||||
return this.socket.emit('notification', { message: 'Could not create character. Please try again (later).' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -12,19 +12,28 @@ type TypeResponse = {
|
||||
characters: Character[]
|
||||
}
|
||||
|
||||
export default function (io: Server, socket: TSocket) {
|
||||
socket.on('character:delete', async (data: TypePayload, callback: (response: TypeResponse) => void) => {
|
||||
export default class CharacterDeleteEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('character:delete', this.handleCharacterDelete.bind(this))
|
||||
}
|
||||
|
||||
private async handleCharacterDelete(data: TypePayload, callback: (response: TypeResponse) => void): Promise<any> {
|
||||
// zod validate
|
||||
try {
|
||||
await CharacterRepository.deleteByUserIdAndId(socket.user?.id as number, data.character_id as number)
|
||||
await CharacterRepository.deleteByUserIdAndId(this.socket.user?.id as number, data.character_id as number)
|
||||
|
||||
const user_id = socket.user?.id as number
|
||||
const user_id = this.socket.user?.id as number
|
||||
const characters: Character[] = (await CharacterRepository.getByUserId(user_id)) as Character[]
|
||||
|
||||
socket.emit('character:list', characters)
|
||||
this.socket.emit('character:list', characters)
|
||||
} catch (error: any) {
|
||||
console.log(error)
|
||||
return socket.emit('notification', { message: 'Character delete failed. Please try again.' })
|
||||
return this.socket.emit('notification', { message: 'Character delete failed. Please try again.' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -3,15 +3,24 @@ import { TSocket } from '../../utilities/types'
|
||||
import { Character } from '@prisma/client'
|
||||
import CharacterRepository from '../../repositories/characterRepository'
|
||||
|
||||
export default function CharacterList(io: Server, socket: TSocket) {
|
||||
socket.on('character:list', async (data: any) => {
|
||||
export default class CharacterListEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('character:list', this.handleCharacterList.bind(this))
|
||||
}
|
||||
|
||||
private async handleCharacterList(data: any): Promise<void> {
|
||||
try {
|
||||
console.log('character:list requested')
|
||||
const user_id = socket.user?.id as number
|
||||
const user_id = this.socket.user?.id as number
|
||||
const characters: Character[] = (await CharacterRepository.getByUserId(user_id)) as Character[]
|
||||
socket.emit('character:list', characters)
|
||||
this.socket.emit('character:list', characters)
|
||||
} catch (error: any) {
|
||||
console.log('character:list error', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../utilities/types'
|
||||
import CharacterRepository from '../../repositories/characterRepository'
|
||||
import ZoneRepository from '../../repositories/zoneRepository'
|
||||
import { isCommand } from '../../utilities/chat'
|
||||
import { gameLogger } from '../../utilities/logger'
|
||||
import CharacterManager from '../../managers/characterManager'
|
||||
|
||||
type TypePayload = {
|
||||
message: string
|
||||
@ -26,7 +26,7 @@ export default class ChatMessageEvent {
|
||||
return
|
||||
}
|
||||
|
||||
const character = await CharacterRepository.getByUserAndId(this.socket.user?.id as number, this.socket.characterId as number)
|
||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
||||
if (!character) {
|
||||
gameLogger.error('chat:send_message error', 'Character not found')
|
||||
callback(false)
|
||||
|
@ -29,6 +29,8 @@ export default class DisconnectEvent {
|
||||
return
|
||||
}
|
||||
|
||||
character.resetMovement = true
|
||||
|
||||
gameLogger.info('User disconnected along with their character')
|
||||
|
||||
await CharacterManager.removeCharacter(character)
|
||||
|
@ -0,0 +1,40 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { CharacterGender, CharacterRace } from '@prisma/client'
|
||||
|
||||
export default class CharacterTypeCreateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterType:create', this.handleCharacterTypeCreate.bind(this))
|
||||
}
|
||||
|
||||
private async handleCharacterTypeCreate(data: undefined, callback: (response: boolean, characterType?: any) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const newCharacterType = await prisma.characterType.create({
|
||||
data: {
|
||||
name: 'New character type',
|
||||
gender: CharacterGender.MALE,
|
||||
race: CharacterRace.HUMAN
|
||||
}
|
||||
})
|
||||
|
||||
callback(true, newCharacterType)
|
||||
} catch (error) {
|
||||
console.error('Error creating character type:', error)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import { CharacterType } from '@prisma/client'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
import CharacterTypeRepository from '../../../../repositories/characterTypeRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
export default class CharacterTypeListEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:characterType:list', this.handleCharacterTypeList.bind(this))
|
||||
}
|
||||
|
||||
private async handleCharacterTypeList(data: IPayload, callback: (response: CharacterType[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
||||
gameMasterLogger.error('gm:characterType:list error', 'Character not found')
|
||||
return callback([])
|
||||
}
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
gameMasterLogger.info(`User ${character.id} tried to list character types but is not a game master.`)
|
||||
return callback([])
|
||||
}
|
||||
|
||||
// get all objects
|
||||
const items = await CharacterTypeRepository.getAll()
|
||||
callback(items)
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import fs from 'fs'
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
|
||||
interface IPayload {
|
||||
object: string
|
||||
}
|
||||
|
||||
export default class ObjectRemoveEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:remove', this.handleObjectRemove.bind(this))
|
||||
}
|
||||
|
||||
private async handleObjectRemove(data: IPayload, callback: (response: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.object.delete({
|
||||
where: {
|
||||
id: data.object
|
||||
}
|
||||
})
|
||||
|
||||
// get root path
|
||||
const public_folder = getPublicPath('objects')
|
||||
|
||||
// remove the tile from the disk
|
||||
const finalFilePath = getPublicPath('objects', data.object + '.png')
|
||||
fs.unlink(finalFilePath, (err) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
|
||||
callback(true)
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
name: string
|
||||
tags: string[]
|
||||
originX: number
|
||||
originY: number
|
||||
isAnimated: boolean
|
||||
frameSpeed: number
|
||||
frameWidth: number
|
||||
frameHeight: number
|
||||
}
|
||||
|
||||
export default class ObjectUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:update', this.handleObjectUpdate.bind(this))
|
||||
}
|
||||
|
||||
private async handleObjectUpdate(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
try {
|
||||
const object = await prisma.object.update({
|
||||
where: {
|
||||
id: data.id
|
||||
},
|
||||
data: {
|
||||
name: data.name,
|
||||
tags: data.tags,
|
||||
originX: data.originX,
|
||||
originY: data.originY,
|
||||
isAnimated: data.isAnimated,
|
||||
frameSpeed: data.frameSpeed,
|
||||
frameWidth: data.frameWidth,
|
||||
frameHeight: data.frameHeight
|
||||
}
|
||||
})
|
||||
callback(true)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
callback(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -2,19 +2,22 @@ import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import { Object } from '@prisma/client'
|
||||
import ObjectRepository from '../../../../repositories/objectRepository'
|
||||
import CharacterManager from '../../../../managers/characterManager'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
/**
|
||||
* Handle game master list object event
|
||||
* @param socket
|
||||
* @param io
|
||||
*/
|
||||
export default function (io: Server, socket: TSocket) {
|
||||
socket.on('gm:object:list', async (data: any, callback: (response: Object[]) => void) => {
|
||||
const character = await characterRepository.getById(socket.characterId as number)
|
||||
export default class ObjectListEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:list', this.handleObjectList.bind(this))
|
||||
}
|
||||
|
||||
private async handleObjectList(data: IPayload, callback: (response: Object[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback([])
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
@ -24,5 +27,5 @@ export default function (io: Server, socket: TSocket) {
|
||||
// get all objects
|
||||
const objects = await ObjectRepository.getAll()
|
||||
callback(objects)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,26 @@
|
||||
import fs from 'fs'
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import CharacterManager from '../../../../managers/characterManager'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
|
||||
interface IPayload {
|
||||
object: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle game master remove object event
|
||||
* @param socket
|
||||
* @param io
|
||||
*/
|
||||
export default function (io: Server, socket: TSocket) {
|
||||
socket.on('gm:object:remove', async (data: IPayload, callback: (response: boolean) => void) => {
|
||||
const character = await characterRepository.getById(socket.characterId as number)
|
||||
export default class ObjectRemoveEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:remove', this.handleObjectRemove.bind(this))
|
||||
}
|
||||
|
||||
private async handleObjectRemove(data: IPayload, callback: (response: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
@ -32,10 +35,10 @@ export default function (io: Server, socket: TSocket) {
|
||||
})
|
||||
|
||||
// get root path
|
||||
const public_folder = path.join(process.cwd(), 'public', 'objects')
|
||||
const public_folder = getPublicPath('objects')
|
||||
|
||||
// remove the tile from the disk
|
||||
const finalFilePath = path.join(public_folder, data.object + '.png')
|
||||
const finalFilePath = getPublicPath('objects', data.object + '.png')
|
||||
fs.unlink(finalFilePath, (err) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
@ -49,5 +52,5 @@ export default function (io: Server, socket: TSocket) {
|
||||
console.log(e)
|
||||
callback(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import CharacterManager from '../../../../managers/characterManager'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
@ -16,14 +15,18 @@ type Payload = {
|
||||
frameHeight: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle game master object update event
|
||||
* @param socket
|
||||
* @param io
|
||||
*/
|
||||
export default function (io: Server, socket: TSocket) {
|
||||
socket.on('gm:object:update', async (data: Payload, callback: (success: boolean) => void) => {
|
||||
const character = await characterRepository.getById(socket.characterId as number)
|
||||
export default class ObjectUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:object:update', this.handleObjectUpdate.bind(this))
|
||||
}
|
||||
|
||||
private async handleObjectUpdate(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
@ -51,5 +54,5 @@ export default function (io: Server, socket: TSocket) {
|
||||
console.error(error)
|
||||
callback(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import path from 'path'
|
||||
import fs from 'fs/promises'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import sharp from 'sharp'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
|
||||
interface IObjectData {
|
||||
[key: string]: Buffer
|
||||
@ -30,7 +30,7 @@ export default class ObjectUploadEvent {
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
const public_folder = path.join(process.cwd(), 'public', 'objects')
|
||||
const public_folder = getPublicPath('objects')
|
||||
|
||||
// Ensure the folder exists
|
||||
await fs.mkdir(public_folder, { recursive: true })
|
||||
@ -54,7 +54,7 @@ export default class ObjectUploadEvent {
|
||||
|
||||
const uuid = object.id
|
||||
const filename = `${uuid}.png`
|
||||
const finalFilePath = path.join(public_folder, filename)
|
||||
const finalFilePath = getPublicPath('objects', filename)
|
||||
await writeFile(finalFilePath, objectData)
|
||||
|
||||
gameMasterLogger.info('gm:object:upload', `Object ${key} uploaded with id ${uuid}`)
|
||||
|
@ -1,27 +1,30 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import path from 'path'
|
||||
import fs from 'fs/promises'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import CharacterManager from '../../../../managers/characterManager'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
|
||||
/**
|
||||
* Handle game master new sprite event
|
||||
* @param socket
|
||||
* @param io
|
||||
*/
|
||||
export default function (io: Server, socket: TSocket) {
|
||||
socket.on('gm:sprite:create', async (data: undefined, callback: (response: boolean) => void) => {
|
||||
export default class SpriteCreateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:sprite:create', this.handleSpriteCreate.bind(this))
|
||||
}
|
||||
|
||||
private async handleSpriteCreate(data: undefined, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(socket.characterId as number)
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
|
||||
const public_folder = path.join(process.cwd(), 'public', 'sprites')
|
||||
const public_folder = getPublicPath('sprites')
|
||||
|
||||
// Ensure the folder exists
|
||||
await fs.mkdir(public_folder, { recursive: true })
|
||||
@ -34,7 +37,7 @@ export default function (io: Server, socket: TSocket) {
|
||||
const uuid = sprite.id
|
||||
|
||||
// Create folder with uuid
|
||||
const sprite_folder = path.join(public_folder, uuid)
|
||||
const sprite_folder = getPublicPath('sprites', uuid)
|
||||
await fs.mkdir(sprite_folder, { recursive: true })
|
||||
|
||||
callback(true)
|
||||
@ -42,5 +45,5 @@ export default function (io: Server, socket: TSocket) {
|
||||
console.error('Error creating sprite:', error)
|
||||
callback(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import CharacterManager from '../../../../managers/characterManager'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
@ -17,7 +17,7 @@ export default class GMSpriteDeleteEvent {
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {
|
||||
this.public_folder = path.join(process.cwd(), 'public', 'sprites')
|
||||
this.public_folder = getPublicPath('sprites')
|
||||
}
|
||||
|
||||
public listen(): void {
|
||||
@ -43,7 +43,7 @@ export default class GMSpriteDeleteEvent {
|
||||
}
|
||||
|
||||
private async deleteSpriteFolder(spriteId: string): Promise<void> {
|
||||
const finalFilePath = path.join(this.public_folder, spriteId)
|
||||
const finalFilePath = getPublicPath('sprites', spriteId)
|
||||
|
||||
if (fs.existsSync(finalFilePath)) {
|
||||
await fs.promises.rmdir(finalFilePath, { recursive: true })
|
||||
|
@ -2,19 +2,22 @@ import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import { Sprite } from '@prisma/client'
|
||||
import SpriteRepository from '../../../../repositories/spriteRepository'
|
||||
import CharacterManager from '../../../../managers/characterManager'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
/**
|
||||
* Handle game master list sprite event
|
||||
* @param socket
|
||||
* @param io
|
||||
*/
|
||||
export default function (io: Server, socket: TSocket) {
|
||||
socket.on('gm:sprite:list', async (data: any, callback: (response: Sprite[]) => void) => {
|
||||
const character = await characterRepository.getById(socket.characterId as number)
|
||||
export default class SpriteListEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:sprite:list', this.handleSpriteList.bind(this))
|
||||
}
|
||||
|
||||
private async handleSpriteList(data: any, callback: (response: Sprite[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback([])
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
@ -24,5 +27,5 @@ export default function (io: Server, socket: TSocket) {
|
||||
// get all sprites
|
||||
const sprites = await SpriteRepository.getAll()
|
||||
callback(sprites)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,10 @@ import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import type { Prisma, SpriteAction } from '@prisma/client'
|
||||
import path from 'path'
|
||||
import { writeFile, mkdir } from 'node:fs/promises'
|
||||
import sharp from 'sharp'
|
||||
import CharacterManager from '../../../../managers/characterManager'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
|
||||
type SpriteActionInput = Omit<SpriteAction, 'id' | 'spriteId' | 'frameWidth' | 'frameHeight'> & {
|
||||
sprites: string[]
|
||||
@ -27,9 +27,18 @@ interface ProcessedSpriteAction extends SpriteActionInput {
|
||||
}>
|
||||
}
|
||||
|
||||
export default function (io: Server, socket: TSocket) {
|
||||
socket.on('gm:sprite:update', async (data: Payload, callback: (success: boolean) => void) => {
|
||||
const character = CharacterManager.getCharacterFromSocket(socket)
|
||||
export default class SpriteUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:sprite:update', this.handleSpriteUpdate.bind(this))
|
||||
}
|
||||
|
||||
private async handleSpriteUpdate(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
||||
if (character?.role !== 'gm') {
|
||||
return callback(false)
|
||||
}
|
||||
@ -46,8 +55,6 @@ export default function (io: Server, socket: TSocket) {
|
||||
console.error('Error updating sprite:', error)
|
||||
callback(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function validateSpriteActions(spriteActions: Prisma.JsonValue): SpriteActionInput[] {
|
||||
try {
|
||||
@ -116,7 +123,7 @@ async function updateDatabase(id: string, name: string, processedActions: Proces
|
||||
}
|
||||
|
||||
async function saveSpritesToDisk(id: string, processedActions: ProcessedSpriteAction[]) {
|
||||
const publicFolder = path.join(process.cwd(), 'public', 'sprites', id)
|
||||
const publicFolder = getPublicPath('sprites', id)
|
||||
await mkdir(publicFolder, { recursive: true })
|
||||
|
||||
await Promise.all(
|
||||
@ -139,8 +146,10 @@ async function saveSpritesToDisk(id: string, processedActions: ProcessedSpriteAc
|
||||
.png()
|
||||
.toBuffer()
|
||||
|
||||
const filename = path.join(publicFolder, `${action}.png`)
|
||||
const filename = getPublicPath('sprites', id, `${action}.png`)
|
||||
await writeFile(filename, combinedImage)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs/promises'
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
|
||||
type Payload = {
|
||||
id: string
|
||||
@ -17,7 +17,7 @@ export default class GMTileDeleteEvent {
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {
|
||||
this.public_folder = path.join(process.cwd(), 'public', 'tiles')
|
||||
this.public_folder = getPublicPath('tiles')
|
||||
}
|
||||
|
||||
public listen(): void {
|
||||
@ -54,7 +54,7 @@ export default class GMTileDeleteEvent {
|
||||
}
|
||||
|
||||
private async deleteTileFile(tileId: string): Promise<void> {
|
||||
const finalFilePath = path.join(this.public_folder, `${tileId}.png`)
|
||||
const finalFilePath = getPublicPath('tiles', `${tileId}.png`)
|
||||
try {
|
||||
await fs.unlink(finalFilePath)
|
||||
} catch (error: any) {
|
||||
|
@ -2,19 +2,22 @@ import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import { Tile } from '@prisma/client'
|
||||
import TileRepository from '../../../../repositories/tileRepository'
|
||||
import CharacterManager from '../../../../managers/characterManager'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
|
||||
interface IPayload {}
|
||||
|
||||
/**
|
||||
* Handle game master list tile event
|
||||
* @param socket
|
||||
* @param io
|
||||
*/
|
||||
export default function (io: Server, socket: TSocket) {
|
||||
socket.on('gm:tile:list', async (data: any, callback: (response: Tile[]) => void) => {
|
||||
const character = await characterRepository.getById(socket.characterId as number)
|
||||
export default class TileListEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:tile:list', this.handleTileList.bind(this))
|
||||
}
|
||||
|
||||
private async handleTileList(data: any, callback: (response: Tile[]) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
@ -24,5 +27,5 @@ export default function (io: Server, socket: TSocket) {
|
||||
// get all tiles
|
||||
const tiles = await TileRepository.getAll()
|
||||
callback(tiles)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import CharacterManager from '../../../../managers/characterManager'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
|
||||
type Payload = {
|
||||
@ -10,14 +9,18 @@ type Payload = {
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle game master tile update event
|
||||
* @param socket
|
||||
* @param io
|
||||
*/
|
||||
export default function (io: Server, socket: TSocket) {
|
||||
socket.on('gm:tile:update', async (data: Payload, callback: (success: boolean) => void) => {
|
||||
const character = await characterRepository.getById(socket.characterId as number)
|
||||
export default class TileUpdateEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:tile:update', this.handleTileUpdate.bind(this))
|
||||
}
|
||||
|
||||
private async handleTileUpdate(data: Payload, callback: (success: boolean) => void): Promise<void> {
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
@ -40,5 +43,5 @@ export default function (io: Server, socket: TSocket) {
|
||||
console.error(error)
|
||||
callback(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +1,36 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../../utilities/types'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import path from 'path'
|
||||
import fs from 'fs/promises'
|
||||
import prisma from '../../../../utilities/prisma'
|
||||
import characterRepository from '../../../../repositories/characterRepository'
|
||||
import { gameMasterLogger } from '../../../../utilities/logger'
|
||||
import { getPublicPath } from '../../../../utilities/storage'
|
||||
|
||||
interface ITileData {
|
||||
[key: string]: Buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle game master upload tile event
|
||||
* @param socket
|
||||
* @param io
|
||||
*/
|
||||
export default function (io: Server, socket: TSocket) {
|
||||
socket.on('gm:tile:upload', async (data: ITileData, callback: (response: boolean) => void) => {
|
||||
export default class TileUploadEvent {
|
||||
constructor(
|
||||
private readonly io: Server,
|
||||
private readonly socket: TSocket
|
||||
) {}
|
||||
|
||||
public listen(): void {
|
||||
this.socket.on('gm:tile:upload', this.handleTileUpload.bind(this))
|
||||
}
|
||||
|
||||
private async handleTileUpload(data: ITileData, callback: (response: boolean) => void): Promise<void> {
|
||||
try {
|
||||
const character = await characterRepository.getById(socket.characterId as number)
|
||||
const character = await characterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) return callback(false)
|
||||
|
||||
if (character.role !== 'gm') {
|
||||
return
|
||||
}
|
||||
|
||||
const public_folder = path.join(process.cwd(), 'public', 'tiles')
|
||||
const public_folder = getPublicPath('tiles')
|
||||
|
||||
// Ensure the folder exists
|
||||
await fs.mkdir(public_folder, { recursive: true })
|
||||
@ -39,7 +43,7 @@ export default function (io: Server, socket: TSocket) {
|
||||
})
|
||||
const uuid = tile.id
|
||||
const filename = `${uuid}.png`
|
||||
const finalFilePath = path.join(public_folder, filename)
|
||||
const finalFilePath = getPublicPath('tiles', filename)
|
||||
await writeFile(finalFilePath, tileData)
|
||||
})
|
||||
|
||||
@ -50,5 +54,5 @@ export default function (io: Server, socket: TSocket) {
|
||||
gameMasterLogger.error('Error uploading tile:', error)
|
||||
callback(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../../utilities/types'
|
||||
import ZoneRepository from '../../../repositories/zoneRepository'
|
||||
import { Zone, ZoneEventTileType, ZoneObject } from '@prisma/client'
|
||||
import { Zone, ZoneEffect, ZoneEventTileType, ZoneObject } from '@prisma/client'
|
||||
import prisma from '../../../utilities/prisma'
|
||||
import zoneManager from '../../../managers/zoneManager'
|
||||
import CharacterRepository from '../../../repositories/characterRepository'
|
||||
@ -22,8 +22,13 @@ interface IPayload {
|
||||
toZoneId: number
|
||||
toPositionX: number
|
||||
toPositionY: number
|
||||
toRotation: number
|
||||
}
|
||||
}[]
|
||||
zoneEffects: {
|
||||
effect: string
|
||||
strength: number
|
||||
}[]
|
||||
zoneObjects: ZoneObject[]
|
||||
}
|
||||
|
||||
@ -88,7 +93,8 @@ export default class ZoneUpdateEvent {
|
||||
create: {
|
||||
toZoneId: zoneEventTile.teleport.toZoneId,
|
||||
toPositionX: zoneEventTile.teleport.toPositionX,
|
||||
toPositionY: zoneEventTile.teleport.toPositionY
|
||||
toPositionY: zoneEventTile.teleport.toPositionY,
|
||||
toRotation: zoneEventTile.teleport.toRotation
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -100,10 +106,19 @@ export default class ZoneUpdateEvent {
|
||||
create: data.zoneObjects.map((zoneObject) => ({
|
||||
objectId: zoneObject.objectId,
|
||||
depth: zoneObject.depth,
|
||||
isRotated: zoneObject.isRotated,
|
||||
positionX: zoneObject.positionX,
|
||||
positionY: zoneObject.positionY
|
||||
}))
|
||||
}
|
||||
},
|
||||
zoneEffects: {
|
||||
deleteMany: { zoneId: data.zoneId },
|
||||
create: data.zoneEffects.map((zoneEffect) => ({
|
||||
effect: zoneEffect.effect,
|
||||
strength: zoneEffect.strength
|
||||
}))
|
||||
},
|
||||
updatedAt: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
@ -115,6 +130,8 @@ export default class ZoneUpdateEvent {
|
||||
return
|
||||
}
|
||||
|
||||
gameMasterLogger.info(`User ${character.id} has updated zone via zone editor.`)
|
||||
|
||||
callback(zone)
|
||||
|
||||
zoneManager.unloadZone(data.zoneId)
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Server } from 'socket.io'
|
||||
import { TSocket } from '../../utilities/types'
|
||||
import { ExtendedCharacter, TSocket } from '../../utilities/types'
|
||||
import ZoneRepository from '../../repositories/zoneRepository'
|
||||
import { Character, Zone } from '@prisma/client'
|
||||
import CharacterManager from '../../managers/characterManager'
|
||||
import { gameLogger } from '../../utilities/logger'
|
||||
import CharacterRepository from '../../repositories/characterRepository'
|
||||
|
||||
interface IResponse {
|
||||
zone: Zone
|
||||
@ -22,10 +23,16 @@ export default class CharacterJoinEvent {
|
||||
|
||||
private async handleCharacterJoin(callback: (response: IResponse) => void): Promise<void> {
|
||||
try {
|
||||
if (!this.socket.characterId) return
|
||||
if (!this.socket.characterId) {
|
||||
gameLogger.error('zone:character:join error', 'Zone requested but no character id set')
|
||||
return
|
||||
}
|
||||
|
||||
const character = CharacterManager.getCharacterFromSocket(this.socket)
|
||||
if (!character) return
|
||||
const character = await CharacterRepository.getById(this.socket.characterId as number)
|
||||
if (!character) {
|
||||
gameLogger.error('zone:character:join error', 'Character not found')
|
||||
return
|
||||
}
|
||||
|
||||
const zone = await ZoneRepository.getById(character.zoneId)
|
||||
if (!zone) {
|
||||
@ -33,16 +40,16 @@ export default class CharacterJoinEvent {
|
||||
return
|
||||
}
|
||||
|
||||
if (character.zoneId) {
|
||||
this.socket.leave(character.zoneId.toString())
|
||||
this.io.to(character.zoneId.toString()).emit('zone:character:leave', character)
|
||||
}
|
||||
CharacterManager.initCharacter(character as ExtendedCharacter)
|
||||
|
||||
this.socket.join(zone.id.toString())
|
||||
|
||||
// let other clients know of new character
|
||||
this.io.to(zone.id.toString()).emit('zone:character:join', character)
|
||||
|
||||
// Log
|
||||
gameLogger.info(`User ${character.id} joined zone ${zone.id}`)
|
||||
|
||||
// send over zone and characters to socket
|
||||
callback({ zone, characters: CharacterManager.getCharactersInZone(zone) })
|
||||
} catch (error: any) {
|
||||
|
@ -1,11 +1,11 @@
|
||||
export function isCommand(message: string, command?: string) {
|
||||
if (command) {
|
||||
return message.startsWith(`:${command} `)
|
||||
return message.startsWith(`/${command} `)
|
||||
}
|
||||
return message.startsWith(':')
|
||||
return message.startsWith('/')
|
||||
}
|
||||
|
||||
export function getArgs(command: string, message: string): string[] | undefined {
|
||||
if (!isCommand(message, command)) return
|
||||
return message.split(`:${command} `)[1].split(' ')
|
||||
return message.split(`/${command} `)[1].split(' ')
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ class config {
|
||||
static REDIS_URL: string = process.env.REDIS_URL || 'redis://@127.0.0.1:6379/4'
|
||||
static HOST: string = process.env.HOST || '0.0.0.0'
|
||||
static PORT: number = process.env.PORT ? parseInt(process.env.PORT) : 6969
|
||||
static CLIENT_URL: string = process.env.CLIENT_URL ? process.env.CLIENT_URL : 'https://sylvan.quest/'
|
||||
static JWT_SECRET: string = process.env.JWT_SECRET || 'secret'
|
||||
|
||||
static ALLOW_DIAGONAL_MOVEMENT: boolean = process.env.ALLOW_DIAGONAL_MOVEMENT === 'true'
|
||||
@ -14,6 +15,11 @@ class config {
|
||||
static DEFAULT_CHARACTER_ZONE: number = parseInt(process.env.DEFAULT_CHARACTER_ZONE || '1')
|
||||
static DEFAULT_CHARACTER_X: number = parseInt(process.env.DEFAULT_CHARACTER_POS_X || '0')
|
||||
static DEFAULT_CHARACTER_Y: number = parseInt(process.env.DEFAULT_CHARACTER_POS_Y || '0')
|
||||
|
||||
static SMTP_HOST: string = process.env.SMTP_HOST || 'my.directonline.io'
|
||||
static SMTP_PORT: number = process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT) : 587
|
||||
static SMTP_USER: string = process.env.SMTP_USER || 'no-reply@sylvan.quest'
|
||||
static SMTP_PASSWORD: string = process.env.SMTP_PASSWORD || 'password'
|
||||
}
|
||||
|
||||
export default config
|
||||
|
@ -2,138 +2,16 @@ import { Application, Request, Response } from 'express'
|
||||
import UserService from '../services/userService'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import config from './config'
|
||||
import { loginAccountSchema, registerAccountSchema } from './zodTypes'
|
||||
import path from 'path'
|
||||
import { TAsset } from './types'
|
||||
import tileRepository from '../repositories/tileRepository'
|
||||
import objectRepository from '../repositories/objectRepository'
|
||||
import spriteRepository from '../repositories/spriteRepository'
|
||||
import { loginAccountSchema, registerAccountSchema, resetPasswordSchema, newPasswordSchema } from './zodTypes'
|
||||
import fs from 'fs'
|
||||
import zoneRepository from '../repositories/zoneRepository'
|
||||
import zoneManager from '../managers/zoneManager'
|
||||
import { httpLogger } from './logger'
|
||||
import { getPublicPath } from './storage'
|
||||
import TileRepository from '../repositories/tileRepository'
|
||||
import { AssetData } from './types'
|
||||
import ZoneRepository from '../repositories/zoneRepository'
|
||||
import SpriteRepository from '../repositories/spriteRepository'
|
||||
|
||||
async function addHttpRoutes(app: Application) {
|
||||
/**
|
||||
* Get all base sprite, assets
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
app.get('/assets/sprites', async (req: Request, res: Response) => {
|
||||
let assets: TAsset[] = []
|
||||
|
||||
const sprites = await spriteRepository.getAll()
|
||||
// sprites all contain spriteActions, loop through these
|
||||
sprites.forEach((sprite) => {
|
||||
sprite.spriteActions.forEach((spriteAction) => {
|
||||
assets.push({
|
||||
key: sprite.id + '-' + spriteAction.action,
|
||||
url: '/assets/sprites/' + sprite.id + '/' + spriteAction.action + '.png',
|
||||
group: spriteAction.isAnimated ? 'sprite_animations' : 'sprites',
|
||||
frameWidth: spriteAction.frameWidth,
|
||||
frameHeight: spriteAction.frameHeight
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
res.json(assets)
|
||||
})
|
||||
|
||||
/**
|
||||
* Get all assets for all zones
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
app.get('/assets/zone', async (req: Request, res: Response) => {
|
||||
const tiles = await tileRepository.getAll()
|
||||
const objects = await objectRepository.getAll()
|
||||
|
||||
const assets: TAsset[] = []
|
||||
tiles.forEach((tile) => {
|
||||
assets.push({
|
||||
key: tile.id,
|
||||
url: '/assets/tiles/' + tile.id + '.png',
|
||||
group: 'tiles'
|
||||
})
|
||||
})
|
||||
|
||||
objects.forEach((object) => {
|
||||
assets.push({
|
||||
key: object.id,
|
||||
url: '/assets/objects/' + object.id + '.png',
|
||||
group: 'objects'
|
||||
})
|
||||
})
|
||||
|
||||
res.json(assets)
|
||||
})
|
||||
|
||||
/**
|
||||
* Get assets for a specific zone
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
app.get('/assets/zone/:zoneId', async (req: Request, res: Response) => {
|
||||
const zoneId = req.params.zoneId
|
||||
if (!zoneId || parseInt(zoneId) === 0) {
|
||||
return res.status(400).json({ message: 'Invalid zone ID' })
|
||||
}
|
||||
|
||||
const zone = await zoneRepository.getById(parseInt(zoneId))
|
||||
if (!zone) {
|
||||
return res.status(404).json({ message: 'Zone not found' })
|
||||
}
|
||||
|
||||
const assets = await zoneManager.getZoneAssets(zone)
|
||||
|
||||
res.json([
|
||||
...assets.tiles.map((x) => {
|
||||
return {
|
||||
key: x,
|
||||
url: '/assets/tiles/' + x + '.png',
|
||||
group: 'tiles'
|
||||
}
|
||||
}),
|
||||
...assets.objects.map((x) => {
|
||||
return {
|
||||
key: x,
|
||||
url: '/assets/objects/' + x + '.png',
|
||||
group: 'objects'
|
||||
}
|
||||
})
|
||||
])
|
||||
})
|
||||
|
||||
/**
|
||||
* Get a specific asset
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
app.get('/assets/:type/:spriteId?/:file', (req: Request, res: Response) => {
|
||||
const assetType = req.params.type
|
||||
const spriteId = req.params.spriteId
|
||||
const fileName = req.params.file
|
||||
|
||||
let assetPath
|
||||
if (assetType === 'sprites' && spriteId) {
|
||||
assetPath = path.join(process.cwd(), 'public', assetType, spriteId, fileName)
|
||||
} else {
|
||||
assetPath = path.join(process.cwd(), 'public', assetType, fileName)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(assetPath)) {
|
||||
httpLogger.error(`File not found: ${assetPath}`)
|
||||
return res.status(404).send('Asset not found')
|
||||
}
|
||||
|
||||
res.sendFile(assetPath, (err) => {
|
||||
if (err) {
|
||||
httpLogger.error('Error sending file:', err)
|
||||
res.status(500).send('Error downloading the asset')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Login
|
||||
* @param req
|
||||
@ -165,16 +43,16 @@ async function addHttpRoutes(app: Application) {
|
||||
* @param res
|
||||
*/
|
||||
app.post('/register', async (req: Request, res: Response) => {
|
||||
const { username, password } = req.body
|
||||
const { username, email, password } = req.body
|
||||
|
||||
try {
|
||||
registerAccountSchema.parse({ username, password })
|
||||
registerAccountSchema.parse({ username, email, password })
|
||||
} catch (error: any) {
|
||||
return res.status(400).json({ message: error.errors[0]?.message })
|
||||
}
|
||||
|
||||
const userService = new UserService()
|
||||
const user = await userService.register(username, password)
|
||||
const user = await userService.register(username, email, password)
|
||||
|
||||
if (user) {
|
||||
return res.status(200).json({ message: 'User registered' })
|
||||
@ -183,6 +61,171 @@ async function addHttpRoutes(app: Application) {
|
||||
return res.status(400).json({ message: 'Failed to register user' })
|
||||
})
|
||||
|
||||
/**
|
||||
* Reset password
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
app.post('/reset-password', async (req: Request, res: Response) => {
|
||||
const { email } = req.body
|
||||
|
||||
try {
|
||||
resetPasswordSchema.parse({ email })
|
||||
} catch (error: any) {
|
||||
return res.status(400).json({ message: error.errors[0]?.message })
|
||||
}
|
||||
|
||||
const userService = new UserService()
|
||||
const sentEmail = await userService.resetPassword( email )
|
||||
|
||||
if (sentEmail) {
|
||||
return res.status(200).json({ message: 'Email has been sent' })
|
||||
}
|
||||
|
||||
return res.status(400).json({ message: 'Failed to send password reset request' })
|
||||
})
|
||||
|
||||
/**
|
||||
* New password
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
app.post('/new-password', async (req: Request, res: Response) => {
|
||||
const { urlToken, password } = req.body
|
||||
|
||||
try {
|
||||
newPasswordSchema.parse({ password })
|
||||
} catch (error: any) {
|
||||
return res.status(400).json({ message: error.errors[0]?.message })
|
||||
}
|
||||
|
||||
const userService = new UserService()
|
||||
const resetPassword = await userService.newPassword( urlToken, password )
|
||||
|
||||
if (resetPassword) {
|
||||
return res.status(200).json({ message: 'Password has been reset' })
|
||||
}
|
||||
|
||||
return res.status(400).json({ message: 'Failed to set new password' })
|
||||
})
|
||||
|
||||
/**
|
||||
* Get all tiles from a zone as an array of ids
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
app.get('/assets/list_tiles', async (req: Request, res: Response) => {
|
||||
// Get all tiles
|
||||
let assets: AssetData[] = []
|
||||
const tiles = await TileRepository.getAll()
|
||||
for (const tile of tiles) {
|
||||
assets.push({
|
||||
key: tile.id,
|
||||
data: '/assets/tiles/' + tile.id + '.png',
|
||||
group: 'tiles',
|
||||
updatedAt: tile.updatedAt
|
||||
} as AssetData)
|
||||
}
|
||||
|
||||
// Return the array
|
||||
res.json(assets)
|
||||
})
|
||||
|
||||
/**
|
||||
* Get all tiles from a zone and serve as AssetData array
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
app.get('/assets/list_tiles/:zoneId', async (req: Request, res: Response) => {
|
||||
const zoneId = req.params.zoneId
|
||||
|
||||
// Check if zoneId is valid number
|
||||
if (!zoneId || parseInt(zoneId) === 0) {
|
||||
return res.status(400).json({ message: 'Invalid zone ID' })
|
||||
}
|
||||
|
||||
// Get zone by id
|
||||
const zone = await ZoneRepository.getById(parseInt(zoneId))
|
||||
if (!zone) {
|
||||
return res.status(404).json({ message: 'Zone not found' })
|
||||
}
|
||||
|
||||
// Get all tiles
|
||||
let assets: AssetData[] = []
|
||||
const tiles = await TileRepository.getByZoneId(parseInt(zoneId))
|
||||
for (const tile of tiles) {
|
||||
assets.push({
|
||||
key: tile.id,
|
||||
data: '/assets/tiles/' + tile.id + '.png',
|
||||
group: 'tiles',
|
||||
updatedAt: tile.updatedAt
|
||||
} as AssetData)
|
||||
}
|
||||
|
||||
// Return the array
|
||||
res.json(assets)
|
||||
})
|
||||
|
||||
app.get('/assets/list_sprite_actions/:spriteId', async (req: Request, res: Response) => {
|
||||
const spriteId = req.params.spriteId
|
||||
// Check if spriteId is valid number
|
||||
if (!spriteId || parseInt(spriteId) === 0) {
|
||||
return res.status(400).json({ message: 'Invalid sprite ID' })
|
||||
}
|
||||
// Get sprite by id
|
||||
const sprite = await SpriteRepository.getById(spriteId)
|
||||
if (!sprite) {
|
||||
return res.status(404).json({ message: 'Sprite not found' })
|
||||
}
|
||||
|
||||
let assets: AssetData[] = []
|
||||
sprite.spriteActions.forEach((spriteAction) => {
|
||||
assets.push({
|
||||
key: sprite.id + '-' + spriteAction.action,
|
||||
data: '/assets/sprites/' + sprite.id + '/' + spriteAction.action + '.png',
|
||||
group: spriteAction.isAnimated ? 'sprite_animations' : 'sprites',
|
||||
updatedAt: sprite.updatedAt,
|
||||
isAnimated: spriteAction.isAnimated,
|
||||
frameCount: JSON.parse(JSON.stringify(spriteAction.sprites)).length,
|
||||
frameWidth: spriteAction.frameWidth,
|
||||
frameHeight: spriteAction.frameHeight
|
||||
})
|
||||
})
|
||||
|
||||
// Return the array
|
||||
res.json(assets)
|
||||
})
|
||||
|
||||
/**
|
||||
* Download asset file
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
app.get('/assets/:type/:spriteId?/:file', (req: Request, res: Response) => {
|
||||
const assetType = req.params.type
|
||||
const spriteId = req.params.spriteId
|
||||
const fileName = req.params.file
|
||||
|
||||
let assetPath
|
||||
if (assetType === 'sprites' && spriteId) {
|
||||
assetPath = getPublicPath(assetType, spriteId, fileName)
|
||||
} else {
|
||||
assetPath = getPublicPath(assetType, fileName)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(assetPath)) {
|
||||
httpLogger.error(`File not found: ${assetPath}`)
|
||||
return res.status(404).send('Asset not found')
|
||||
}
|
||||
|
||||
res.sendFile(assetPath, (err) => {
|
||||
if (err) {
|
||||
httpLogger.error('Error sending file:', err)
|
||||
res.status(500).send('Error downloading the asset')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
httpLogger.info('Web routes added')
|
||||
}
|
||||
|
||||
|
49
src/utilities/json.ts
Normal file
49
src/utilities/json.ts
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import pino from 'pino'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getRootPath } from './storage'
|
||||
|
||||
// Array of log types
|
||||
const LOG_TYPES = ['http', 'game', 'gameMaster', 'app', 'queue'] as const
|
||||
const LOG_TYPES = ['http', 'game', 'gameMaster', 'app', 'queue', 'command'] as const
|
||||
type LogType = (typeof LOG_TYPES)[number]
|
||||
|
||||
const createLogger = (name: LogType) =>
|
||||
@ -30,7 +30,7 @@ const loggers = Object.fromEntries(LOG_TYPES.map((type) => [type, createLogger(t
|
||||
|
||||
const watchLogs = () => {
|
||||
LOG_TYPES.forEach((type) => {
|
||||
const logFile = path.join(__dirname, '../../logs', `${type}.log`)
|
||||
const logFile = getRootPath('logs', `${type}.log`)
|
||||
|
||||
fs.watchFile(logFile, (curr, prev) => {
|
||||
if (curr.size > prev.size) {
|
||||
@ -43,6 +43,6 @@ const watchLogs = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const { http: httpLogger, game: gameLogger, gameMaster: gameMasterLogger, app: appLogger, queue: queueLogger } = loggers
|
||||
export const { http: httpLogger, game: gameLogger, gameMaster: gameMasterLogger, app: appLogger, queue: queueLogger, command: commandLogger } = loggers
|
||||
|
||||
export { watchLogs }
|
||||
|
33
src/utilities/storage.ts
Normal file
33
src/utilities/storage.ts
Normal file
@ -0,0 +1,33 @@
|
||||
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)
|
||||
}
|
||||
|
||||
export function getAppPath(folder: string, ...additionalSegments: string[]) {
|
||||
const baseDir = config.ENV === 'development' ? 'src' : 'dist'
|
||||
return path.join(process.cwd(), baseDir, folder, ...additionalSegments)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -21,14 +21,24 @@ export type ExtendedCharacter = Character & {
|
||||
resetMovement: boolean
|
||||
}
|
||||
|
||||
export type TAsset = {
|
||||
export type AssetData = {
|
||||
key: string
|
||||
url: string
|
||||
data: string
|
||||
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
|
||||
updatedAt: Date
|
||||
isAnimated?: boolean
|
||||
frameCount?: number
|
||||
frameWidth?: number
|
||||
frameHeight?: number
|
||||
}
|
||||
|
||||
export type WorldSettings = {
|
||||
date: Date
|
||||
isRainEnabled: boolean
|
||||
isFogEnabled: boolean
|
||||
fogDensity: number
|
||||
}
|
||||
|
||||
// export type TCharacter = Socket & {
|
||||
// user?: User
|
||||
// character?: Character
|
||||
|
3
src/utilities/utilities.ts
Normal file
3
src/utilities/utilities.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function unduplicateArray(array: any[]) {
|
||||
return [...new Set(array.flat())]
|
||||
}
|
@ -20,6 +20,28 @@ export const registerAccountSchema = z.object({
|
||||
.min(3, { message: 'Name must be at least 3 characters long' })
|
||||
.max(255, { message: 'Name must be at most 255 characters long' })
|
||||
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, { message: 'Name must start with a letter and can only contain letters, numbers, underscores, or dashes' }),
|
||||
email: z
|
||||
.string()
|
||||
.min(3, { message: 'Email must be at least 3 characters long' })
|
||||
.max(255, { message: 'Email must be at most 255 characters long' })
|
||||
.regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' }),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, {
|
||||
message: 'Password must be at least 8 characters long'
|
||||
})
|
||||
.max(255)
|
||||
})
|
||||
|
||||
export const resetPasswordSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(3, { message: 'Email must be at least 3 characters long' })
|
||||
.max(255, { message: 'Email must be at most 255 characters long' })
|
||||
.regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' })
|
||||
})
|
||||
|
||||
export const newPasswordSchema = z.object({
|
||||
password: z
|
||||
.string()
|
||||
.min(8, {
|
||||
|
9
src/utilities/zone.ts
Normal file
9
src/utilities/zone.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export function FlattenZoneArray(tiles: string[][]) {
|
||||
const normalArray = []
|
||||
|
||||
for (const row of tiles) {
|
||||
normalArray.push(...row)
|
||||
}
|
||||
|
||||
return normalArray
|
||||
}
|
Reference in New Issue
Block a user